Check if an element has focus - Vue.js directive - javascript

I am trying to check if an element has focus, in my case Input, and add a class to another element.
This is what I am trying, but I am not sure why hasFocus() is not working.
onFocus () {
let isFocused = document.el.querySelector('a-input')
let focusedEl = document.el.querySelector('a-button')
if(isFocused.hasFocus()) {
focusedEl.classList.add('testClass')
}
}
I am trying to do this in a Vue.js custom directive.

There is a suggestion in the Vue.js forum to use the focusin event:
created() {
document.addEventListener('focusin', this.focusChanged)
},
beforeDestroy() {
document.removeEventListener('focusin', this.focusChanged)
},
methods: {
focusChanged (event) {
const el = event.target
// do something with the element.
}
}

Since I mentioned that I need to make it as a custom directive:
This is how I fixed it.
class someClassName {
constructor (el, config = {}) {
this.el = el
this.input = el.querySelector('.a-input')
this.button = el.querySelector('.a-button')
this.onInputFocus = this.onInputFocus.bind(this)
this.attachEvents()
}
onInputFocus () {
this.button.classList.add('testclass')
}
attachEvents () {
this.input.addEventListener('focus', this.onInputFocus)
}
}

Related

Remove a JavaScript class method from eventlistener inside another method of the same class

Description & Goal
I have a list with items and clicking on an item shows a detail view, shows a close button and add the eventlistener for closing this item to the button.
By clicking on this button the detail view should be closed and the eventlistener should be removed.
I can't use an anonymous function because removing won't work with it (See here and here).
Problem
Removing doesn't work.
Code
export default class ToggleDetails {
constructor(jobAdId) {
this.jobAdId = jobAdId
this.opened = false
}
toggle() {
const jobAdContainer = document.getElementById(this.jobAdId)
// doing some other css manipulation for the detail view
this.handleCloseButton()
}
handleCloseButton() {
const closeButton = document.getElementById('uh-job-detail-close-button')
const $this = () => {
this.toggle()
}
if (this.opened === true) {
closeButton.classList.remove('uh-job-detail-close-button-show')
closeButton.removeEventListener('click', $this)
this.opened = false
} else {
closeButton.classList.add('uh-job-detail-close-button-show')
closeButton.addEventListener('click', $this)
this.opened = true
}
}
}
HTML structure
"Solution"/Workaround
I solved it, by cloning and replacing the button with itself. The clone doesn't have the eventlisteners (Thanks to this post)
handleCloseButton () {
const closeButton = document.getElementById(
'uh-job-detail-close-button')
closeButton.classList.toggle('uh-job-detail-close-button-show')
if (this.opened === true) {
const elClone = closeButton.cloneNode(true)
closeButton.parentNode.replaceChild(elClone, closeButton)
this.opened = !this.opened
} else {
closeButton.addEventListener('click',
() => { this.toggle() })
this.opened = !this.opened
}
}
Try using a named function and passing the value of this into toggle:
export default class ToggleDetails {
constructor(jobAdId) {
this.jobAdId = jobAdId
this.opened = false
}
toggle(t) {
// doing some other css manipulation for the detail view
t.handleCloseButton()
}
handleCloseButton() {
const closeButton = document.getElementById('uh-job-detail-close-button')
let listenerToggle = () => {
this.toggle(this);
};
if (this.opened === true) {
closeButton.classList.remove('uh-job-detail-close-button-show')
closeButton.removeEventListener('click', listenerToggle)
this.opened = false
} else {
closeButton.classList.add('uh-job-detail-close-button-show')
closeButton.addEventListener('click', listenerToggle)
this.opened = true
}
}
}

addEventListener on dynamically created elements not working

