Undefined blot inserted after embedding custom inline blot in Quill editor - javascript

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.

Related

Getting a response from a modal dialog using a promise

I am currently working on a dialog in order to replace the browser‘s original alert and confirm dialogs since I do not like them. My dialog is very flexible in it‘s use.
So my function is called msgBox and it has got the following syntax:
msgBox (prompt, title, buttons, modal, gradient)
All params are optional. In that case the function uses default values.
So far it works excellent already. Please see the code below.
I want to achieve now, getting as result the button that is clicked by the user to assign it to a variable or use it in an If-statement.
For example:
let response = msgBox("Load Settings?", "Confirm", ["Yes","No", "Restore defaults"]);
or
if (msgBox("Load file?", "Confirm", ["Yes","No", "Search"]) == 'Yes') {
// do some stuff
}
I want the dialog work this way, that it returns'false' in case it is non-modal and the user clicked on an area outside, otherwise it should return the clicked button as string in order to evaluate it.
Here is the code so far:
let isModal = true;
let msgBoxAnswer = null;
// #####################################################################################
// PURPOSE: Displays a modal | modeless dialog with dynamical buttons
//
// PARAMETER: - prompt = prompt text
// - titel = Titelzeie des Dialogs
// - buttons = Button-Array, sepatated by komma - Default = 'Ok'
// - modal = dialog modal | modeless - default = modal
// - gradient = optional: gradient of the titlebar
// RETURNS: : the value of the clicked button as string
// #####################################################################################
async function asyncMsgBox(prompt, title, buttons = ['Ok'], modal = true, gradient) {
// do some init stuff...
msgBoxAnswer = null;
if (prompt == null) {prompt = ''}
if (title == null || title.trim() == '') {
title = document.getElementsByTagName("title")[0].innerHTML;
}
isModal = modal;
let classnames = 'overlay';
if (modal) { classnames += ' dark'}
// search for the parent container (usually it's the <body>)
let parent = document.getElementsByTagName('body')[0];
parent.innerHTML += `<div id="msgBoxBG" class="${classnames}" onclick="closeDialog('cancel')"></div>`;
// catching an empty button-array
if (buttons[0] == '') {buttons[0] = 'Ok'}
// first create the buttons!
// otherwise the <div> is going to be closed automatically
let btnCode ='';
for (let i = 0; i < buttons.length; i++) {
const btn = buttons[i];
btnCode += `<button class="btnMsgbox" onclick="closeDialog('${btn}')">${btn}</button>`;
}
classnames = 'titlebar';
if (gradient) { classnames += ' gradient'}
let container = $('msgBoxBG');
container.innerHTML = `
<div class="dialog">
<div class ="${classnames}">
<h2 id="msgCaption">${title}</h2>
</div>
<p id="msgPrompt">${prompt}</p>
${btnCode}
</div>`;
try {
// HERE START THE PROBLEMS...
let answer = await closeDialog();
return answer; // resolve()
} catch(err){
return false; // reject()
}
}
function closeDialog(response){
return new Promise(function(resolve, reject) {
if (response) {
if (isModal && response == 'cancel') {return};
if (!isModal && response == 'cancel') {
$('msgBoxBG').remove();
reject('false');
} else if (response !='cancel') {
$('msgBoxBG').remove();
resolve(response);
} else {
return;
}
}
});
}
The problem is, that await closeDialog() finds the response-parameter 'undefined' at the first call and returns before the user is able to click on any button at all, which should call the same function.
I must add that I am not very experienced in Javascript yet. So if anyone has another suggestion or solution how to solve the problem, I am open to learn.
Thanks in advance for your help!

How do I use 'click' and 'dbclick' events on one element?

