how to make Intersection Observer to replicate bootstrap scroll spy behavior - javascript
I'm building a blazor application where I should keep java script code to minimal. So I'm using only bootstrap css in my parallax page. I have made scroll spy like behavior work with window.onscroll since it cannot be done with c# as of now; but it gives poor performance.
After googling, I recently came across IntersectionObserver API. so I thought of making my existing window.onscroll logic to work with IntersectionObserver API. However I'm not able to achieve it.
Here is what I'm trying to do. In my parallax page with fixed top nav bar and I would like to apply active css class to a.nav-items when the user scroll reaches the respective section he wants to view.
Here is the HTML:
<body data-spy="scroll" data-target=".site-nav" data-offset="55">
<header id="page-hero" class="site-header d-flex flex-column align-content-between">
<nav class="site-nav family-sans navbar navbar-expand-md navbar-dark fixed-top">
<div class="container-fluid">
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#myTogglerNav" aria-controls="myTogglerNav"
aria-label="Toggle Navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand text-uppercase" href="#page-hero">
<i class="fas fa-cube mr-2"></i> Layout Components</a>
<div class="collapse navbar-collapse" id="myTogglerNav">
<div class="navbar-nav ml-auto font-weight-regular text-uppercase">
<a class="nav-item nav-link" href="#page-hero">home</a>
<a class="nav-item nav-link" href="#page-multicolumn">columns</a>
<a class="nav-item nav-link" href="#page-media">media</a>
<a class="nav-item nav-link" href="#page-photogrid">grid</a>
<a class="nav-item nav-link" href="#page-carousel">carousel</a>
<a class="nav-item nav-link" href="#page-nested">nested</a>
<a class="nav-item nav-link" href="#page-icons">icons</a>
<a class="nav-item nav-link" href="#page-floater">floater</a>
<a class="nav-item nav-link" href="#page-cards">cards</a>
</div>
</div>
</div>
</nav>
<section class="layout-hero jumbotron jumbotron-fluid d-flex align-items-center mt-5 text-reverse">
<div class="container text-center">
......
......
......
<article id="page-multicolumn" class="page-section vertical-padding">
<header class="page-section-header container">
.....
.....
.....
<article id="page-media" class="page-section vertical-padding">
<header class="page-section-header container text-center">
....
I have article tag with id matching href of a in top .nav-item. I have only pasted few of the html as the skeleton is same for all article tags.
Here is my existing window.onscroll:
var sections = {};
document.querySelectorAll(".page-section,.site-header").forEach(section => sections[section.id] = section.offsetTop);
window.onscroll = function () {
document.querySelector('header nav').classList.toggle('inbody', document.documentElement.scrollTop > 380);
document.querySelector('#page-media .layout-animation').style['visibility'] = 'hidden';
var scrollPosition = document.documentElement.scrollTop || document.body.scrollTop;
Object.keys(sections).forEach(key => {
if (sections[key] <= scrollPosition + 55) {
document.querySelectorAll('a.nav-item').forEach(a => a.classList.remove('active'));
document.querySelector('a[href="#' + key + '"]').classList.add('active');
if (key === 'page-media') {
document.querySelector('#page-media .layout-animation').classList.add('animated', 'fadeInRight');
}
}
});
};
The above code works. But I would like to use the right API.
Here is my IntersectionObserver:
if (window.IntersectionObserver) {
let observer = new IntersectionObserver(
(entries, observer) => {
console.log(entries);
entries.forEach(entry => {
/* Here's where we deal with every intersection */
document.querySelectorAll('a.nav-item').forEach(a => a.classList.remove('active'));
if (entry.isIntersecting) {
document.querySelector('header nav').classList.toggle('inbody', entry.target.id !== 'page-hero');
document.querySelector('#page-media .layout-animation').style['visibility'] = 'hidden';
document.querySelector('a[href="#' + entry.target.id + '"]').classList.add('active');
if (entry.target.id === 'page-media') {
document.querySelector('#page-media .layout-animation').classList.add('animated', 'fadeInRight');
}
}
});
}, { root: document.querySelector('.site-nav'), rootMargin: "0px", threshold: 0 });
document.querySelectorAll('.page-section,.site-header').forEach(section => observer.observe(section));
}
But my intersection observer is not working. I tired using console.log(entry) to see what happens. All the entries gets logged only on page load and not working after that. Please assist on where I'm wrong.
The reason the code here doesn't work is that it's running at the wrong time. The call to document.querySelectorAll('.page-section,.site-header') needs to be executed after those elements have been rendered into the DOM, otherwise it won't match anything.
Here is how I made it to work, I tweaked the function little bit as shown below,
function highlightMenu() {
const sections = document.querySelectorAll('.page-section,.site-header');
const config = {
rootMargin: '-55px 0px -85%'
};
let observer = new IntersectionObserver(function
(entries, self) {
entries.forEach(entry => {
if (entry.isIntersecting) {
intersectionHandler(entry);
}
});
}, config);
sections.forEach(section => observer.observe(section));
}
function intersectionHandler(entry) {
const id = entry.target.id;
document.querySelector('header nav').classList.toggle('inbody', id !== 'page-hero');
if (id === 'page-media') {
document.querySelector('#page-media .layout-animation').classList.add('animated', 'fadeInRight');
}
const currentlyActive = document.querySelector('nav a.nav-item.active');
const shouldBeActive = document.querySelector('nav a.nav-item[href="#' + id + '"]');
if (currentlyActive) {
currentlyActive.classList.remove('active');
}
if (shouldBeActive) {
shouldBeActive.classList.add('active');
}
}
and called from inside my component OnAfterRenderAsync using JS interop in blazor,
#code{
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("highlightMenu");
}
}
}
Related
How to hide bootstrap offcanvas in vue3 when router-link is clicked?
Good day, I would like to hide bootstrap-5/Offcanvas when I click on link inside. Here is Offcanvas: //Button activate Offcanvas <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasDarkNavbar" aria-controls="offcanvasDarkNavbar"> <span class="navbar-toggler-icon"></span> </button> //Offcanvas <div class="offcanvas offcanvas-end text-bg-dark" tabindex="-1" id="offcanvasDarkNavbar" aria-labelledby="offcanvasDarkNavbarLabel"> <div class="offcanvas-header"> <h5 class="offcanvas-title" id="offcanvasDarkNavbarLabel">Menu</h5> </div> <div class="offcanvas-body"> <ul class="nav nav-pills flex-column mb-sm-auto mb-0 align-items-center align-items-sm-start" id="menu"> <li class="nav-item"> <router-link to="/about" class="nav-link link-info align-middle px-0"> <i class="fs-4 bi-house"></i> <span class="ms-1 d-none d-sm-inline">Home</span> </router-link> </li> </ul> </div> </div> In official docs they say: You can create an offcanvas instance with the constructor, for example: var myOffcanvas = document.getElementById('myOffcanvas') OR var bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas) and then use method "hide". I tried to use in router-link #click="hideThisCanvas" and then methods: { hideThisCanvas(){ let myOffcanvas = document.getElementById('offcanvasDarkNavbar') myOffcanvas.hide(); } } but it gives the error myOffcanvas.hide is not a function. Please, help!
you have to create an offcanvas instance with the constructor, for example: methods: { hideThisCanvas(){ let myOffcanvas = document.getElementById('offcanvasDarkNavbar') let bsOffcanvas = bootstrap.Offcanvas.getInstance(myOffcanvas); bsOffcanvas.hide(); } } OR methods: { hideThisCanvas(){ let myOffcanvas = document.getElementById('offcanvasDarkNavbar') let bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas); bsOffcanvas.hide(); } } Because using traditional document selectors is not ideal for modern JS framework, you can create a ref for your offCanvas like this. <div ref="offCanvas" class="offcanvas offcanvas-start" tabindex="-1" id="example_canvas"> Then access it in your vuejs function the way you should example let myOffcanvas = this.$refs.offCanvas; your final method should be something like this methods: { hideThisCanvas(){ let myOffcanvas = this.$refs.offCanvas; let bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas); bsOffcanvas.hide(); } } I do not know so much about vuejs but I hope you get concept now and it helps you.
Toggling lottie hamburger/menu animation on .nav-link click?
SOLVED, SEE MY SOLUTION BELOW Hello so my navigation works fine like this apart from when the links are clicked, the lottie animation doesn't toggle back to it's first frame. Could anyone help me with a solution to this as I've been trying for awhile now with no luck. I'm using bootstrap and this is my navigation toggler button: <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#lowetoggle" aria-controls="lowetoggle" aria-expanded="false" aria-label="Toggle navigation" > <div class="lowe-menu" style="width: 50px;"></div> </button> <lottie-player id="toggleLottie" src="assets/menu.json" style="width:50px;">"></lottie-player> </button> and this is my js: let iconMenu = document.querySelector('.lowe-menu'); let animationMenu = bodymovin.loadAnimation({ container: iconMenu, renderer: 'svg', loop: false, autoplay: false, path: "/assets/menu.json" }); var directionMenu = 1; iconMenu.addEventListener('click', (e) => { animationMenu.setDirection(directionMenu); animationMenu.play(); directionMenu = -directionMenu; }); var navLinks = document.querySelectorAll('.nav-link') var menuToggle = document.getElementById('lowetoggle') var bsCollapse = new bootstrap.Collapse(menuToggle, {toggle:false}) navLinks.forEach((l) => { l.addEventListener('click', () => { bsCollapse.toggle() }) });
Bootstrap 5 Lottie Hamburger Menu Solved this myself, So if you are using Lottie for your hamburger menu in Bootstrap, and your page is using anchor links to scroll to different sections, this code will make the hamburger close on clicking a .nav-link element. Hope this is useful, as I've not found anyone else with a snippet for this online! Maybe I didn't far though! let iconMenu = document.querySelector('.lowe-menu'); let animationMenu = bodymovin.loadAnimation({ container: iconMenu, renderer: 'svg', loop: false, autoplay: false, animationData: {"v":"5.7.4","fr":60,"ip":0,"op":39,"w":600,"h":600,"nm":"hamburger to x","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"ScaleCTRL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[391.026,391.026,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":39,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Line03","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":32,"s":[48]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":37,"s":[45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":47,"s":[45]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":52,"s":[48]},{"t":60,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[0,30.5,0],"to":[0,-5.083,0],"ti":[0,5.083,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[0,0,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[0,0,0],"to":[0,5.083,0],"ti":[0,-5.083,0]},{"t":69,"s":[0,30.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-9.75,-20,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-63,-20],[43.5,-20]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":39,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Line02","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[-9.75,-20,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-63,-20],[43.5,-20]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.411],"y":[1]},"o":{"x":[0.938],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":20,"s":[50]},{"i":{"x":[0.411],"y":[1]},"o":{"x":[0.938],"y":[0]},"t":58,"s":[50]},{"t":78,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.411],"y":[1]},"o":{"x":[0.938],"y":[0]},"t":1,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":20,"s":[50]},{"i":{"x":[0.411],"y":[1]},"o":{"x":[0.938],"y":[0]},"t":58,"s":[50]},{"t":78,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":39,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Line01","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":32,"s":[-48]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":37,"s":[-45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":47,"s":[-45]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":52,"s":[-48]},{"t":60,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[0,-30.5,0],"to":[0,5.083,0],"ti":[0,-5.083,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[0,0,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[0,0,0],"to":[0,-5.083,0],"ti":[0,5.083,0]},{"t":69,"s":[0,-30.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-9.75,-20,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-63,-20],[43.5,-20]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":39,"st":0,"bm":0}],"markers":[]} }); var directionMenu = 1; iconMenu.addEventListener('click', (e) => { animationMenu.setDirection(directionMenu); animationMenu.play(); directionMenu = -directionMenu; }); var navLinks = document.querySelectorAll('.nav-link') var menuToggle = document.getElementById('lowetoggle') var bsCollapse = new bootstrap.Collapse(menuToggle, {toggle:false}) navLinks.forEach((l) => { l.addEventListener('click', () => { if (window.innerWidth < 992){ // This part fixes flickering on desktop nav-items! bsCollapse.toggle() animationMenu.setDirection(directionMenu); animationMenu.play(); directionMenu = -directionMenu; } }); }); .navbar-lowe {background-color: black;} <link href="https://cdn.jsdelivr.net/npm/bootstrap#5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"/> <nav class="navbar navbar-expand-lg navbar-lowe"> <div class="container-fluid"> <a class="navbar-brand" href="#"> <p>Logo</p> </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#lowetoggle" aria-controls="lowetoggle" aria-expanded="false" aria-label="Toggle navigation" > <div class="lowe-menu" style="width: 50px;"></div> </button> <div class="collapse navbar-collapse" id="lowetoggle"> <div class="navbar-nav align-items-end"> <a class="nav-link active" href="#home">Home</a> <a class="nav-link" href="#work">Work</a> <a class="nav-link" href="#contact">Contact</a> </div> <div class="navbar-text ms-auto text-end"> <a class="btn nav-button" role="button">Call Today <strong>05555555 555</strong></a> </div> </div> </div> </nav> <script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.8.1/lottie.min.js"></script> <script src="https://unpkg.com/#lottiefiles/lottie-player#latest/dist/lottie-player.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap#5.1.3/dist/js/bootstrap.bundle.min.js"></script>
Updating module on instant change when list is updated
Sorry for the long post, but I tried explaining things in as much detail as possible. So as I dive deeper into JavaScript and start learning more and more about AJAX requests and other components, I've stumbled across something that I can't seem to figure out. So below, I will explain what I'm doing and what I would like to do, and see if someone has some guidance for me. So here is my Vue.js app: new Vue({ name: 'o365-edit-modal', el: '#o365-modal-edit', data: function() { return { list: {}, } }, created() { this.fetchApplicationsMenu(); }, methods: { fetchApplicationsMenu() { var self = this; wp.apiRequest( { path: 'fh/v1/menus/applications', method: 'GET', }).then(menu => self.list = menu.data); }, changed() { const selected = this.$data.list.selected; function get_ids(list, field) { const output = []; for (let i=0; i < list.length ; ++i) output.push(list[i][field]); return output; } const result = get_ids(selected, "id"); wp.apiRequest( { path: 'fh/v1/menus/applications', method: 'PUT', data: { ids: result, }, }).then((post) => { return post; }, (error) => { console.log(error); }); }, add(x) { this.$data.list.selected.push(...this.$data.list.available.splice(x, 1)); this.changed(); }, remove(x) { this.$data.list.available.push(...this.$data.list.selected.splice(x, 1)); this.changed(); }, }, }); Then here is the HTML portion that I'm using to render the two columns: <div class="column is-half-desktop is-full-mobile buttons"> <nav class="level is-mobile mb-0"> <div class="level-left"> <div class="level-item is-size-5 has-text-left">Selected</div> </div> <div class="level-right"> <div class="level-item"> <i class="fas fa-sort-alpha-up is-clickable"></i> </div> </div> </nav> <hr class="mt-1 mb-3"> <draggable class="list-group" v-model="list.selected" v-bind="dragOptions" :list="list.selected" :move="onMove" #change="changed"> <button class="button is-fullwidth is-flex list-group-item o365_app_handle level is-mobile" v-for="(app, index) in list.selected" :key="app.id"> <div class="level-left"> <span class="icon" aria-hidden="true"> <img :src="app.icon_url" /> </span> <span>{{app.name}}</span> </div> <div class="level-right"> <span class="icon has-text-danger is-clickable" #click="remove(index)"> <i class="fas fa-times"></i> </span> </div> </button> </draggable> </div> <div class="column is-half-desktop is-full-mobile buttons"> <div class="is-size-5 has-text-left">Available</div> <hr class="mt-1 mb-3"> <draggable class="list-group" v-model="list.available" v-bind="dragOptions" :list="list.available" :move="onMove"> <button class="button is-fullwidth is-flex list-group-item o365_app_handle level is-mobile" v-for="(app, index) in list.available" :key="app.id"> <div class="level-left"> <span class="icon" aria-hidden="true"> <img :src="app.icon_url" /> </span> <span>{{app.name}}</span> </div> <div class="level-right"> <span class="icon has-text-primary is-clickable" #click="add(index)"> <i class="fas fa-plus"></i> </span> </div> </button> </draggable> </div> That outputs the following items, and all works great. See the video display below of each component working as needed. This all works great! I'm calling the changed() method on add and remove which grabs all the IDs and stores them in the DB via an endpoint. The Problem: Now I have the following dropdown menu, which depends on the fh/v1/menus/applications endpoint to pull in all the items as shown below: As you can see below, when I open the dropdown, it has three apps, when I open the cog wheel and remove one of the apps and it saves it but the dropdown doesn't get automatically updated, I have to refresh the page and then I will see the updates. Does anyone know how to fetch the new items without a refresh? Here is the HTML and the JS for the dropdown piece: HTML: As you can see in there, I have data-source="applications" which pulls in the items inside the init_menu as shown in the JS. <div class="dropdown-menu" id="dropdown-o365" role="menu"> <div class="dropdown-content"> <div class="container is-fluid px-4 pb-4"> <?php if ($application = Applications::init()): ?> <div class="columns"> <div class="dropdown-item column is-full has-text-centered is-size-6"> <div class="level is-mobile"> <div class="level-left"> <?= $application->get_name() ?> </div> <div class="level-right"> <a class="navbar-item modal-element icon" id="o365-apps-cogwheel" data-target="o365-modal-edit" aria-haspopup="true"> <i class="fa fa-cog"></i> </a> </div> </div> </div> </div> <div class="columns is-multiline" data-source="applications"></div> <?php else: ?> <div class="columns"> <div class="column is-full"> No applications present. </div> </div> <?php endif; ?> </div> </div> </div> Then here is the JavaScript. I initilize the method inside DOMContentLoaded using init_menu('applications');: function init_menu(paths) { paths.forEach(path => { const target = document.querySelector('[data-source=' + path + ']'); if (target) { wp.api.loadPromise.done(function () { const Menus = wp.api.models.Post.extend({ url: wpApiSettings.root + 'fh/v1/menus/' + path, }); const menus = new Menus(); menus.fetch().then(posts => { // This returns the data object. const data = posts.data; let post_list; // Check if it's an array and see if selected is empty otherwise show available. if (Array.isArray(data.selected) && data.selected.length !== 0) { post_list = data.selected; } else { post_list = data.available; } post_list.forEach(function (post) { switch(path) { case 'applications': target.appendChild(create_apps_dom_tree(post)); break; default: console.log('Path route is invalid.'); break; } }) }) }) } }); } function create_apps_dom_tree(post) { const { icon_url, url, name, } = post const container = document.createElement('div'); container.className = 'column is-one-third is-flex py-0'; const anchor = document.createElement('a'); anchor.href = url; anchor.className = 'dropdown-item px-2 is-flex is-align-items-center'; const figure = document.createElement('figure'); figure.className = 'image is-32x32 is-flex'; const img = document.createElement('img'); img.src = icon_url; const span = document.createElement('span'); span.className = 'pl-2'; span.textContent = name; figure.appendChild(img); anchor.append(figure, span); container.appendChild(anchor); return container; } If anyone has some guidance or an answer on how to pull in live data from the database on the fly, that would be amazing. Basically, I need my data-source: to automatically grab the items when my vue/db request is sent so I don't have to refresh the page. Inside my Vue app, I have the following method: fetchApplicationsMenu() { var self = this; wp.apiRequest( { path: 'fh/v1/menus/applications', method: 'GET', }).then(menu => self.list = menu.data); }, which calls a GET request and then stores the data inside the return { list: {} }.
A quick fix might be to just invoke init_menu() from the component's beforeDestroy() hook, called when the dialog closes. You might choose to do it from changed() instead if the dropdown is still accessible with this dialog open. new Vue({ // option 1: beforeDestroy() { init_menu('applications'); }, // option 2: methods: { changed() { init_menu('applications'); } } }) Alternative: You already know what the final application list is in changed(), so you could update the dropdown with the new list from that method. function update_menu(path, post_list) { const target = document.querySelector('[data-source=' + path + ']'); // remove all existing children Array.from(target.childNodes).forEach(x => x.remove()); post_list.forEach(post => target.appendChild(create_apps_dom_tree(post))) } new Vue({ methods: { changed() { update_menu('applications', this.$data.available); } } })
When I load two identical (confirmed by IntelliJ) JS scripts into my web app one produces different results than the other
TL;DR: IntelliJ confirms to different scripts are identical, but they produce different results when ran in the browser (using IntelliJ with Tomcat). Exact details below. I am creating a web project using Spring MVC that allows me to play chess. I am using the chess.js library and the chessboard.js library. According to the chess.js library README, it has a methodpgn() that returns the moves of the game as a string. You can optionally pass in a JSON to set a max_length and a newline character so that there is a new line character after black moves. For example game.pgn({ max_width: 5, newline_char: '<br />' }). Here's my problem. I made a script called initgame.js that instantiates a chess game using the libraries and attempted to use the example above so that the moves printed out would be formatted to print a new line after each turn. It wasn't working as I had hoped. So I created another script test_game.js to experiment and when I got the behavior I wanted I copy and pasted the contents of test_game.js into initgame.js. Changed the <script> tag point to initgame.js again and it was ignoring the line breaks again. Used IntelliJ to compare the files and IntelliJ confirms that the files are indeed identical. Now I am just dumbfounded. I tried rebuilding the project, cleaning the Artifacts, cleaning Maven. Nothing. I even closed out of IntelliJ and restarted. When I run the program with test_game.js it works as desired. When I run it with initgame.js it ignores the line breaks. As a potential hint, this doesn't happen when I use Visual Studio and load it into the browser as an .html instead of a .jsp Any insight is appreciated! Code and screen shots of output are below as well as a screen shot IntelliJ comparing the files. initgame.js // NOTE: this example uses the chess.js library: // https://github.com/jhlywa/chess.js var board = null; const game = new Chess() var $status = $('#status'); var $fen = $('#fen'); var $pgn = $('#pgn'); function onDragStart (source, piece, position, orientation) { // do not pick up pieces if the game is over if (game.game_over()) return false; // only pick up pieces for the side to move if ((game.turn() === 'w' && piece.search(/^b/) !== -1) || (game.turn() === 'b' && piece.search(/^w/) !== -1)) { return false } } function onDrop (source, target) { // see if the move is legal var move = game.move({ from: source, to: target, promotion: 'q' // NOTE: always promote to a queen for example simplicity }); // illegal move if (move === null) return 'snapback'; updateStatus() } // update the board position after the piece snap // for castling, en passant, pawn promotion function onSnapEnd () { board.position(game.fen()) } function updateStatus () { var status = ''; var moveColor = 'White'; if (game.turn() === 'b') { moveColor = 'Black' } // checkmate? if (game.in_checkmate()) { status = 'Game over, ' + moveColor + ' is in checkmate.' } // draw? else if (game.in_draw()) { status = 'Game over, drawn position' } // game still on else { status = moveColor + ' to move'; // check? if (game.in_check()) { status += ', ' + moveColor + ' is in check' } } $status.html(status); $fen.html(game.fen()); $pgn.html(game.pgn({ max_width: 5, newline_char: '<br />' })) } var config = { draggable: true, position: 'start', onDragStart: onDragStart, onDrop: onDrop, onSnapEnd: onSnapEnd }; board = Chessboard('myBoard', config); updateStatus(); test_game.js // NOTE: this example uses the chess.js library: // https://github.com/jhlywa/chess.js var board = null; const game = new Chess() var $status = $('#status'); var $fen = $('#fen'); var $pgn = $('#pgn'); function onDragStart (source, piece, position, orientation) { // do not pick up pieces if the game is over if (game.game_over()) return false; // only pick up pieces for the side to move if ((game.turn() === 'w' && piece.search(/^b/) !== -1) || (game.turn() === 'b' && piece.search(/^w/) !== -1)) { return false } } function onDrop (source, target) { // see if the move is legal var move = game.move({ from: source, to: target, promotion: 'q' // NOTE: always promote to a queen for example simplicity }); // illegal move if (move === null) return 'snapback'; updateStatus() } // update the board position after the piece snap // for castling, en passant, pawn promotion function onSnapEnd () { board.position(game.fen()) } function updateStatus () { var status = ''; var moveColor = 'White'; if (game.turn() === 'b') { moveColor = 'Black' } // checkmate? if (game.in_checkmate()) { status = 'Game over, ' + moveColor + ' is in checkmate.' } // draw? else if (game.in_draw()) { status = 'Game over, drawn position' } // game still on else { status = moveColor + ' to move'; // check? if (game.in_check()) { status += ', ' + moveColor + ' is in check' } } $status.html(status); $fen.html(game.fen()); $pgn.html(game.pgn({ max_width: 5, newline_char: '<br />' })) } var config = { draggable: true, position: 'start', onDragStart: onDragStart, onDrop: onDrop, onSnapEnd: onSnapEnd }; board = Chessboard('myBoard', config); updateStatus(); .jsp page <!doctype html> <%# page contentType="text/html;charset=UTF-8" language="java" %> <%#taglib prefix="spring" uri="http://www.springframework.org/tags" %> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/javascript/chessboardjs/css/chessboard-1.0.0.min.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/bootstrap.min.css"> <title>Hello, world!</title> </head> <body> <main> <div class="container"> <div class="d-flex justify-content-between align-items-center"> <h1>Hello World</h1> Login </div> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <a class="navbar-brand" href="#"> <i class="fas fa-chess-queen"></i> </a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a> </li> <li class="nav-item"> <a class="nav-link" href="#">Link</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Dropdown </a> <div class="dropdown-menu" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="#">Action</a> <a class="dropdown-item" href="#">Another action</a> <div class="dropdown-divider"></div> <a class="dropdown-item" href="#">Something else here</a> </div> </li> <li class="nav-item"> <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search"> <!-- Replace with Spring security Login form --> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> </form> </div> </nav> <div class="container"> <div class="row justify-content-center mx-1 my-3"> <div id="myBoard" class="col-6"></div> <div class="card bg-light col-3"> <div class="card-header">Header</div> <div class="card-body"> <h5 class="card-title">PGN</h5> <p class="card-text">Here are the moves of the game as printed by test_game.js</p> <div class="card-text" id="pgn"></div> </div> <button type="button" class="btn btn-primary m-3">Reset Game</button> </div> </div> <div class="row justify-content-center"> <label>Status:</label> <div id="status"></div> <label>FEN:</label> <div id="fen"></div> <!-- <label>PGN:</label> <div id="pgn"></div> --> </div> </div> </div> </main> <%-- jquery --%> <script src="https://code.jquery.com/jquery-3.4.1.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js#1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> <script src="${pageContext.request.contextPath}/static/javascript/chessboardjs/js/chessboard-1.0.0.min.js"></script> <script src="${pageContext.request.contextPath}/static/javascript/node_modules/chess.js/chess.js"></script> <script src="${pageContext.request.contextPath}/static/javascript/test_game.js"></script> </body> </html> With initgame.js With test_game.js The results of the IntelliJ compare function
try this : replace $pgn.html(game.pgn({ max_width: 5, newline_char: '<br />' c})) by var pgnn = game.pgn({ max_width: 5, newline_char: '<br />' }) pgnn = pgnn + " </br>" $pgn.html(pgnn) i'm not sure that it gonna work but i think
Response menu not collapsing and reloads page
At http://www.dentalo.se/contact it is using a responsive menu for mobile devices. When clicking on the menu button it collapses the menu but the same time reloads the page. Can anyone tell my why and how I can fix it? Thanks for the time you are taking to help me. Edit HTML <div class="container"> <div class="navbar-header"> <!-- BEGIN RESPONSIVE MENU TOGGLER --> <button type="button" class="navbar-toggle btn navbar-btn" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <!-- END RESPONSIVE MENU TOGGLER --> <!-- BEGIN LOGO (you can use logo image instead of text)--> <a class="navbar-brand logo-v1" href="/"> <img src="/assets/img/logo_blue.png" id="logoimg" alt=""> </a> <!-- END LOGO --> </div> <!-- BEGIN TOP NAVIGATION MENU --> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li id="MainMenuHome">Start</li> <li class="dropdown" id="MainMenuDentalo"> <a class="dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" data-delay="0" data-close-others="false" href="#"> Dentalo <i class="icon-angle-down"></i> </a> <ul class="dropdown-menu"> <li id="SubMenuAbout">Om Dentalo</li> <li id="SubMenuJob">Lediga tjänster</li> <li id="SubMenuConnect">Anslut</li> </ul> </li> <li id="MainMenuLogIn">Logga in</li> <li id="MainMenuContact">Kontakt</li> <li class="menu-search"> <span class="sep"></span> <i class="icon-search search-btn"></i> <div class="search-box"> <form action="#"> <div class="input-group input-large"> <asp:TextBox runat="server" placeholder="Sök..." CssClass="form-control" ID="textBoxSearch"></asp:TextBox> <span class="input-group-btn"> <asp:Button runat="server" CssClass="btn theme-btn" ID="ButtonSearch" OnClick="ButtonSearch_Click" Text="Sök" /> </span> </div> </form> </div> </li> </ul> </div> <!-- BEGIN TOP NAVIGATION MENU --> </div> JavaScript /* * Project: Twitter Bootstrap Hover Dropdown * Author: Cameron Spear * Contributors: Mattia Larentis * * Dependencies?: Twitter Bootstrap's Dropdown plugin * * A simple plugin to enable twitter bootstrap dropdowns to active on hover and provide a nice user experience. * * No license, do what you want. I'd love credit or a shoutout, though. * * http://cameronspear.com/blog/twitter-bootstrap-dropdown-on-hover-plugin/ */ ;(function($, window, undefined) { // outside the scope of the jQuery plugin to // keep track of all dropdowns var $allDropdowns = $(); // if instantlyCloseOthers is true, then it will instantly // shut other nav items when a new one is hovered over $.fn.dropdownHover = function(options) { // the element we really care about // is the dropdown-toggle's parent $allDropdowns = $allDropdowns.add(this.parent()); return this.each(function() { var $this = $(this), $parent = $this.parent(), defaults = { delay: 500, instantlyCloseOthers: true }, data = { delay: $(this).data('delay'), instantlyCloseOthers: $(this).data('close-others') }, settings = $.extend(true, {}, defaults, options, data), timeout; $parent.hover(function(event) { // so a neighbor can't open the dropdown if(!$parent.hasClass('open') && !$this.is(event.target)) { return true; } if(shouldHover) { if(settings.instantlyCloseOthers === true) $allDropdowns.removeClass('open'); window.clearTimeout(timeout); $parent.addClass('open'); } }, function() { if(shouldHover) { timeout = window.setTimeout(function() { $parent.removeClass('open'); }, settings.delay); } }); // this helps with button groups! $this.hover(function() { if(shouldHover) { if(settings.instantlyCloseOthers === true) $allDropdowns.removeClass('open'); window.clearTimeout(timeout); $parent.addClass('open'); } }); // handle submenus $parent.find('.dropdown-submenu').each(function(){ var $this = $(this); var subTimeout; $this.hover(function() { if(shouldHover) { window.clearTimeout(subTimeout); $this.children('.dropdown-menu').show(); // always close submenu siblings instantly $this.siblings().children('.dropdown-menu').hide(); } }, function() { var $submenu = $this.children('.dropdown-menu'); if(shouldHover) { subTimeout = window.setTimeout(function() { $submenu.hide(); }, settings.delay); } else { // emulate Twitter Bootstrap's default behavior $submenu.hide(); } }); }); }); }; // helper variables to guess if they are using a mouse var shouldHover = false, mouse_info = { hits: 0, x: null, y: null }; $(document).ready(function() { // apply dropdownHover to all elements with the data-hover="dropdown" attribute $('[data-hover="dropdown"]').dropdownHover(); // if the mouse movements are "smooth" or there are more than 20, they probably have a real mouse $(document).mousemove(function(e){ mouse_info.hits++; if (mouse_info.hits > 20 || (Math.abs(e.pageX - mouse_info.x) + Math.abs(e.pageY - mouse_info.y)) < 4) { $(this).unbind(e); shouldHover = true; } else { mouse_info.x = e.pageX; mouse_info.y = e.pageY; } }); }); // for the submenu to close on delay, we need to override Bootstrap's CSS in this case var css = '.dropdown-submenu:hover>.dropdown-menu{display:none}'; var style = document.createElement('style'); style.type = 'text/css'; if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } $('head')[0].appendChild(style); })(jQuery, this);
It appears that your button element (navbar-btn) is submitting your form. Try adding type="button" to the button element. Also in the click event handler you can also try to add e.preventDefault() to your click event handler. I cant seem to find the JavaScript from the click event of your button so I can help much more. If this does not work, please edit your question to show the relevant HTML and JavaScript sections.