I want to add click events to images present in inner html. But when I try to add it, it won't work.
Template
<div [innerHTML]="myHtml"></div>
Code
myHtml;
ngOnInit() {
const root = document.createElement('div');
root.innerHTML = this.getHtml();
const images = root.getElementsByTagName('img');
Array.prototype.forEach.call(images, (image: HTMLImageElement, i) => {
image.addEventListener('click', (event: any) => {
console.log('click', event);
});
});
this.myHtml = root.innerHTML;
}
Here is the stackblitz
You need to use ngAfterViewInit() to achieve this,
I have modified the code and is working fine.
Please check the link
https://stackblitz.com/edit/angular-7da7cd
Hope this helps.
You can use elementRef for specifying <img> element. Then you can use the following to add event listener into all <img>:
ngAfterViewInit() {
var elem = this.elementRef.nativeElement.querySelectorAll('img');
if (elem) {
elem.forEach(res => {
res.addEventListener('click', this.onclick.bind(this));
})
}
}
Note that:
onclick() {
alert("Image clicked");
}
is a custom function which you can customize. And don't forget to import { ElementRef } from '#angular/core';
Stackblitz Fork.
More detail into AfterViewInit, .bind, the difference between ElementRef and Renderer
This may be due to restriction that event listener will be registered only for elements before page cycle starts. Did you try doing it more angular-ish way by Renderer2
this.renderer.listen('img', 'click', (evt) => {
console.log('Clicking the document', evt);
});
Docs : https://angular.io/api/core/Renderer2
Its not yet in the DOM. Do the same with ngAfterViewInit:
ngAfterViewInit() {
const root = document.createElement('div');
root.innerHTML = this.getHtml();
const images = root.getElementsByTagName('img');
Array.prototype.forEach.call(images, (image: HTMLImageElement, i) => {
image.addEventListener('click', (event: any) => {
console.log('click', event);
});
});
this.myHtml = root.innerHTML;
}

Add click event to element inserted by javascript