I have a tag element for a web page. when clicking once, one logic is executed, when clicking twice, another. However, the DOM only responds to one click and fires immediately, no matter how quickly you try to fire a double click event.
How do I use these two events on one element?
export const createTag = (name) => {
const tag = document.createElement('span');
tag.innerHTML = name;
tag.addEventListener('dblclick', () => {
tags.removeChild(tag);
storeTags.splice(storeTags.indexOf(name), 1);
});
tag.addEventListener('click', () => {
search.value = tag.innerHTML;
const keyboardEvent = new KeyboardEvent('keypress', {
code: 'Enter',
key: 'Enter',
charCode: 13,
keyCode: 13,
view: window,
bubbles: true,
});
storeTags.splice(storeTags.indexOf(name), 1);
storeTags.unshift(name);
search.dispatchEvent(keyboardEvent);
});
return tag;
};
The basic idea to achieve this would be to add some small delay to the click operation, and cancel that if a dblclick is received. This does lead to reduced responsiveness of your UI, as your application is now having to explicitly distinguish between single and double clicks in a kind of "wait and see" approach. This can also be unreliable as the user may set any length of time as their double-click threshold in their device's Accessibility settings.
However, if these issues are deemed not to be enough of a concern, the "best of the worst" way to do this would be to manually implement double-click checks.
export const createTag = (name) => {
const tag = document.createElement('span');
tag.innerHTML = name;
const clickHander = () => {
search.value = tag.innerHTML;
const keyboardEvent = new KeyboardEvent('keypress', {
code: 'Enter',
key: 'Enter',
charCode: 13,
keyCode: 13,
view: window,
bubbles: true,
});
storeTags.splice(storeTags.indexOf(name), 1);
storeTags.unshift(name);
search.dispatchEvent(keyboardEvent);
};
const dblClickHandler = () => {
tags.removeChild(tag);
storeTags.splice(storeTags.indexOf(name), 1);
};
let clickTimer;
tag.addEventListener('click', () => {
if( clickTimer) {
clearTimeout(clickTimer);
dblClickHandler();
clickTimer = null;
}
else {
clickTimer = setTimeout(() => {
clickHandler();
clickTimer = null;
}, 500);
}
});
return tag;
};
Adjust the timer as you feel appropriate. Smaller numbers will lead to more responsive clicks, but will make double-clicking harder for people with reduced mobility.
I would really strongly recommend using a different UI here. Overloading clicks in this way is a really bad idea in general.

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.

Custom event on fragment change in Reveal.js

What would be the right way to programmatically add fragments to a slide in Reveal.js? I have a JavaScript widget on a slide that can go through 5 states, and I would like to go through them with fragment transitions.
I tried to achieve something similar with dummy fragments, like in the representative example below. This is intended to change the src of an image on fragment change. The example has an issue, though. When approaching a slide by pressing previous a number of times, the slide should start at its last fragment state. In the example, however, the image src starts in state 1, and doesn't know how to go further back on additional previous-steps.
Any pointers would be appreciated!
<img src="img1.png" id="my-image">
<span class="fragment update-img-src" data-target="my-image" data-src="img2.svg"></span>
<script>
Reveal.addEventListener('fragmentshown', function(event) {
if (event.fragment.classList.contains('update-img-src')) {
// Find the target image by ID
var target = document.getElementById(event.fragment.dataset.target);
// Keep a stack of previously shown images, so we can always revert back on 'fragmenthidden'
if (target.dataset.stack == null) {
target.dataset.stack = JSON.stringify([target.getAttribute('src')]);
}
target.dataset.stack = JSON.stringify([event.fragment.dataset.src, ...JSON.parse(target.dataset.stack)]);
// Update the image
target.setAttribute('src', event.fragment.dataset.src);
}
});
Reveal.addEventListener('fragmenthidden', function(event) {
if (event.fragment.classList.contains('update-img-src')) {
// Return to the previously shown image.
// Remove the top from the history stack
var target = document.getElementById(event.fragment.dataset.target);
if (target.dataset.stack == null) {
console.log('Trying to hide', event.fragment.dataset.src, 'but there is no stack.');
} else {
var [_, ...tail] = JSON.parse(target.dataset.stack);
target.dataset.stack = JSON.stringify(tail);
// Set the image source to the previous value
target.setAttribute('src', tail[0]);
}
}
});
</script>
Here's a hacky solution that I put together. It allows you to register any number of fragments on a slide with a callback function.
function registerFakeFragments(slide, fragmentIndices, stateChangeHandler) {
const identifier = `fake-${Math.round(1000000000*Math.random())}`;
let i = 1;
for (let fragmentIndex of fragmentIndices) {
const span = document.createElement('span');
span.dataset.target = identifier;
span.classList.add('fragment');
span.classList.add('fake-fragment');
span.setAttribute('data-fragment-index', JSON.stringify(fragmentIndex));
span.dataset.stateIndex = JSON.stringify(i);
slide.appendChild(span);
++i;
}
let currentState = null; // last reported state
const listener = () => {
const currentSlide = Reveal.getCurrentSlide();
if (currentSlide && currentSlide === slide) {
// Find the latest visible state
let state = 0;
currentSlide.querySelectorAll(`.fake-fragment.visible[data-target=${identifier}]`).forEach(f => {
const index = JSON.parse(f.dataset.stateIndex);
if (index > state) {
state = index;
}
});
// If the state changed, call the handler.
if (state != currentState) {
stateChangeHandler(state);
currentState = state;
}
}
};
Reveal.addEventListener('fragmentshown', listener);
Reveal.addEventListener('fragmenthidden', listener);
Reveal.addEventListener('slidechanged', listener);
}

