I have an application that has multiple "widgets" that can be dragged and dropped onto the widget preview. Each widget has its own class and I am looking for a way to pass these classes into the drag and drop events.
Currently on the dragstart event I am passing the typeof the widget class and converting it to a string:
const addWidget = (widgetType) => {
// widgetBox is the box dragged onto the widget preview to specify the user wants to create this widget
const widgetBox = document.createElement("div");
// style widgetBox
widgetBox.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("widget-type", String(widgetType));
})
}
addWidget(TextboxWidget); // TextboxWidget is a class
On the drop event in the widget preview I then get the widget type string and am forced to manually check for each widget, which is not ideal:
element.addEventListener("drop", () => {
e.preventDefault();
const widgetType = e.dataTransfer.getData("widget-type");
switch(widgetType) {
case String(TextboxWidget):
widget = new TextboxWidget();
break;
// etc for each widget
default:
return;
}
})
Ideally I would like to be able to pass the widget class to the drop event, and then be able to create an instance of it like so:
// dragover
const addWidget = (widget) => {
const widgetBox = document.createElement("div");
widgetBox.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("widget", widget);
})
}
addWidget(TextboxWidget);
// drop
element.addEventListener("drop", () => {
e.preventDefault();
const WidgetClass = e.dataTransfer.getData("widget");
const widgetInstance = new WidgetClass();
Related
I am trying to make a drop down list that dynamically adds elements from an API. When the user selects an item in the dropdown, it should add a class called "current" to that item. Only one dropdown item in the list can have the class 'current' applied to it.
I have successfully create the HTML elements (listItem) and appended them to the list. However, when I try to add event listener, the event registers with the child elements img and div with text such that when user clicks those, the class 'current' is applied there and not the parent node.
I read up on "event bubbling" but not sure if this is my issue or not.
document.addEventListener('DOMContentLoaded', async () => {
const dropDownToggle = document.querySelector('.w-dropdown-toggle')
const dropDownList = document.querySelector('.w-dropdown-list')
const countries = await getCountries();
countries.forEach((country) => {
const countryName = country.name.common;
const cca2 = country.cca2;
const svgUrl = country.flags.svg;
let prefix = country.idd.root + country.idd.suffixes?.[0]
prefix = Number.isNaN(prefix) ? prefix="" : prefix
//console.log(countryName, cca2, prefix)
const listItem = createListItem(countryName, cca2, svgUrl, prefix);
// Bad code here: <a> tag gets event listener but so do its children
listItem.addEventListener("click", (event) => {
console.log(event);
//console.log(event);
//document.querySelector('current')?.classList.remove('current');
//document.querySelector('current').ariaSelected = false;
console.log('hello')
event.target.classList.add("current");
event.target.ariaSelected = true;
console.log('goodbye')
});
dropDownList.append(listItem);
})
});
const getCountries = async () => {
let url = 'https://restcountries.com/v3.1/all'
const response = await fetch(url)
const data = await response.json();
return data;
}
const createListItem = (countryName, cca2, svgUrl, prefix) => {
const template = `
<a data-element="item" aria-role="option" aria-selected="false" href="#" class="dropdown_item w-inline-block" tabindex="0"><img src="${svgUrl}" loading="lazy" data-element="flag" alt="" class="dropdown_flag"><div data-element="value" class="dropdown_text">${cca2}</div></a>
`
const listItem = document.createElement("template");
listItem.innerHTML = template.trim();
return listItem.content.firstElementChild
}
This is expected. event.target is the innermost element that was actually clicked, not the one that the event listener is installed on.
To use the latter, just refer to event.currentTarget or listItem, instead of event.target.
Please don't mark it as duplicate
I'm new in angular material design and I have a problem with mat-autocomplete. I have multiple Mat-Autocomplete in FormArray of FromGroup. On keyup in the input field, it's getting data from API calls and filled the autocomplete. After getting data on Keyup it will open the panel.
when I press word then a list of autocomplete opens then I want this list as infinite-scroll
I have multiple autocomplete in formArray of formGroup.
I don't want to use third-party dependency in the project like ngx-infinite-scroll.
Working Demo in this Stackblitz Link
When you want to detect autocomplete scroll end position you can use custom directive. In this directive you can calculate position of panel control and detect scroll end position, and once scroll end detected you can emit event to component. Directive Name is mat-autocomplete[optionsScroll] so that it auto detects mat-autocomplete component with optionScroll event and this custom directive is applied to all of this matching component. Directive is as follow..
export interface IAutoCompleteScrollEvent {
autoComplete: MatAutocomplete;
scrollEvent: Event;
}
#Directive({
selector: 'mat-autocomplete[optionsScroll]',
exportAs: 'mat-autocomplete[optionsScroll]'
})
export class MatAutocompleteOptionsScrollDirective {
#Input() thresholdPercent = 0.8;
#Output('optionsScroll') scroll = new EventEmitter<IAutoCompleteScrollEvent>();
_onDestroy = new Subject();
constructor(public autoComplete: MatAutocomplete) {
this.autoComplete.opened
.pipe(
tap(() => {
// Note: When autocomplete raises opened, panel is not yet created (by Overlay)
// Note: The panel will be available on next tick
// Note: The panel wil NOT open if there are no options to display
setTimeout(() => {
// Note: remove listner just for safety, in case the close event is skipped.
this.removeScrollEventListener();
this.autoComplete.panel.nativeElement.addEventListener(
'scroll',
this.onScroll.bind(this)
);
}, 5000);
}),
takeUntil(this._onDestroy)
)
.subscribe();
this.autoComplete.closed
.pipe(
tap(() => this.removeScrollEventListener()),
takeUntil(this._onDestroy)
)
.subscribe();
}
private removeScrollEventListener() {
if (this.autoComplete?.panel) {
this.autoComplete.panel.nativeElement.removeEventListener(
'scroll',
this.onScroll
);
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this.removeScrollEventListener();
}
onScroll(event: Event) {
if (this.thresholdPercent === undefined) {
console.log('undefined');
this.scroll.next({ autoComplete: this.autoComplete, scrollEvent: event });
} else {
const scrollTop = (event.target as HTMLElement).scrollTop;
const scrollHeight = (event.target as HTMLElement).scrollHeight;
const elementHeight = (event.target as HTMLElement).clientHeight;
const atBottom = scrollHeight === scrollTop + elementHeight;
if (atBottom) {
this.scroll.next();
}
}
}
}
Now, you have to call scroll event to mat-autocomplete. On every scroll end onScroll() event is called by our directive.
<mat-autocomplete (optionsScroll)="onScroll()" > ... </mat-autocomplete>
Now, You have to load first and next chunk of data to mat-autocomplete like this..
weightData$ = this.startSearch$.pipe(
startWith(''),
debounceTime(200),
switchMap(filter => {
//Note: Reset the page with every new seach text
let currentPage = 1;
return this.next$.pipe(
startWith(currentPage),
//Note: Until the backend responds, ignore NextPage requests.
exhaustMap(_ => this.getProducts(String(filter), currentPage)),
tap(() => currentPage++),
//Note: This is a custom operator because we also need the last emitted value.
//Note: Stop if there are no more pages, or no results at all for the current search text.
takeWhileInclusive((p: any) => p.length > 0),
scan((allProducts: any, newProducts: any) => allProducts.concat(newProducts), [] ) );
})
);
private getProducts(startsWith: string, page: number): Observable<any[]> {
const take = 6;
const skip = page > 0 ? (page - 1) * take : 0;
const filtered = this.weightData.filter(option => String(option).toLowerCase().startsWith(startsWith.toLowerCase()));
return of(filtered.slice(skip, skip + take));
}
onScroll() {
this.next$.next();
}
So at first time we load only first chunk of data, when we reached end of scroll then we again emit event using next$ subject and our stream weightData$ is rerun and gives us appropiate output.
I'm struggling to come up with a clean solution for a design that enables a class to do something when something has happened in another class. I've been looking at Promises, callbacks and events but the application of Promises and events hasnt stuck yet.
My design issue is that I want a user to click on a button, which opens a filemanager (basically a popup with lots if images to select) and then update a thumbnail on the page when the user has selected an image from the filemanager.
I have a class that handles the controls on the main page which creates the filemanager class and opens it.
I want the main class to respond to the selected image and update the widget - I want the filemanager to just be responsible for selections not updating the main page.
So, how do I communicate between the classes so that the main class gets the selected image and does the update?
I've looked at loads of tutorials but confused on the implementation in this case.
Also - do events only relate to DOM elements. For example could I create a custom event to fire once a selection is made? confused.com
here#s the bones of my classes (removed some elements to keep brief):
export let fileImageWidgetControls = class {
/**
* sets up buttons
* sets up control actions
* opens filemanager
* handles update image
*
* */
options = {
scope: ['file', 'image'],
addBtns: {
image: '#add-image',
file: '#add-file'
},
editBtns: {
image: [
'.preview-image',
'#replace-image',
'#remove-image'
],
file: [
'.preview-file',
'#replace-file',
'#remove-file'
]
}
};
constructor() {
this.imageWidget = new updateWidget;
this.imageControls = new ImageControls(this.options);
this.initialiseControls();
this.observer();
}
openFileManager = () => {
const filemanager = new filemanagerHandler({
images:true
});
filemanager.open();
/**HOW WILL THIS CLASS KNOW THAT AN IMAGE HAS BEEN SELECTED?**/
let selectedItem = filemanager.getSelectedAsset();
}
/**
* select image via filemanager
*/
select = () => {
this.openFileManager();
};
initialiseControls = () => {
const module = this;
this.options.scope.forEach((scope) => {
//add
$(this.options.addBtns[scope]).on('click', (e, scope)=> {
e.preventDefault();
module.select(scope);
});
});
}
and here's the filemanager class (again trimmed down to relevant parts):
export let filemanagerHandler = class {
constructor({
images = false, //show image panel
files = false, //hide files panel
upload = true, //show upload panel
serverURL = '/filemanager',
baseUrl = 'http://site.s3.amazonaws.com'
} = {}) {
this.options = {
activeTabs: {
images: images,
files: files,
upload: upload
},
serverURL: serverURL,
baseURL: baseUrl
}
}
/**
* set up filemanager panels and controls
*/
init = () => {
this.imagePreviewModal = $('#imagePreview3');
this.modalSelectBtn = $('#modalSelectBtn');
this.tabClick();
this.setUpPanels(this.options.activeTabs);
this.setUpControls();
this.uploadHander = new uploadHandler;
this.observer();
this.setUpEvents();
}
open = ()=> {
colBox.init({
href: this.options.serverURL
});
colBox.colorbox();
}
/**
* set up controls within filemanager form (once loaded)
*/
setUpControls = () => {
let module = this;
$('.select-image').on('click', function (e) {
e.preventDefault();
module.handleSelection($(this), 'img');
})
}
/**
* close colorbox
* selection then handled from within bind colbox closed
* in calling class
*/
closeColorbox() {
colBox.closeColorbox();
}
setUrl = (url) => {
this.options.serverURL = url;
}
/**
* get the properties of the selected file
* from the element and updates selectedAsset attribute
* #param element (selected element)
* #param type (image or file
* #param callback
*/
handleSelection = (element, type, callback) => {
//set up selected element to be used by calling method in calling class
this.selectedAsset = {
filename: element.attr('href'),
id: element.data('id'),
caption: element.data('caption'),
type: type
}
/** HOW CAN I GET THE CALLING CLASS TO RESPOND TO THIS EVENT/SELECTION?**/
/** IVE THOUGHT ABOUT A CALLBACK HERE BUT FEELS CLUMSY ?**/
callback();
}
getSelectedAsset = () => {
return (this.selectedAsset === undefined ? false : this.selectedAsset);
}
setUpEvents = () =>{
//watch for colorbox to complete and initiate controls in the open document
$(document).bind('cbox_complete', () => {
this.init();
});
}
};
I am working on twitter like user mentions for Quill editor.
My custom blot code is
import Quill from 'quill';
const Base = Quill.import('blots/inline');
export default class MentionBlot extends Base {
static create(data) {
const node = super.create(data.name);
node.innerHTML = `#${data.name}`;
node.setAttribute('contenteditable', false);
node.setAttribute('name', data.name);
node.setAttribute('id', data.id);
return node;
}
MentionBlot.blotName = 'usermention';
MentionBlot.tagName = 'aumention';
I am displaying users list in dropdown. When ever one of the user name is clicked, I am embedding the user as #User in quill editor.
this is the click event I had written for it. The thing I am doing here is I am replacing the text user entered after # with the custom Inline blot.
searchResult.forEach(element => {
element.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
const quillEditor = window.editor;
const content = quillEditor.getText();
const quillRange = quillEditor.selection.savedRange; // cursor position
if (!quillRange || quillRange.length != 0) return;
const cursorPosition = quillRange.index;
let mentionStartAt = 0;
let lengthToBeDeleted = 0;
for (let i = cursorPosition - 1; i >= 0; --i) {
const char = content[i];
if (char == '#') {
mentionStartAt = i;
lengthToBeDeleted += 1;
break;
} else {
lengthToBeDeleted += 1;
}
}
const data = {
name: element.innerHTML,
id: element.getAttribute('id')
};
quillEditor.deleteText(mentionStartAt, lengthToBeDeleted);
quillEditor.insertEmbed(mentionStartAt, 'usermention', data, 'api');
const cursorAfterDelete =
quillEditor.selection.savedRange.index + element.innerHTML.length;
quillEditor.insertText(cursorAfterDelete + 1, ' ', 'api');
quillEditor.setSelection(cursorAfterDelete + 2, 'api');
quillEditor.format('usermention', false, 'api');
});
});
}
Until here, everything is working like charm but the issue I am facing is after inserting the embed usermention blot, If the user enters Enter button on Keyboard to go to new line, Quill's handleEnter() function is getting triggered and it is inserting #undefined usermention blot to the editor.
After executing the above function, my editor state is this.
When I press enter to go to new line, this is the debug state of handleEnter() function - Quill
#undefined usermention got inserted into the editor. I want the user to enter new line.
When the user presses Enter, I understood that quill.format() is returning usermention:true. But if the user presses Enter after typing few more characters, it is taking him to new line and in this case quill.format() is empty.
Can some one please help me in this regard. Thank you.
Reference: https://quilljs.com/docs/modules/keyboard/
Handling enter with Quill keyboard binding is easier than adding addLister to it, the below method helps you to handle whenever the enter event fires in the quill editor
var quill = new Quill('#editor', modules: {
keyboard: {
bindings: bindings
}}});
var bindings = {
handleEnter: {
key: '13', // enter keycode
handler: function(range, context) {
//You can get the context here
}
}
};
I hope the above answer suits your needs.
I use google MATERIAL COMPONENTS FOR THE WEB and have problems with the "Dialogs".
Check my codepen: Dialog
What do I have to do to have multiple dialogs per page?
JS:
// Find all the dialogs on the page
const dialogEls = Array.from(document.querySelectorAll('.mdc-dialog'));
dialogEls.forEach((ele) => {
const dialog = new mdc.dialog.MDCDialog(ele);
dialog.listen('MDCDialog:accept', function() {
console.log('accepted');
})
dialog.listen('MDCDialog:cancel', function() {
console.log('canceled');
})
// From here I do not know how to continue....
// Here the selector '#dialog-login' should still be dynamic
document.querySelector('#dialog-login').addEventListener('click', function (evt) {
event.preventDefault(evt);
dialog.lastFocusedTarget = evt.target;
// This shows all dialogs, which is wrong.
dialog.show();
})
});
I have updated answer from #Jodo.
I suggest for dynamic approach using data attribute on dialog tags with value of opening button.
// Find all the dialogs on the page
const dialogEls = Array.from(document.querySelectorAll('.mdc-dialog'));
dialogEls.forEach((ele) => {
const dialog = new mdc.dialog.MDCDialog(ele);
dialog.listen('MDCDialog:accept', function() {
console.log('accepted');
})
dialog.listen('MDCDialog:cancel', function() {
console.log('canceled');
})
document.querySelector('#' + ele.dataset.dialog).addEventListener('click', function (evt) {
dialog.show();
});
});
https://codepen.io/woytam/pen/abvdZBQ?editors=1010
Simply add data-dialog attribute to each dialog with value of your button/link. JavaScript function then will use ele.dataset.dialog in foreach.
The Dialog opens twice because you create two event listeners for #dialog-login.
One of them opens the Login Dialog the other one opens the Delivery Dialog.
Since you have two different distinct Dialogs, I would suggest a more static way and declare both dialogs independently:
const dialogLoginEle = document.getElementById('mdc-dialog-login');
const dialogLogin = new mdc.dialog.MDCDialog(dialogLoginEle);
dialogLogin.listen('MDCDialog:accept', function() {
console.log('accepted login');
});
dialogLogin.listen('MDCDialog:cancel', function() {
console.log('canceled login');
});
const dialogDeliveryEle = document.getElementById('mdc-dialog-delivery-condition');
const dialogDelivery = new mdc.dialog.MDCDialog(dialogDeliveryEle);
dialogDelivery.listen('MDCDialog:accept', function() {
console.log('accepted delivery');
});
dialogDelivery.listen('MDCDialog:cancel', function() {
console.log('canceled delivery');
});
document.querySelector('#dialog-login').addEventListener('click', function (evt) {
dialogLogin.show();
});
document.querySelector('#dialog-delivery').addEventListener('click', function (evt) {
dialogDelivery.show();
});
https://codepen.io/j-o-d-o/pen/XZqNYy?editors=1010
Here a "dynamic" approach as requested, but IMHO this is not very readable and error prone.
// Find all the dialogs on the page
const dialogEls = Array.from(document.querySelectorAll('.mdc-dialog'));
// First one is the Login, Second one is the Delivery
var dialogArr = [];
dialogEls.forEach((ele) => {
const dialog = new mdc.dialog.MDCDialog(ele);
dialog.listen('MDCDialog:accept', function() {
console.log('accepted');
})
dialog.listen('MDCDialog:cancel', function() {
console.log('canceled');
})
dialogArr.push(dialog);
});
document.querySelector('#dialog-login').addEventListener('click', function (evt) {
dialogArr[0].show();
});
document.querySelector('#dialog-delivery').addEventListener('click', function (evt) {
dialogArr[1].show();
});
https://codepen.io/j-o-d-o/pen/jZxmxa?editors=1010