Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,41 @@ export const content = css`
.ql-align-right {
text-align: right;
}

.ql-code-block-container {
font-family: monospace;
background-color: var(--vaadin-background-container);
border-radius: var(--vaadin-radius-s);
white-space: pre-wrap;
margin-block: var(--vaadin-padding);
padding: var(--vaadin-padding-container);
}

.ql-editor li {
list-style-type: none;
position: relative;
}

.ql-editor li > .ql-ui::before {
display: inline-block;
margin-left: -1.5em;
margin-right: 0.3em;
text-align: right;
white-space: nowrap;
width: 1.2em;
}

.ql-editor li[data-list='bullet'] {
list-style-type: disc;
}

.ql-editor li[data-list='ordered'] {
counter-increment: list-0;
}

.ql-editor li[data-list='ordered'] > .ql-ui::before {
content: counter(list-0, decimal) '. ';
}
/* quill core end */

blockquote {
Expand All @@ -126,10 +161,10 @@ export const content = css`
padding-inline-start: var(--vaadin-padding-s);
}

code,
pre {
code {
background-color: var(--vaadin-background-container);
border-radius: var(--vaadin-radius-s);
padding: 0.125rem 0.25rem;
}

pre {
Expand All @@ -138,10 +173,6 @@ export const content = css`
padding: var(--vaadin-padding-container);
}

code {
padding: 0.125rem 0.25rem;
}

img {
max-width: 100%;
}
Expand Down
89 changes: 33 additions & 56 deletions packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,26 @@ import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';

const Quill = window.Quill;

// Workaround for text disappearing when accepting spellcheck suggestion
// See https://github.com/quilljs/quill/issues/2096#issuecomment-399576957
const Inline = Quill.import('blots/inline');

class CustomColor extends Inline {
constructor(domNode, value) {
super(domNode, value);

// Map <font> properties
domNode.style.color = domNode.color;

const span = this.replaceWith(new Inline(Inline.create()));

span.children.forEach((child) => {
if (child.attributes) child.attributes.copy(span);
if (child.unwrap) child.unwrap();
});

this.remove();

return span; // eslint-disable-line no-constructor-return
// There are some issues e.g. `spellcheck="false"` not preserved
// See https://github.com/slab/quill/issues/4289
// Fix to add `spellcheck="false"` on the `<pre>` tag removed by Quill
const QuillCodeBlockContainer = Quill.import('formats/code-block-container');

class CodeBlockContainer extends QuillCodeBlockContainer {
html(index, length) {
const markup = super.html(index, length);
const tempDiv = document.createElement('div');
tempDiv.innerHTML = markup;
const preTag = tempDiv.querySelector('pre');
if (preTag) {
preTag.setAttribute('spellcheck', 'false');
return preTag.outerHTML;
}
return markup; // fallback
}
}

CustomColor.blotName = 'customColor';
CustomColor.tagName = 'FONT';

Quill.register(CustomColor, true);
Quill.register('formats/code-block-container', CodeBlockContainer, true);

const HANDLERS = [
'bold',
Expand All @@ -69,8 +61,6 @@ const STATE = {
CLICKED: 2,
};

const TAB_KEY = 9;

const DEFAULT_I18N = {
undo: 'undo',
redo: 'redo',
Expand Down Expand Up @@ -374,23 +364,21 @@ export const RichTextEditorMixin = (superClass) =>
}
});

const TAB_KEY = 9;

editorContent.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (!this.__tabBindings) {
this.__tabBindings = this._editor.keyboard.bindings[TAB_KEY];
this._editor.keyboard.bindings[TAB_KEY] = null;
this.__tabBindings = this._editor.keyboard.bindings.Tab;
this._editor.keyboard.bindings.Tab = null;
}
} else if (this.__tabBindings) {
this._editor.keyboard.bindings[TAB_KEY] = this.__tabBindings;
this._editor.keyboard.bindings.Tab = this.__tabBindings;
this.__tabBindings = null;
}
});

editorContent.addEventListener('blur', () => {
if (this.__tabBindings) {
this._editor.keyboard.bindings[TAB_KEY] = this.__tabBindings;
this._editor.keyboard.bindings.Tab = this.__tabBindings;
this.__tabBindings = null;
}
});
Expand Down Expand Up @@ -496,7 +484,7 @@ export const RichTextEditorMixin = (superClass) =>
buttons[index].focus();
}
// Esc and Tab focuses the content
if (e.keyCode === 27 || (e.keyCode === TAB_KEY && !e.shiftKey)) {
if (e.keyCode === 27 || (e.key === 'Tab' && !e.shiftKey)) {
e.preventDefault();
this._editor.focus();
}
Expand Down Expand Up @@ -552,19 +540,19 @@ export const RichTextEditorMixin = (superClass) =>
this._toolbar.querySelector('button:not([tabindex])').focus();
};

const keyboard = this._editor.getModule('keyboard');
const bindings = keyboard.bindings[TAB_KEY];
const keyboard = this._editor.keyboard;
const bindings = keyboard.bindings.Tab;

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

keyboard.bindings[TAB_KEY] = [...originalBindings, moveFocusBinding];
keyboard.bindings.Tab = [...originalBindings, moveFocusBinding];

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

/** @private */
Expand Down Expand Up @@ -603,6 +591,7 @@ export const RichTextEditorMixin = (superClass) =>
_applyLink(link) {
if (link) {
this._markToolbarClicked();
this._editor.focus();
this._editor.format('link', link, SOURCE.USER);
this._editor.getModule('toolbar').update(this._editor.selection.savedRange);
}
Expand Down Expand Up @@ -686,6 +675,7 @@ export const RichTextEditorMixin = (superClass) =>
const color = event.detail.color;
this._colorValue = color === '#000000' ? null : color;
this._markToolbarClicked();
this._editor.focus();
this._editor.format('color', this._colorValue, SOURCE.USER);
this._toolbar.style.setProperty('--_color-value', this._colorValue);
this._colorEditing = false;
Expand All @@ -701,36 +691,23 @@ export const RichTextEditorMixin = (superClass) =>
const color = event.detail.color;
this._backgroundValue = color === '#ffffff' ? null : color;
this._markToolbarClicked();
this._editor.focus();
this._editor.format('background', this._backgroundValue, SOURCE.USER);
this._toolbar.style.setProperty('--_background-value', this._backgroundValue);
this._backgroundEditing = false;
}

/** @private */
__updateHtmlValue() {
const editor = this.shadowRoot.querySelector('.ql-editor');
let content = editor.innerHTML;

// Remove Quill classes, e.g. ql-syntax, except for align
content = content.replace(/class="([^"]*)"/gu, (_match, group1) => {
const classes = group1.split(' ').filter((className) => {
return !className.startsWith('ql-') || className.startsWith('ql-align');
});
return `class="${classes.join(' ')}"`;
});
// Remove meta spans, e.g. cursor which are empty after Quill classes removed
content = content.replace(/<span[^>]*><\/span>/gu, '');

// We have to use this instead of `innerHTML` to get correct tags like `<pre>` etc.
let content = this._editor.getSemanticHTML();
// Replace Quill align classes with inline styles
[this.__dir === 'rtl' ? 'left' : 'right', 'center', 'justify'].forEach((align) => {
content = content.replace(
new RegExp(` class=[\\\\]?"\\s?ql-align-${align}[\\\\]?"`, 'gu'),
` style="text-align: ${align}"`,
);
});

content = content.replace(/ class=""/gu, '');

this._setHtmlValue(content);
}

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

const deltaFromHtml = this._editor.clipboard.convert(htmlValue);
const deltaFromHtml = this._editor.clipboard.convert({ html: htmlValue });

// Restore whitespace characters after the conversion
Object.entries(whitespaceCharacters).forEach(([character, replacement]) => {
Expand Down
11 changes: 6 additions & 5 deletions packages/rich-text-editor/test/a11y.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ describe('accessibility', () => {
it('should focus a toolbar button on meta-f10 combo', (done) => {
sinon.stub(buttons[0], 'focus').callsFake(done);
editor.focus();
const e = keyboardEventFor('keydown', 121, ['alt']);
const e = keyboardEventFor('keydown', 121, ['alt'], 'F10');
content.dispatchEvent(e);
});

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

Expand All @@ -141,7 +141,7 @@ describe('accessibility', () => {
done();
});
editor.focus();
const e = keyboardEventFor('keydown', 9, ['shift']);
const e = keyboardEventFor('keydown', 9, ['shift'], 'Tab');
content.dispatchEvent(e);
});

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

it('should change indentation and prevent shift-tab keydown in the code block', () => {
rte.value = '[{"insert":" foo"},{"attributes":{"code-block":true},"insert":"\\n"}]';
editor.focus();
editor.setSelection(2, 0);
const e = keyboardEventFor('keydown', 9, ['shift']);
const e = keyboardEventFor('keydown', 9, ['shift'], 'Tab');
content.dispatchEvent(e);
flushValueDebouncer();
expect(rte.value).to.equal('[{"insert":"foo"},{"attributes":{"code-block":true},"insert":"\\n"}]');
Expand Down
13 changes: 2 additions & 11 deletions packages/rich-text-editor/test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,6 @@ describe('rich text editor', () => {
expect(rte.htmlValue).to.equal('<h3><em>Foo</em>Bar</h3>');
});

it('should filter out ql-* class names', () => {
// Modify the editor content directly, as setDangerouslyHtmlValue() strips
// classes
rte.shadowRoot.querySelector('.ql-editor').innerHTML =
'<pre class="ql-syntax foo ql-cursor"><code>console.log("hello")</code></pre>';
rte.__updateHtmlValue();
expect(rte.htmlValue).to.equal('<pre class="foo"><code>console.log("hello")</code></pre>');
});

it('should not filter out ql-* in content', () => {
rte.dangerouslySetHtmlValue('<p>mysql-driver</p>');
flushValueDebouncer();
Expand Down Expand Up @@ -318,7 +309,7 @@ describe('rich text editor', () => {
const htmlWithExtraSpaces = '<p>Extra spaces</p>';
rte.dangerouslySetHtmlValue(htmlWithExtraSpaces);
flushValueDebouncer();
expect(rte.htmlValue).to.equal(htmlWithExtraSpaces);
expect(rte.htmlValue).to.equal('<p>Extra&nbsp;&nbsp; spaces</p>');
});

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

it('should return the quill editor innerHTML', () => {
expect(rte.htmlValue).to.equal('<p><br></p>');
expect(rte.htmlValue).to.equal('<p></p>');
});

it('should be updated from user input to Quill', () => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions packages/rich-text-editor/vendor/vaadin-quill.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/rich-text-editor/vendor/vaadin-quill.js.map

Large diffs are not rendered by default.

Loading