How do you handle click-outside of element properly in vuejs? - javascript

Is there any proper solution for handling click-outside of elements?
there are general solutions out there, like Handling clicks outside an element without jquery :
window.onload = function() {
// For clicks inside the element
document.getElementById('myElement').onclick = function(e) {
// Make sure the event doesn't bubble from your element
if (e) { e.stopPropagation(); }
else { window.event.cancelBubble = true; }
// Place the code for this element here
alert('this was a click inside');
};
// For clicks elsewhere on the page
document.onclick = function() {
alert('this was a click outside');
};
};
But the problem is almost all projects have multiple and different popups in different components which i should handle their click-outsides.
how should i handle click-outisde without using a global window.on?(I think it is not possible to put all components outside-case handler in window.on )

After struggling with this and searching about this, i found how to solve this problem using vuejs directive without bleeding:
1. using libraries:
v-click-outside is a good one,
https://www.npmjs.com/package/v-click-outside
2. without a library:
```
//main.js
import '#/directives';
......
// directives.js
import Vue from "vue";
Vue.directive('click-outside', {
bind: function (element, binding, vnode) {
element.clickOutsideEvent = function (event) { // check that click was outside the el and his children
if (!(element === event.target || element.contains(event.target))) { // and if it did, call method provided in attribute value
vnode.context[binding.expression](event);
// binding.value(); run the arg
}
};
document.body.addEventListener('click', element.clickOutsideEvent)
},
unbind: function (element) {
document.body.removeEventListener('click', element.clickOutsideEvent)
}
});
```
use it every-where you want with v-click-outside directive like below:
//header.vue
<div class="profileQuickAction col-lg-4 col-md-12" v-click-outside="hidePopUps">
...
</>
you can check this on

You can also directly use VueUse vOnClickOUtside directive.
<script setup lang="ts">
import { vOnClickOutside } from '#vueuse/components'
const modal = ref(true)
function closeModal() {
modal.value = false
}
</script>
<template>
<div v-if="modal" v-on-click-outside="closeModal">
Hello World
</div>
</template>

It might be little late but i ve found a clear solution
<button class="m-search-btn" #click="checkSearch" v-bind:class="{'m-nav-active':search === true}">
methods:{
checkSearch(e){
var clicked = e.target;
this.search = !this.search;
var that = this;
window.addEventListener('click',function check(e){
if (clicked === e.target || e.target.closest('.search-input')){ //I ve used target closest to protect the input that has search bar in it
that.search = true;
}else {
that.search = false;
removeEventListener('click',check)
}
})
}}

Related

How to attach click events to iframe content?

I have a scenario where I wanted to load a page on inital load. I found that this would do the trick:
<main id="mainContent">
<iframe id="InitalIframe" src="./Pages/Start/index.html" onload="this.before((this.contentDocument.body||this.contentDocument).children[0]);this.remove()"></iframe>
</main>
I have some links in my header which I attach click listners to:
(function() {
document.querySelectorAll(".link").forEach((item) => {
item.addEventListener("click", (event) => {
event.preventDefault();
const url = event.target.dataset["url"];
getHtmlFile(`./Pages/${url}/`, (data) => {
document.getElementById("mainContent").innerHTML = data;
executeScripts();
});
return false;
});
});
})();
This worked untill I added a few links inside of the Start/Index.html file which gets renderd via the iframe.
I have these two buttons inside of that html.
<button type="button" class="link refbtn" data-Url="One">
One
</button>
<button type="button" class="link refbtn" data-Url="Two">
Two
</button>
Since I attached my listners before the iframe has loaded they never get picked up.
But when I waited for the iframe to load:
document.getElementById("InitalIframe").onload = function() {
document.querySelectorAll(".link").forEach((item) => {
item.addEventListener("click", (event) => {
event.preventDefault();
const url = event.target.dataset["url"];
getHtmlFile(`./Pages/${url}/`, (data) => {
document.getElementById("mainContent").innerHTML = data;
executeScripts();
});
return false;
});
});
};
the click events did not get attached and I got a weird looking result on the page.
Question is how do I accomplish this?
For anyone struggling with the same:
I made my life easier by listening for document click events. when I found that an element with a certain class was clicked I triggered desierd functions:
document.addEventListener(
"click",
function(event) {
event.preventDefault();
const classes = event.target.classList;
if (classes.contains("link")) {
const url = event.target.dataset["url"];
app.fetcHtml(`./Pages/${url}/`, (data) => {
document.getElementById("mainContent").innerHTML = data;
});
}
return false;
},
false
);
You can get to work this with a different approach also. In other words, I had the method closest() in pure Javascript finding me the closest event target that is trigerred when I click inside the container. It will always get me the nearest <a> element which I clicked when I had wandered throught the clickable div/container area.
let base; // the container for the variable content
let dataSet;
base = document.getElementById(base.id); // target your Iframe in this case.
base.addEventListener('click', function (event) {
let selector = '.link'; // any css selector for children
// find the closest parent of the event target that
// matches the selector
let closest = event.target.closest(selector);
if (closest !== undefined && base.contains(closest)) {
dataSet= event.target.dataset["url"];
app.fetcHtml(`./Pages/${url}/`, (data) => {
document.getElementById("mainContent").innerHTML = data;
});
}
});
You can try and see if it works like this also.
Cheers