If I click on the first "Edit" I get a console.log('click happend') But if I add a one of these boxes via javascript (click on "Add box") and then the Edit click from this new box does not work. I know it's because the javascript run when the element was not there and that's why there is no click event listener. I also know with jQuery I could do like so:
$('body').on('click', '.edit', function(){ // do whatever };
and that would work.
But how can I do this with plain Javascript? I couldn't find any helpful resource. Created a simple example which I would like to be working. What is the best way to solve this?
So the problem is: If you add a box and then click on "Edit" nothing happens.
var XXX = {};
XXX.ClickMe = function(element){
this.element = element;
onClick = function() {
console.log('click happend');
};
this.element.addEventListener('click', onClick.bind(this));
};
[...document.querySelectorAll('.edit')].forEach(
function (element, index) {
new XXX.ClickMe(element);
}
);
XXX.PrototypeTemplate = function(element) {
this.element = element;
var tmpl = this.element.getAttribute('data-prototype');
addBox = function() {
this.element.insertAdjacentHTML('beforebegin', tmpl);
};
this.element.addEventListener('click', addBox.bind(this));
};
[...document.querySelectorAll('[data-prototype]')].forEach(
function (element, index) {
new XXX.PrototypeTemplate(element);
}
);
[data-prototype] {
cursor: pointer;
}
<div class="box"><a class="edit" href="#">Edit</a></div>
<span data-prototype="<div class="box"><a class="edit" href="#">Edit</a></div>">Add box</span>
JSFiddle here
This Q/A is useful information but it does not answer my question on how to solve the problem. Like how can I invoke the eventListener(s) like new XXX.ClickMe(element); for those elements inserted dynamically into DOM?
Here's a method that mimics $('body').on('click', '.edit', function () { ... }):
document.body.addEventListener('click', function (event) {
if (event.target.classList.contains('edit')) {
...
}
})
Working that into your example (which I'll modify a little):
var XXX = {
refs: new WeakMap(),
ClickMe: class {
static get (element) {
// if no instance created
if (!XXX.refs.has(element)) {
console.log('created instance')
// create instance
XXX.refs.set(element, new XXX.ClickMe(element))
} else {
console.log('using cached instance')
}
// return weakly referenced instance
return XXX.refs.get(element)
}
constructor (element) {
this.element = element
}
onClick (event) {
console.log('click happened')
}
},
PrototypeTemplate: class {
constructor (element) {
this.element = element
var templateSelector = this.element.getAttribute('data-template')
var templateElement = document.querySelector(templateSelector)
// use .content.clone() to access copy fragment inside of <template>
// using template API properly, but .innerHTML would be more compatible
this.template = templateElement.innerHTML
this.element.addEventListener('click', this.addBox.bind(this))
}
addBox () {
this.element.insertAdjacentHTML('beforeBegin', this.template, this.element)
}
}
}
Array.from(document.querySelectorAll('[data-template]')).forEach(function (element) {
// just insert the first one here
new XXX.PrototypeTemplate(element).addBox()
})
// event delegation instead of individual ClickMe() event listeners
document.body.addEventListener('click', function (event) {
if (event.target.classList.contains('edit')) {
console.log('delegated click')
// get ClickMe() instance for element, and create one if necessary
// then call its onClick() method using delegation
XXX.ClickMe.get(event.target).onClick(event)
}
})
[data-template] {
cursor: pointer;
}
/* compatibility */
template {
display: none;
}
<span data-template="#box-template">Add box</span>
<template id="box-template">
<div class="box">
<a class="edit" href="#">Edit</a>
</div>
</template>
This uses WeakMap() to hold weak references to each instance of ClickMe(), which allows the event delegation to efficiently delegate by only initializing one instance for each .edit element, and then referencing the already-created instance on future delegated clicks through the static method ClickMe.get(element).
The weak references allow instances of ClickMe() to be garbage collected if its element key is ever removed from the DOM and falls out-of-scope.
You can do something like this...
document.addEventListener('click',function(e){
if(e.target && e.target.className.split(" ")[0]== 'edit'){
new XXX.ClickMe(e.target);}
})
var XXX = {};
XXX.ClickMe = function(element) {
this.element = element;
this.element.addEventListener('click', onClick.bind(this));
};
XXX.PrototypeTemplate = function(element) {
this.element = element;
var tmpl = this.element.getAttribute('data-prototype');
addBox = function() {
this.element.insertAdjacentHTML('beforebegin', tmpl);
};
this.element.addEventListener('click', addBox.bind(this));
};
[...document.querySelectorAll('[data-prototype]')].forEach(
function(element, index) {
new XXX.PrototypeTemplate(element);
}
);
document.addEventListener('click', function(e) {
if (e.target && e.target.className.split(" ")[0] == 'edit') {
console.log('click happend');
}
})
[data-prototype] {
cursor: pointer;
}
<div class="box"><a class="edit" href="#">Edit</a></div>
<span data-prototype="<div class="box"><a class="edit" href="#">Edit</a></div>">Add box</span>
Do it like jQuery: have a parent element that controls the event delegation. In the following, I use document.body as the parent:
document.body.addEventListener('click', e => {
if (e.target.matches('.edit')) {
// do whatever
}
});
Working example:
var XXX = {};
XXX.PrototypeTemplate = function(element) {
this.element = element;
var tmpl = this.element.getAttribute('data-prototype');
addBox = function() {
this.element.insertAdjacentHTML('beforebegin', tmpl);
};
this.element.addEventListener('click', addBox.bind(this));
};
new XXX.PrototypeTemplate(document.querySelector('[data-prototype]'));
document.body.addEventListener('click', e => {
if (e.target.matches('.edit')) {
// do whatever
console.log('click happend');
}
});
[data-prototype] {
cursor: pointer;
}
<div class="box"><a class="edit" href="#">Edit</a></div>
<span data-prototype="<div class="box"><a class="edit" href="#">Edit</a></div>">Add box</span>
Take a look at what MDN says about Element.prototype.matches.
Thanks to all answering on the question, it is all helpfull information. What about the following:
Wrap all the functions needed inside a XXX.InitializeAllFunctions = function(wrap) {} and pass the document as the wrap on first page load. So it behaves like it did before. When inserting new DOM Elements just pass those also to this function before inserting into DOM. Works like a charm:
var XXX = {};
XXX.ClickMe = function(element){
this.element = element;
onClick = function() {
console.log('click happend');
};
this.element.addEventListener('click', onClick.bind(this));
};
XXX.PrototypeTemplate = function(element) {
this.element = element;
addBox = function() {
var tmpl = this.element.getAttribute('data-prototype');
var html = new DOMParser().parseFromString(tmpl, 'text/html');
XXX.InitializeAllFunctions(html); // Initialize here on all new HTML
// before inserting into DOM
this.element.parentNode.insertBefore(
html.body.childNodes[0],
this.element
);
};
this.element.addEventListener('click', addBox.bind(this));
};
XXX.InitializeAllFunctions = function(wrap) {
var wrap = wrap == null ? document : wrap;
[...wrap.querySelectorAll('[data-prototype]')].forEach(
function (element, index) {
new XXX.PrototypeTemplate(element);
}
);
[...wrap.querySelectorAll('.edit')].forEach(
function (element, index) {
new XXX.ClickMe(element);
}
);
};
XXX.InitializeAllFunctions(document);
[data-prototype] {
cursor: pointer;
}
<div class="box"><a class="edit" href="#">Edit</a></div>
<span data-prototype="<div class="box"><a class="edit" href="#">Edit</a></div>">Add box</span>

changed.jstree event not working in jstree inside of class method

I am working on jstree checkbox plugin and I want to get the checked and unchecked state of the checkbox items. The changed.jstree event is not working in the _setEvents() method. But it is working fine when I add the event inside of _setTree() method. I need to get the event trigger in _setEvents() method.
This is the code I am using:
export default class Test {
constructor(container) {
this._tab = null;
this._container = container;
this._setEvents();
}
get tab() {
return this._tab;
}
show(data) {
this._data = data;
this._setTree();
return this;
}
_setEvents() {
const self = this;
$(document)
.on('click', '#js-image-container', () => {
$('#js-image-uploader').trigger('click');
});
$('#js-categories-container').on('changed.jstree', () => this._handleSelection());//this wont work
return this;
}
_setTree() {
$('#js-categories-container').jstree(jsonTreeGenerator.generate(this._data.categories));
$('#js-categories-container').on('changed.jstree', () => this._handleSelection()); //this will work
return this;
}
_handleSelection() {
alert(1);
}
}
Since you are adding the js-categories-container element dynamically, I believe it is not there yet at the time when you bind an event to it. So you have to delegate event handling, e.g. same way as you do for the js-image-container element. Check demo - Fiddle Demo:
$(document).on('click', '#js-image-container',
() => { $('#js-image-uploader').trigger('click'); }
);
$(document).on("click", '#js-categories-container',
() => {
// get the node if you need it
var node = $('#js-categories-container').jstree().get_node(event.target);
// do something
this._handleSelection();
}
);

JavaScript ES6 import/export and class extends

I builded a custom element which is a hamburger button and now I'm working on a side nav. In this side nav I want to use my hamburger button so I try to export my HCHamburger class which correspond to my button and import it in my SideNav class. The idea is to animate my button position when the side nav is opened. I try to extends my SideNav class with HCHamburger but I got the following error : Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function.
My HCHambuger class looks like this :
'use strict';
export default class HCHamburger extends HTMLElement {
get menuButton() {
if (!this._menuButton) {
this._menuButton = this.querySelector('.hamburger-menu');
}
return this._menuButton;
}
get bar() {
if (!this._bar) {
this._bar = this.querySelector('.bar');
}
return this._bar;
}
attachedCallback() {
this.menuButton.addEventListener('click', _ => {
const sideNavContainerEl = document.querySelector('.js-side-nav-container');
this.bar.classList.toggle("animate");
if (sideNavContainerEl.getAttribute('nav-opened') == 'false') {
this.openMenuButton(sideNavContainerEl);
} else {
this.closeMenuButton(sideNavContainerEl);
}
});
}
sayHello() {
console.log('TOTO');
}
openMenuButton(sideNavContainerEl) {
this.style.transform = `translateX(${sideNavContainerEl.offsetWidth}px)`;
}
closeMenuButton(sideNavContainerEl) {
this.style.transform = `translateX(0px)`;
}
}
document.registerElement('hc-hamburger', HCHamburger);
And my SideNav class like this :
'use strict';
import Detabinator from './detabinator.js';
import HCHamburger from './hamburger.js';
class SideNav extends HCHamburger {
constructor () {
super();
this.toggleMenuEl = document.querySelector('.js-menu');
this.showButtonEl = document.querySelector('.js-menu-show');
this.hideButtonEl = document.querySelector('.js-menu-hide');
this.sideNavEl = document.querySelector('.js-side-nav');
this.sideNavContainerEl = document.querySelector('.js-side-nav-container');
// Control whether the container's children can be focused
// Set initial state to inert since the drawer is offscreen
this.detabinator = new Detabinator(this.sideNavContainerEl);
this.detabinator.inert = true;
this.toggleSideNav = this.toggleSideNav.bind(this);
this.showSideNav = this.showSideNav.bind(this);
this.hideSideNav = this.hideSideNav.bind(this);
this.blockClicks = this.blockClicks.bind(this);
this.onTouchStart = this.onTouchStart.bind(this);
this.onTouchMove = this.onTouchMove.bind(this);
this.onTouchEnd = this.onTouchEnd.bind(this);
this.onTransitionEnd = this.onTransitionEnd.bind(this);
this.update = this.update.bind(this);
this.startX = 0;
this.currentX = 0;
this.touchingSideNav = false;
this.supportsPassive = undefined;
this.addEventListeners();
}
// apply passive event listening if it's supported
applyPassive () {
if (this.supportsPassive !== undefined) {
return this.supportsPassive ? {passive: true} : false;
}
// feature detect
let isSupported = false;
try {
document.addEventListener('test', null, {get passive () {
isSupported = true;
}});
} catch (e) { }
this.supportsPassive = isSupported;
return this.applyPassive();
}
addEventListeners () {
this.toggleMenuEl.addEventListener('click', this.toggleSideNav);
this.sideNavEl.addEventListener('click', this.hideSideNav);
this.sideNavContainerEl.addEventListener('click', this.blockClicks);
this.sideNavEl.addEventListener('touchstart', this.onTouchStart, this.applyPassive());
this.sideNavEl.addEventListener('touchmove', this.onTouchMove, this.applyPassive());
this.sideNavEl.addEventListener('touchend', this.onTouchEnd);
}
onTouchStart (evt) {
if (!this.sideNavEl.classList.contains('side-nav--visible'))
return;
this.startX = evt.touches[0].pageX;
this.currentX = this.startX;
this.touchingSideNav = true;
requestAnimationFrame(this.update);
}
onTouchMove (evt) {
if (!this.touchingSideNav)
return;
this.currentX = evt.touches[0].pageX;
const translateX = Math.min(0, this.currentX - this.startX);
if (translateX < 0) {
evt.preventDefault();
}
}
onTouchEnd (evt) {
if (!this.touchingSideNav)
return;
this.touchingSideNav = false;
const translateX = Math.min(0, this.currentX - this.startX);
this.sideNavContainerEl.style.transform = '';
if (translateX < 0) {
this.hideSideNav();
}
}
update () {
if (!this.touchingSideNav)
return;
requestAnimationFrame(this.update);
const translateX = Math.min(0, this.currentX - this.startX);
this.sideNavContainerEl.style.transform = `translateX(${translateX}px)`;
}
blockClicks (evt) {
evt.stopPropagation();
}
onTransitionEnd (evt) {
this.sideNavEl.classList.remove('side-nav--animatable');
this.sideNavEl.removeEventListener('transitionend', this.onTransitionEnd);
}
showSideNav () {
this.sideNavEl.classList.add('side-nav--animatable');
this.sideNavEl.classList.add('side-nav--visible');
this.detabinator.inert = false;
this.sideNavEl.addEventListener('transitionend', this.onTransitionEnd);
}
hideSideNav () {
this.sideNavEl.classList.add('side-nav--animatable');
this.sideNavEl.classList.remove('side-nav--visible');
this.detabinator.inert = true;
this.sideNavEl.addEventListener('transitionend', this.onTransitionEnd);
}
toggleSideNav () {
if (this.sideNavContainerEl.getAttribute('nav-opened') == 'true') {
this.hideSideNav();
this.sideNavContainerEl.setAttribute('nav-opened', 'false');
} else {
this.showSideNav();
this.sideNavContainerEl.setAttribute('nav-opened', 'true');
}
}
}
new SideNav();
I'm using webpack to build my JS code and maybe it's the reason of my issue... I tried different method to import/export but nothing worked.
I thought to just export the method that I needed but it didn't work neither.
Thank's
Fundamentally, there's just a mis-match between the DOM's API and JavaScript's inheritance (at present). You can't do the extends HTMLElement thing on current browsers. You may be able to at some point when the custom elements specification settles down and is widely-implemented in its final form, but not right now.
If you transpile, you'll get the error you have in your question, because the transpiled code tries to do something along these lines:
function MyElement() {
HTMLElement.call(this);
}
var e = new MyElement();
If you don't transpile (requiring ES2015+ support on the browser), you'll likely get a different error:
TypeError: Illegal constructor
class MyElement extends HTMLElement {
}
let e = new MyElement();
You have a couple of options that don't involve inheriting from HTMLElement: Wrapping and prototype augmentation
Wrapping
You have a function that wraps elements. It might create wrappers for individual elements, or sets of elements like jQuery; here's a very simple set example:
// Constructor function creating the wrapper; this one is set-based
// like jQuery, but unlike jQuery requires that you call it via `new`
// (just to keep the example simple).
function Nifty(selectorOrElementOrArray) {
if (!selectorOrElementOrArray) {
this.elements = [];
} else {
if (typeof selectorOrElementOrArray === "string") {
this.elements = Array.prototype.slice.call(
document.querySelectorAll(selectorOrElementOrArray)
);
} else if (Array.isArray(selectorOrElementOrArray)) {
this.elements = selectorOrElementOrArray.slice();
} else {
this.elements = [selectorOrElementOrArray];
}
}
}
Nifty.prototype.addClass = function addClass(cls) {
this.elements.forEach(function(element) {
element.classList.add(cls);
});
};
// Usage
new Nifty(".foo").addClass("test");
new Nifty(".bar").addClass("test2");
.test {
color: green;
}
.test2 {
background-color: yellow;
}
<div id="test">
<span class="bar">bar1</span>
<span class="foo">foo1</span>
<span class="bar">bar2</span>
<span class="foo">foo2</span>
<span class="bar">bar3</span>
</div>
Prototype Augmentation
You can augment HTMLElement.prototype. There are vocal contingents both for and against doing so, the "against" primarily point to the possibility of conflicts if multiple scripts try to add the same properties to it (or if the W3C or WHAT-WG add new properties/methods to it), which is obviously a very real possibility. But if you keep your property names fairly unlikely to be used by others, you can minimize that possibility:
// Add xyzSelect and xyzAddClass to the HTMLElement prototype
Object.defineProperties(HTMLElement.prototype, {
"xyzSelect": {
value: function xyzSelect(selector) {
return Array.prototype.slice.call(this.querySelectorAll(selector));
}
},
"xyzAddClass": {
value: function xyzAddClass(cls) {
return this.classList.add(cls);
}
}
});
// Usage
var test = document.getElementById("test");
test.xyzSelect(".foo").forEach(function(e) { e.xyzAddClass("test"); });
test.xyzSelect(".bar").forEach(function(e) { e.xyzAddClass("test2"); });
.test {
color: green;
}
.test2 {
background-color: yellow;
}
<div id="test">
<span class="bar">bar1</span>
<span class="foo">foo1</span>
<span class="bar">bar2</span>
<span class="foo">foo2</span>
<span class="bar">bar3</span>
</div>
Prototype augmentation works in modern browsers, and also IE8. It didn't work in IE7 and earlier.

Categories