Skip to content

Commit a93b758

Browse files
committed
refactor!: update to Quill v2, drop outdated Firefox patch
1 parent 76a7f05 commit a93b758

File tree

7 files changed

+88
-154
lines changed

7 files changed

+88
-154
lines changed

packages/rich-text-editor/src/vaadin-rich-text-editor-content-styles.js

+34-8
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,37 @@ export const contentStyles = css`
7676
.ql-align-right {
7777
text-align: right;
7878
}
79+
80+
.ql-code-block-container {
81+
font-family: monospace;
82+
}
83+
84+
.ql-editor li {
85+
list-style-type: none;
86+
position: relative;
87+
}
88+
89+
.ql-editor li > .ql-ui::before {
90+
display: inline-block;
91+
margin-left: -1.5em;
92+
margin-right: 0.3em;
93+
text-align: right;
94+
white-space: nowrap;
95+
width: 1.2em;
96+
}
97+
98+
.ql-editor li[data-list='bullet'] {
99+
list-style-type: disc;
100+
}
101+
102+
.ql-editor li[data-list='ordered'] {
103+
counter-increment: list-0;
104+
}
105+
106+
.ql-editor li[data-list='ordered'] > .ql-ui::before {
107+
content: counter(list-0, decimal) '. ';
108+
}
109+
79110
/* quill core end */
80111
81112
blockquote {
@@ -85,24 +116,19 @@ export const contentStyles = css`
85116
padding-left: 1em;
86117
}
87118
88-
code,
89-
pre {
119+
/* Quill converts <pre> to this */
120+
.ql-code-block-container {
90121
background-color: #f0f0f0;
91122
border-radius: 0.1875em;
92123
}
93124
94-
pre {
125+
.ql-code-block-container {
95126
white-space: pre-wrap;
96127
margin-bottom: 0.3125em;
97128
margin-top: 0.3125em;
98129
padding: 0.3125em 0.625em;
99130
}
100131
101-
code {
102-
font-size: 85%;
103-
padding: 0.125em 0.25em;
104-
}
105-
106132
img {
107133
max-width: 100%;
108134
}

packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js

+35-94
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,30 @@
1010
*/
1111
import '../vendor/vaadin-quill.js';
1212
import { timeOut } from '@vaadin/component-base/src/async.js';
13-
import { isFirefox } from '@vaadin/component-base/src/browser-utils.js';
1413
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
1514
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
1615

1716
const Quill = window.Quill;
1817

19-
// Workaround for text disappearing when accepting spellcheck suggestion
20-
// See https://github.com/quilljs/quill/issues/2096#issuecomment-399576957
21-
const Inline = Quill.import('blots/inline');
22-
23-
class CustomColor extends Inline {
24-
constructor(domNode, value) {
25-
super(domNode, value);
26-
27-
// Map <font> properties
28-
domNode.style.color = domNode.color;
29-
30-
const span = this.replaceWith(new Inline(Inline.create()));
31-
32-
span.children.forEach((child) => {
33-
if (child.attributes) child.attributes.copy(span);
34-
if (child.unwrap) child.unwrap();
35-
});
36-
37-
this.remove();
38-
39-
return span; // eslint-disable-line no-constructor-return
18+
// Fix to add `spellcheck="false"` on the `<pre>` tag removed by Quill
19+
// TODO: Quill also removes `<code>` tag from the output, should add it?
20+
const QuillCodeBlockContainer = Quill.import('formats/code-block-container');
21+
22+
class CodeBlockContainer extends QuillCodeBlockContainer {
23+
html(index, length) {
24+
const markup = super.html(index, length);
25+
const tempDiv = document.createElement('div');
26+
tempDiv.innerHTML = markup;
27+
const preTag = tempDiv.querySelector('pre');
28+
if (preTag) {
29+
preTag.setAttribute('spellcheck', 'false');
30+
return preTag.outerHTML;
31+
}
32+
return markup; // fallback
4033
}
4134
}
4235

43-
CustomColor.blotName = 'customColor';
44-
CustomColor.tagName = 'FONT';
45-
46-
Quill.register(CustomColor, true);
36+
Quill.register('formats/code-block-container', CodeBlockContainer, true);
4737

4838
const HANDLERS = [
4939
'bold',
@@ -70,8 +60,6 @@ const STATE = {
7060
CLICKED: 2,
7161
};
7262

73-
const TAB_KEY = 9;
74-
7563
const DEFAULT_I18N = {
7664
undo: 'undo',
7765
redo: 'redo',
@@ -352,11 +340,6 @@ export const RichTextEditorMixin = (superClass) =>
352340
this.__patchToolbar();
353341
this.__patchKeyboard();
354342

355-
/* c8 ignore next 3 */
356-
if (isFirefox) {
357-
this.__patchFirefoxFocus();
358-
}
359-
360343
this.__setDirection(this.__dir);
361344

362345
const editorContent = editor.querySelector('.ql-editor');
@@ -380,23 +363,21 @@ export const RichTextEditorMixin = (superClass) =>
380363
}
381364
});
382365

383-
const TAB_KEY = 9;
384-
385366
editorContent.addEventListener('keydown', (e) => {
386367
if (e.key === 'Escape') {
387368
if (!this.__tabBindings) {
388-
this.__tabBindings = this._editor.keyboard.bindings[TAB_KEY];
389-
this._editor.keyboard.bindings[TAB_KEY] = null;
369+
this.__tabBindings = this._editor.keyboard.bindings.Tab;
370+
this._editor.keyboard.bindings.Tab = null;
390371
}
391372
} else if (this.__tabBindings) {
392-
this._editor.keyboard.bindings[TAB_KEY] = this.__tabBindings;
373+
this._editor.keyboard.bindings.Tab = this.__tabBindings;
393374
this.__tabBindings = null;
394375
}
395376
});
396377

397378
editorContent.addEventListener('blur', () => {
398379
if (this.__tabBindings) {
399-
this._editor.keyboard.bindings[TAB_KEY] = this.__tabBindings;
380+
this._editor.keyboard.bindings.Tab = this.__tabBindings;
400381
this.__tabBindings = null;
401382
}
402383
});
@@ -483,7 +464,7 @@ export const RichTextEditorMixin = (superClass) =>
483464
buttons[index].focus();
484465
}
485466
// Esc and Tab focuses the content
486-
if (e.keyCode === 27 || (e.keyCode === TAB_KEY && !e.shiftKey)) {
467+
if (e.keyCode === 27 || (e.key === 'Tab' && !e.shiftKey)) {
487468
e.preventDefault();
488469
this._editor.focus();
489470
}
@@ -529,52 +510,6 @@ export const RichTextEditorMixin = (superClass) =>
529510
return elem;
530511
}
531512

532-
/** @private */
533-
__patchFirefoxFocus() {
534-
// In Firefox 63+ with native Shadow DOM, when moving focus out of
535-
// contenteditable and back again within same shadow root, cursor
536-
// disappears. See https://bugzilla.mozilla.org/show_bug.cgi?id=1496769
537-
const editorContent = this.shadowRoot.querySelector('.ql-editor');
538-
let isFake = false;
539-
540-
const focusFake = () => {
541-
isFake = true;
542-
this.__fakeTarget = this.__createFakeFocusTarget();
543-
document.body.appendChild(this.__fakeTarget);
544-
// Let the focus step out of shadow root!
545-
this.__fakeTarget.focus();
546-
return new Promise((resolve) => {
547-
setTimeout(resolve);
548-
});
549-
};
550-
551-
const focusBack = (offsetNode, offset) => {
552-
this._editor.focus();
553-
if (offsetNode) {
554-
this._editor.selection.setNativeRange(offsetNode, offset);
555-
}
556-
document.body.removeChild(this.__fakeTarget);
557-
delete this.__fakeTarget;
558-
isFake = false;
559-
};
560-
561-
editorContent.addEventListener('mousedown', (e) => {
562-
if (!this._editor.hasFocus()) {
563-
const { x, y } = e;
564-
const { offset, offsetNode } = document.caretPositionFromPoint(x, y);
565-
focusFake().then(() => {
566-
focusBack(offsetNode, offset);
567-
});
568-
}
569-
});
570-
571-
editorContent.addEventListener('focusin', () => {
572-
if (isFake === false) {
573-
focusFake().then(() => focusBack());
574-
}
575-
});
576-
}
577-
578513
/** @private */
579514
__patchToolbar() {
580515
const toolbar = this._editor.getModule('toolbar');
@@ -602,19 +537,19 @@ export const RichTextEditorMixin = (superClass) =>
602537
this._toolbar.querySelector('button:not([tabindex])').focus();
603538
};
604539

605-
const keyboard = this._editor.getModule('keyboard');
606-
const bindings = keyboard.bindings[TAB_KEY];
540+
const keyboard = this._editor.keyboard;
541+
const bindings = keyboard.bindings.Tab;
607542

608543
// Exclude Quill shift-tab bindings, except for code block,
609544
// as some of those are breaking when on a newline in the list
610545
// https://github.com/vaadin/vaadin-rich-text-editor/issues/67
611546
const originalBindings = bindings.filter((b) => !b.shiftKey || (b.format && b.format['code-block']));
612-
const moveFocusBinding = { key: TAB_KEY, shiftKey: true, handler: focusToolbar };
547+
const moveFocusBinding = { key: 'Tab', shiftKey: true, handler: focusToolbar };
613548

614-
keyboard.bindings[TAB_KEY] = [...originalBindings, moveFocusBinding];
549+
keyboard.bindings.Tab = [...originalBindings, moveFocusBinding];
615550

616551
// Alt-f10 focuses a toolbar button
617-
keyboard.addBinding({ key: 121, altKey: true, handler: focusToolbar });
552+
keyboard.addBinding({ key: 'F10', altKey: true, handler: focusToolbar });
618553
}
619554

620555
/** @private */
@@ -653,6 +588,7 @@ export const RichTextEditorMixin = (superClass) =>
653588
_applyLink(link) {
654589
if (link) {
655590
this._markToolbarClicked();
591+
this._editor.focus();
656592
this._editor.format('link', link, SOURCE.USER);
657593
this._editor.getModule('toolbar').update(this._editor.selection.savedRange);
658594
}
@@ -735,6 +671,7 @@ export const RichTextEditorMixin = (superClass) =>
735671
const color = event.detail.color;
736672
this._colorValue = color === '#000000' ? null : color;
737673
this._markToolbarClicked();
674+
this._editor.focus();
738675
this._editor.format('color', this._colorValue, SOURCE.USER);
739676
this._toolbar.style.setProperty('--_color-value', this._colorValue);
740677
this._colorEditing = false;
@@ -750,15 +687,19 @@ export const RichTextEditorMixin = (superClass) =>
750687
const color = event.detail.color;
751688
this._backgroundValue = color === '#ffffff' ? null : color;
752689
this._markToolbarClicked();
690+
this._editor.focus();
753691
this._editor.format('background', this._backgroundValue, SOURCE.USER);
754692
this._toolbar.style.setProperty('--_background-value', this._backgroundValue);
755693
this._backgroundEditing = false;
756694
}
757695

758696
/** @private */
759697
__updateHtmlValue() {
760-
const editor = this.shadowRoot.querySelector('.ql-editor');
761-
let content = editor.innerHTML;
698+
// We have to use this instead of `innerHTML` to get correct tags like `<pre>` etc.
699+
let content = this._editor.getSemanticHTML();
700+
701+
// TODO there are some issues e.g. `spellcheck="false"` not preserved
702+
// See https://github.com/slab/quill/issues/4289
762703

763704
// Remove Quill classes, e.g. ql-syntax, except for align
764705
content = content.replace(/class="([^"]*)"/gu, (_match, group1) => {
@@ -827,7 +768,7 @@ export const RichTextEditorMixin = (superClass) =>
827768
htmlValue = htmlValue.replaceAll(/>[^<]*</gu, (match) => match.replaceAll(character, replacement)); // NOSONAR
828769
});
829770

830-
const deltaFromHtml = this._editor.clipboard.convert(htmlValue);
771+
const deltaFromHtml = this._editor.clipboard.convert({ html: htmlValue });
831772

832773
// Restore whitespace characters after the conversion
833774
Object.entries(whitespaceCharacters).forEach(([character, replacement]) => {

0 commit comments

Comments
 (0)