PrimeNg TabView with ConfirmDialog

I'm trying to use PrimeNg TabView component along with confirmDialog unsuccessfully, here is my code:
<p-tabView (onChange)="onTabChange($event)" [(activeIndex)]="index">...</p-tabView>
onTabChange(event){
this.confirmationService.confirm({
message: 'Do you confirm ?',
accept: () => {
this.index = event.index;
},
reject:() =>{ }
});
}
Do you have an idea on how to prevent or allow tab change using confirm dialog ?
Thanks
Based on similar solution for material design tabs, here is the solution for my issue:
in html Declare a local variable referencing TabView DOM object:
<p-tabView #onglets>...</p-tabView>
in component.ts, change default function called when click on tab with specific
function to match your case:
#ViewChild('onglets') onglets: TabView;
this.onglets.open = this.interceptOngletChange.bind(this);
...
interceptOngletChange(event: Event, tab: TabPanel){
const result = confirm(Do you really want to leave the tab?);
return result && TabView.prototype.open.apply(this.onglets, argumentsList);
});
}
I had similar problem. Needed show dialog before tab change.
My solution:
HTML
<p-tabView #tabView (onChange)="onChange($event)" />
TS
#ViewChild('tabView') tabView: TabView;
onChange(event: any) {
const previoustab = this.tabView.tabs[this.prevIndex]; //saved previous/initial index
previoustab.selected = true;
const selectedTab = this.tabView.tabs[event.index];
selectedTab.selected = false;
this.tabView.activeIndex = this.prevIndex;
this.nextIndex= event.index;
}
GoToNextTab() {
this.tabView.activeIndex = this.nextIndex;
this.prevIndex= this.nextIndex;
this.tabView.open(undefined, this.tabView.tabs[this.nextIndex]);
}
With this code you will stay on the selected tab without tab style changes.

How to hook on library function (Golden Layout) and call additional methods