CodeMirror - insert text into editor when there are multiple editors

I have two codemirror editors on one page. A drop down list of items and radio group to target the correct editor.
What I want to do is on change of the drop down list insert the value of the item into the targeted editor (deleted by the radio group).
my code is as below: however the function isnt working. When I alert the item value and the target I get expected results, however the function to insert the text is failing:
<script type="text/javascript">
function editor(id) {
var editor = CodeMirror.fromTextArea(id, {
continuousScanning: 500,
lineNumbers: true
});
editor.setSize(null, 550);
}
var config_id = document.getElementById('id_config')
var config = editor(config_id);
var remote_config_id = document.getElementById('id_remote_config')
var remote_config = editor(remote_config_id);
function insertStringInTemplate(str, target) {
if (target== "id_config") {
var doc = config
} else {
var doc = remote_config
}
var cursor = doc.getCursor();
var pos = {
line: cursor.line,
ch: cursor.ch
}
doc.replaceRange(str, pos);
}
$(function(){
// bind change event to select
$('#template_vars').on('change', function () {
var var_data = $(this).val(); // get selected value
var var_target = $('input[name=target]:checked').val();
insertStringInTemplate(var_data, var_target)
return false;
});
});
$("#template_vars").chosen({no_results_text: "Oops, nothing found!"});
</script>
however the function to insert the text is failing
That function (i.e. insertStringInTemplate()) is working good/properly; however, the problem is with the editor() function, where you forgot to return the editor (i.e. the CodeMirror instance).
So a simple fix would be:
function editor(id) {
var editor = CodeMirror.fromTextArea(id, {
continuousScanning: 500,
lineNumbers: true
});
editor.setSize(null, 550);
return editor; // <- here's the fix
}
Demo on CodePen.
However in that demo, I added an if block to the insertStringInTemplate() function, as in the following code:
function insertStringInTemplate(str, target) {
if (target== "id_config") {
var doc = config
} else {
var doc = remote_config
}
// If there's a selection, replace the selection.
if ( doc.somethingSelected() ) {
doc.replaceSelection( str );
return;
}
// Otherwise, we insert at the cursor position.
var cursor = doc.getCursor();
var pos = {
line: cursor.line,
ch: cursor.ch
}
doc.replaceRange(str, pos);
}

Categories

Resources