Skip to content

Commit 9bf3931

Browse files
refactor!: update to Quill v2.0 and use getSemanticHTML (#9007)
* refactor!: update to Quill v2 and use getSemanticHTML * refactor: update Quill build to handle whitespace characters * refactor: remove logic no longer needed with getSemanticHTML * chore: update Quill build to fix Chrome selection issue * chore: update Quill build to version using quill-delta-es * chore: update Quill build to version with lodash-es replaced * test: update visual test screenshots for lists * test: remove unused test The Quill 2.0 semantic HTML does not seem to output any "ql-*" classes except alignment. * chore: remove todo The code tag seems to be still present in the semantic HTML output. * refactor: remove ql class replacing workarounds The workarounds are not necessary anymore since the Quill 2.0 semantic HTML does not contain them. The alignment class transformation is left intact. * test: update baseline images --------- Co-authored-by: ugur-vaadin <[email protected]>
1 parent 8d7e5e8 commit 9bf3931

File tree

10 files changed

+119
-92
lines changed

10 files changed

+119
-92
lines changed

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,41 @@ export const content = css`
118118
.ql-align-right {
119119
text-align: right;
120120
}
121+
122+
.ql-code-block-container {
123+
font-family: monospace;
124+
background-color: var(--vaadin-background-container);
125+
border-radius: var(--vaadin-radius-s);
126+
white-space: pre-wrap;
127+
margin-block: var(--vaadin-padding);
128+
padding: var(--vaadin-padding-container);
129+
}
130+
131+
.ql-editor li {
132+
list-style-type: none;
133+
position: relative;
134+
}
135+
136+
.ql-editor li > .ql-ui::before {
137+
display: inline-block;
138+
margin-left: -1.5em;
139+
margin-right: 0.3em;
140+
text-align: right;
141+
white-space: nowrap;
142+
width: 1.2em;
143+
}
144+
145+
.ql-editor li[data-list='bullet'] {
146+
list-style-type: disc;
147+
}
148+
149+
.ql-editor li[data-list='ordered'] {
150+
counter-increment: list-0;
151+
}
152+
153+
.ql-editor li[data-list='ordered'] > .ql-ui::before {
154+
content: counter(list-0, decimal) '. ';
155+
}
121156
/* quill core end */
122157
123158
blockquote {
@@ -126,10 +161,10 @@ export const content = css`
126161
padding-inline-start: var(--vaadin-padding-s);
127162
}
128163
129-
code,
130-
pre {
164+
code {
131165
background-color: var(--vaadin-background-container);
132166
border-radius: var(--vaadin-radius-s);
167+
padding: 0.125rem 0.25rem;
133168
}
134169
135170
pre {
@@ -138,10 +173,6 @@ export const content = css`
138173
padding: var(--vaadin-padding-container);
139174
}
140175
141-
code {
142-
padding: 0.125rem 0.25rem;
143-
}
144-
145176
img {
146177
max-width: 100%;
147178
}

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

Lines changed: 33 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,26 @@ import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
1515

1616
const Quill = window.Quill;
1717

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

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

4739
const HANDLERS = [
4840
'bold',
@@ -69,8 +61,6 @@ const STATE = {
6961
CLICKED: 2,
7062
};
7163

72-
const TAB_KEY = 9;
73-
7464
const DEFAULT_I18N = {
7565
undo: 'undo',
7666
redo: 'redo',
@@ -374,23 +364,21 @@ export const RichTextEditorMixin = (superClass) =>
374364
}
375365
});
376366

377-
const TAB_KEY = 9;
378-
379367
editorContent.addEventListener('keydown', (e) => {
380368
if (e.key === 'Escape') {
381369
if (!this.__tabBindings) {
382-
this.__tabBindings = this._editor.keyboard.bindings[TAB_KEY];
383-
this._editor.keyboard.bindings[TAB_KEY] = null;
370+
this.__tabBindings = this._editor.keyboard.bindings.Tab;
371+
this._editor.keyboard.bindings.Tab = null;
384372
}
385373
} else if (this.__tabBindings) {
386-
this._editor.keyboard.bindings[TAB_KEY] = this.__tabBindings;
374+
this._editor.keyboard.bindings.Tab = this.__tabBindings;
387375
this.__tabBindings = null;
388376
}
389377
});
390378

391379
editorContent.addEventListener('blur', () => {
392380
if (this.__tabBindings) {
393-
this._editor.keyboard.bindings[TAB_KEY] = this.__tabBindings;
381+
this._editor.keyboard.bindings.Tab = this.__tabBindings;
394382
this.__tabBindings = null;
395383
}
396384
});
@@ -496,7 +484,7 @@ export const RichTextEditorMixin = (superClass) =>
496484
buttons[index].focus();
497485
}
498486
// Esc and Tab focuses the content
499-
if (e.keyCode === 27 || (e.keyCode === TAB_KEY && !e.shiftKey)) {
487+
if (e.keyCode === 27 || (e.key === 'Tab' && !e.shiftKey)) {
500488
e.preventDefault();
501489
this._editor.focus();
502490
}
@@ -552,19 +540,19 @@ export const RichTextEditorMixin = (superClass) =>
552540
this._toolbar.querySelector('button:not([tabindex])').focus();
553541
};
554542

555-
const keyboard = this._editor.getModule('keyboard');
556-
const bindings = keyboard.bindings[TAB_KEY];
543+
const keyboard = this._editor.keyboard;
544+
const bindings = keyboard.bindings.Tab;
557545

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

564-
keyboard.bindings[TAB_KEY] = [...originalBindings, moveFocusBinding];
552+
keyboard.bindings.Tab = [...originalBindings, moveFocusBinding];
565553

566554
// Alt-f10 focuses a toolbar button
567-
keyboard.addBinding({ key: 121, altKey: true, handler: focusToolbar });
555+
keyboard.addBinding({ key: 'F10', altKey: true, handler: focusToolbar });
568556
}
569557

570558
/** @private */
@@ -603,6 +591,7 @@ export const RichTextEditorMixin = (superClass) =>
603591
_applyLink(link) {
604592
if (link) {
605593
this._markToolbarClicked();
594+
this._editor.focus();
606595
this._editor.format('link', link, SOURCE.USER);
607596
this._editor.getModule('toolbar').update(this._editor.selection.savedRange);
608597
}
@@ -686,6 +675,7 @@ export const RichTextEditorMixin = (superClass) =>
686675
const color = event.detail.color;
687676
this._colorValue = color === '#000000' ? null : color;
688677
this._markToolbarClicked();
678+
this._editor.focus();
689679
this._editor.format('color', this._colorValue, SOURCE.USER);
690680
this._toolbar.style.setProperty('--_color-value', this._colorValue);
691681
this._colorEditing = false;
@@ -701,36 +691,23 @@ export const RichTextEditorMixin = (superClass) =>
701691
const color = event.detail.color;
702692
this._backgroundValue = color === '#ffffff' ? null : color;
703693
this._markToolbarClicked();
694+
this._editor.focus();
704695
this._editor.format('background', this._backgroundValue, SOURCE.USER);
705696
this._toolbar.style.setProperty('--_background-value', this._backgroundValue);
706697
this._backgroundEditing = false;
707698
}
708699

709700
/** @private */
710701
__updateHtmlValue() {
711-
const editor = this.shadowRoot.querySelector('.ql-editor');
712-
let content = editor.innerHTML;
713-
714-
// Remove Quill classes, e.g. ql-syntax, except for align
715-
content = content.replace(/class="([^"]*)"/gu, (_match, group1) => {
716-
const classes = group1.split(' ').filter((className) => {
717-
return !className.startsWith('ql-') || className.startsWith('ql-align');
718-
});
719-
return `class="${classes.join(' ')}"`;
720-
});
721-
// Remove meta spans, e.g. cursor which are empty after Quill classes removed
722-
content = content.replace(/<span[^>]*><\/span>/gu, '');
723-
702+
// We have to use this instead of `innerHTML` to get correct tags like `<pre>` etc.
703+
let content = this._editor.getSemanticHTML();
724704
// Replace Quill align classes with inline styles
725705
[this.__dir === 'rtl' ? 'left' : 'right', 'center', 'justify'].forEach((align) => {
726706
content = content.replace(
727707
new RegExp(` class=[\\\\]?"\\s?ql-align-${align}[\\\\]?"`, 'gu'),
728708
` style="text-align: ${align}"`,
729709
);
730710
});
731-
732-
content = content.replace(/ class=""/gu, '');
733-
734711
this._setHtmlValue(content);
735712
}
736713

@@ -778,7 +755,7 @@ export const RichTextEditorMixin = (superClass) =>
778755
htmlValue = htmlValue.replaceAll(/>[^<]*</gu, (match) => match.replaceAll(character, replacement)); // NOSONAR
779756
});
780757

781-
const deltaFromHtml = this._editor.clipboard.convert(htmlValue);
758+
const deltaFromHtml = this._editor.clipboard.convert({ html: htmlValue });
782759

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

packages/rich-text-editor/test/a11y.test.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,14 @@ describe('accessibility', () => {
123123
it('should focus a toolbar button on meta-f10 combo', (done) => {
124124
sinon.stub(buttons[0], 'focus').callsFake(done);
125125
editor.focus();
126-
const e = keyboardEventFor('keydown', 121, ['alt']);
126+
const e = keyboardEventFor('keydown', 121, ['alt'], 'F10');
127127
content.dispatchEvent(e);
128128
});
129129

130130
it('should focus a toolbar button on shift-tab combo', (done) => {
131131
sinon.stub(buttons[0], 'focus').callsFake(done);
132132
editor.focus();
133-
const e = keyboardEventFor('keydown', 9, ['shift']);
133+
const e = keyboardEventFor('keydown', 9, ['shift'], 'Tab');
134134
content.dispatchEvent(e);
135135
});
136136

@@ -141,7 +141,7 @@ describe('accessibility', () => {
141141
done();
142142
});
143143
editor.focus();
144-
const e = keyboardEventFor('keydown', 9, ['shift']);
144+
const e = keyboardEventFor('keydown', 9, ['shift'], 'Tab');
145145
content.dispatchEvent(e);
146146
});
147147

@@ -157,6 +157,7 @@ describe('accessibility', () => {
157157
sinon.stub(editor, 'focus').callsFake(done);
158158
const e = new CustomEvent('keydown', { bubbles: true });
159159
e.keyCode = 9;
160+
e.key = 'Tab';
160161
e.shiftKey = false;
161162
const result = buttons[0].dispatchEvent(e);
162163
expect(result).to.be.false; // DispatchEvent returns false when preventDefault is called
@@ -170,15 +171,15 @@ describe('accessibility', () => {
170171
rte.value = '[{"attributes":{"list":"bullet"},"insert":"Foo\\n"}]';
171172
editor.focus();
172173
editor.setSelection(0, 2);
173-
const e = keyboardEventFor('keydown', 9, ['shift']);
174+
const e = keyboardEventFor('keydown', 9, ['shift'], 'Tab');
174175
content.dispatchEvent(e);
175176
});
176177

177178
it('should change indentation and prevent shift-tab keydown in the code block', () => {
178179
rte.value = '[{"insert":" foo"},{"attributes":{"code-block":true},"insert":"\\n"}]';
179180
editor.focus();
180181
editor.setSelection(2, 0);
181-
const e = keyboardEventFor('keydown', 9, ['shift']);
182+
const e = keyboardEventFor('keydown', 9, ['shift'], 'Tab');
182183
content.dispatchEvent(e);
183184
flushValueDebouncer();
184185
expect(rte.value).to.equal('[{"insert":"foo"},{"attributes":{"code-block":true},"insert":"\\n"}]');

packages/rich-text-editor/test/basic.test.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,6 @@ describe('rich text editor', () => {
277277
expect(rte.htmlValue).to.equal('<h3><em>Foo</em>Bar</h3>');
278278
});
279279

280-
it('should filter out ql-* class names', () => {
281-
// Modify the editor content directly, as setDangerouslyHtmlValue() strips
282-
// classes
283-
rte.shadowRoot.querySelector('.ql-editor').innerHTML =
284-
'<pre class="ql-syntax foo ql-cursor"><code>console.log("hello")</code></pre>';
285-
rte.__updateHtmlValue();
286-
expect(rte.htmlValue).to.equal('<pre class="foo"><code>console.log("hello")</code></pre>');
287-
});
288-
289280
it('should not filter out ql-* in content', () => {
290281
rte.dangerouslySetHtmlValue('<p>mysql-driver</p>');
291282
flushValueDebouncer();
@@ -318,7 +309,7 @@ describe('rich text editor', () => {
318309
const htmlWithExtraSpaces = '<p>Extra spaces</p>';
319310
rte.dangerouslySetHtmlValue(htmlWithExtraSpaces);
320311
flushValueDebouncer();
321-
expect(rte.htmlValue).to.equal(htmlWithExtraSpaces);
312+
expect(rte.htmlValue).to.equal('<p>Extra&nbsp;&nbsp; spaces</p>');
322313
});
323314

324315
it('should not break code block attributes', () => {
@@ -343,7 +334,7 @@ describe('rich text editor', () => {
343334
});
344335

345336
it('should return the quill editor innerHTML', () => {
346-
expect(rte.htmlValue).to.equal('<p><br></p>');
337+
expect(rte.htmlValue).to.equal('<p></p>');
347338
});
348339

349340
it('should be updated from user input to Quill', () => {
-92 Bytes
Loading
-242 Bytes
Loading
40 Bytes
Loading

packages/rich-text-editor/vendor/vaadin-quill.js

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/rich-text-editor/vendor/vaadin-quill.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)