I'm using a library called Golden Layout, it has a function called destroy which will close all the application window, on window close or refesh
I need to add additional method to the destroy function. I need to removeall the localstorage aswell.
How do i do it ? Please help
Below is the plugin code.
lm.LayoutManager = function( config, container ) {
....
destroy: function() {
if( this.isInitialised === false ) {
return;
}
this._onUnload();
$( window ).off( 'resize', this._resizeFunction );
$( window ).off( 'unload beforeunload', this._unloadFunction );
this.root.callDownwards( '_$destroy', [], true );
this.root.contentItems = [];
this.tabDropPlaceholder.remove();
this.dropTargetIndicator.destroy();
this.transitionIndicator.destroy();
this.eventHub.destroy();
this._dragSources.forEach( function( dragSource ) {
dragSource._dragListener.destroy();
dragSource._element = null;
dragSource._itemConfig = null;
dragSource._dragListener = null;
} );
this._dragSources = [];
},
I can access the destroy method in the component like this
this.layout = new GoldenLayout(this.config, this.layoutElement.nativeElement);
this.layout.destroy();`
My code
#HostListener('window:beforeunload', ['$event'])
beforeunloadHandler(event) {
var originalDestroy = this.layout.destroy;
this.layout.destroy = function() {
// Call the original
originalDestroy.apply(this, arguments);
localStorage.clear();
};
}
Looking at the documentation, GoldenLayout offers an itemDestroyed event you could hook to do your custom cleanup. The description is:
Fired whenever an item gets destroyed.
If for some reason you can't, the general answer is that you can easily wrap the function:
var originalDestroy = this.layout.destroy;
this.layout.destroy = function() {
// Call the original
originalDestroy.apply(this, arguments);
// Do your additional work here
};
You may be able to do this for all instances if necessary by modifying GoldenLayout.prototype:
var originalDestroy = GoldenLayout.prototype.destroy;
GoldenLayout.prototype.destroy = function() {
// Call the original
originalDestroy.apply(this, arguments);
// Do your additional work here
};
Example:
// Stand-in for golden laout
function GoldenLayout() {
}
GoldenLayout.prototype.destroy = function() {
console.log("Standard functionality");
};
// Your override:
var originalDestroy = GoldenLayout.prototype.destroy;
GoldenLayout.prototype.destroy = function() {
// Call the original
originalDestroy.apply(this, arguments);
// Do your additional work here
console.log("Custom functionality");
};
// Use
var layout = new GoldenLayout();
layout.destroy();
Hooking into golden layout is the intended purpose for the events.
As briefly touched on by #T.J. Crowder, there is the itemDestroyed event which is called when an item in the layout is destroyed.
You can just listen for this event like such:
this.layout.on('itemDestroyed', function() {
localStorage.clear();
})
However, this event is called every time anything is destroyed, and propagates down the tree, even just by closing a tab. This means that if you call destroy on the layout root, you will get an event for every RowOrColumn, Stack and Component
I would recommend to check the item passed into the event and ignore if not the main window (root item)
this.layout.on('itemDestroyed', function(item) {
if (item.type === "root") {
localStorage.clear();
}
})

Javascript: managing clicks from one instance to the next

I am very new to Javascript.
I am trying to write this baby jQuery plugin that I will use to make dropdown lists. What I am failing to achieve (beyond things that I do not notice) is to neatly exit or deactivate my active instance as I click on another instance. I tried to illustrate my problem in the following fiddle (keeping the structure I am using):
https://jsfiddle.net/andinse/m0kwfj9d/23/
What the Javascript looks like:
$(document).ready(function() {
$.fn.activator = function() {
var Activator = function(el) {
this.html = $('html');
this.el = el;
this.is_active = false;
this.initialize();
};
Activator.prototype.initialize = function() {
var self = this;
self.el.on('click', function(e) {
if (self.is_active === false) {
self.toggle('activate');
} else {
self.toggle('deactivate');
}
});
};
Activator.prototype.toggle = function(action) {
var self = this;
if (action === 'activate') {
console.log('activating ' + self.el[0].className);
self.is_active = true;
self.el.addClass('red');
self.html.on('click', function(e) {
if (e.target != self.el[0]) {
self.toggle('deactivate');
}
});
}
if (action === 'deactivate') {
console.log('deactivating ' + self.el[0].className);
self.is_active = false;
self.el.removeClass('red');
self.html.off('click');
}
};
if (typeof this !== 'undefined') {
var activator = new Activator(this);
}
return this;
};
$('.a').activator();
$('.b').activator();
$('.c').activator();
});
My idea was:
To watch for clicks on html as soon as the instance is active (thus ready to be deactivated). On click, to check if the event.target is the same as the active instance. If not, to deactivate this instance.
To stop watching for clicks as soon as the instance is inactive. So that we're not doing unnecessary work.
When it is set like this, it seems to work for only one cycle (click on A activates A then click on B activates B and deactivates A then click on C activates C but doesn't deactivate B).
If I get rid of the "self.html.off('click')" it seems to work kind of ok but if I look at the log I can see the "toggle" function is sometimes triggered multiple times per click. There must be a cleaner way.
Any piece of help greatly appreciated.
With your logic, when clicking any element you should deactivate any current activated element. Either do it globally:
$('.your_activation_class').removeClass('.your_activation_class');
or in some parent scope
$('some_parent_selector .your_activation_class').removeClass('.your_activation_class');

Multiple click handlers for a single element

I've written a few events to handle opening and closing of a snap js drawer. This code below works, but I feel it could be written more efficiently. Any suggestions?
function openMobileMenu() {
event.preventDefault();
snapper.open('left');
$('#btn-menu').off('click', openMobileMenu);
$('#btn-menu').on('click', closeMobileMenu);
}
function closeMobileMenu() {
event.preventDefault();
snapper.close('left');
$('#btn-menu').on('click', openMobileMenu);
$('#btn-menu').off('click', closeMobileMenu);
}
$('#btn-menu').on('click', openMobileMenu);
Make your code modular and your concepts explicit.
You can start by creating a MobileMenu object which encapsulates the logic.
Note: The following code was not tested.
var MobileMenu = {
_snapper: null,
_$button: null,
_direction: 'left',
init: function (button, snapper, direction) {
this._$button = $(button);
this._snapper = snapper;
if (direction) this._direction = direction;
this._toggleSnapperVisibilityWhenButtonClicked();
},
_toggleSnapperVisibilityWhenbuttonClicked: function () {
this._$button.click($.proxy(this.toggle, this));
},
toggle: function () {
var snapperClosed = this._snapper.state().state == 'closed',
operation = snapperClosed? 'open' : 'closed';
this._snapper[operation](this._direction);
}
};
Then in your page you can just do the following to initialize your feature:
var mobileMenu = Object.create(MobileMenu).init('#btn-menu', snapper);
Modularizing your code will make it more maintainable and understandable in the long run, but also allow you to unit test it. You also gain a lot more flexibily because of the exposed API of your component which allows other code to interact with it.
E.g. you can now toggle the menu visibility with mobileMenu.toggle().
Use a variable to keep track of the state:
var menu_open = false;
$("#btn-menu").on('click', function(event) {
event.preventDefault();
if (menu_open) {
snapper.close('left');
} else {
snapper.open('left');
}
menu_open = !menu_open; // toggle variable
});
snap has a .state() method, which returns an object stuffed with properties, one of which is .state.
I think you want :
$('#btn-menu').on('click', function() {
if(snapper.state().state == "closed") {
snapper.open('left');
} else {
snapper.close('left');
}
});
Or, in one line :
$('#btn-menu').on('click', function() {
snapper[['close','open'][+(snapper.state().state == 'closed')]]('left');
});
Also, check How do I make a toggle button? in the documentation.

Categories

Resources