@vaadin/rich-text-editor 25.0.0-alpha2 → 25.0.0-alpha20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -21
- package/src/styles/vaadin-rich-text-editor-base-icons.js +38 -0
- package/src/styles/vaadin-rich-text-editor-base-styles.d.ts +13 -0
- package/src/styles/vaadin-rich-text-editor-base-styles.js +580 -0
- package/src/styles/vaadin-rich-text-editor-popup-overlay-base-styles.js +43 -0
- package/src/vaadin-rich-text-editor-mixin.d.ts +2 -0
- package/src/vaadin-rich-text-editor-mixin.js +119 -143
- package/src/vaadin-rich-text-editor-popup.js +97 -13
- package/src/vaadin-rich-text-editor.d.ts +11 -9
- package/src/vaadin-rich-text-editor.js +172 -85
- package/vaadin-rich-text-editor.js +1 -1
- package/vendor/vaadin-quill.js +4 -3
- package/vendor/vaadin-quill.js.map +1 -1
- package/web-types.json +2 -2
- package/web-types.lit.json +2 -2
- package/src/vaadin-rich-text-editor-content-styles.js +0 -115
- package/src/vaadin-rich-text-editor-icons.js +0 -90
- package/src/vaadin-rich-text-editor-popup-mixin.js +0 -70
- package/src/vaadin-rich-text-editor-styles.js +0 -61
- package/src/vaadin-rich-text-editor-toolbar-styles.js +0 -180
- package/theme/lumo/vaadin-rich-text-editor-styles.d.ts +0 -4
- package/theme/lumo/vaadin-rich-text-editor-styles.js +0 -296
- package/theme/lumo/vaadin-rich-text-editor.d.ts +0 -6
- package/theme/lumo/vaadin-rich-text-editor.js +0 -6
|
@@ -9,41 +9,33 @@
|
|
|
9
9
|
* license.
|
|
10
10
|
*/
|
|
11
11
|
import '../vendor/vaadin-quill.js';
|
|
12
|
+
import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
12
13
|
import { timeOut } from '@vaadin/component-base/src/async.js';
|
|
13
|
-
import { isFirefox } from '@vaadin/component-base/src/browser-utils.js';
|
|
14
14
|
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
|
15
15
|
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
|
|
16
16
|
|
|
17
17
|
const Quill = window.Quill;
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
// See https://github.com/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
this.remove();
|
|
38
|
-
|
|
39
|
-
return span; // eslint-disable-line no-constructor-return
|
|
19
|
+
// There are some issues e.g. `spellcheck="false"` not preserved
|
|
20
|
+
// See https://github.com/slab/quill/issues/4289
|
|
21
|
+
// Fix to add `spellcheck="false"` on the `<pre>` tag removed by Quill
|
|
22
|
+
const QuillCodeBlockContainer = Quill.import('formats/code-block-container');
|
|
23
|
+
|
|
24
|
+
class CodeBlockContainer extends QuillCodeBlockContainer {
|
|
25
|
+
html(index, length) {
|
|
26
|
+
const markup = super.html(index, length);
|
|
27
|
+
const tempDiv = document.createElement('div');
|
|
28
|
+
tempDiv.innerHTML = markup;
|
|
29
|
+
const preTag = tempDiv.querySelector('pre');
|
|
30
|
+
if (preTag) {
|
|
31
|
+
preTag.setAttribute('spellcheck', 'false');
|
|
32
|
+
return preTag.outerHTML;
|
|
33
|
+
}
|
|
34
|
+
return markup; // fallback
|
|
40
35
|
}
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
CustomColor.tagName = 'FONT';
|
|
45
|
-
|
|
46
|
-
Quill.register(CustomColor, true);
|
|
38
|
+
Quill.register('formats/code-block-container', CodeBlockContainer, true);
|
|
47
39
|
|
|
48
40
|
const HANDLERS = [
|
|
49
41
|
'bold',
|
|
@@ -70,8 +62,6 @@ const STATE = {
|
|
|
70
62
|
CLICKED: 2,
|
|
71
63
|
};
|
|
72
64
|
|
|
73
|
-
const TAB_KEY = 9;
|
|
74
|
-
|
|
75
65
|
const DEFAULT_I18N = {
|
|
76
66
|
undo: 'undo',
|
|
77
67
|
redo: 'redo',
|
|
@@ -88,6 +78,8 @@ const DEFAULT_I18N = {
|
|
|
88
78
|
superscript: 'superscript',
|
|
89
79
|
listOrdered: 'list ordered',
|
|
90
80
|
listBullet: 'list bullet',
|
|
81
|
+
outdent: 'outdent',
|
|
82
|
+
indent: 'indent',
|
|
91
83
|
alignLeft: 'align left',
|
|
92
84
|
alignCenter: 'align center',
|
|
93
85
|
alignRight: 'align right',
|
|
@@ -177,7 +169,7 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
177
169
|
'#ffffff', '#facccc', '#ffebcc', '#ffffcc', '#cce8cc', '#cce0f5', '#ebd6ff',
|
|
178
170
|
'#bbbbbb', '#f06666', '#ffc266', '#ffff66', '#66b966', '#66a3e0', '#c285ff',
|
|
179
171
|
'#888888', '#a10000', '#b26b00', '#b2b200', '#006100', '#0047b2', '#6b24b2',
|
|
180
|
-
'#444444', '#5c0000', '#663d00', '#666600', '#003700', '#002966', '#3d1466'
|
|
172
|
+
'#444444', '#5c0000', '#663d00', '#666600', '#003700', '#002966', '#3d1466',
|
|
181
173
|
];
|
|
182
174
|
},
|
|
183
175
|
},
|
|
@@ -352,11 +344,6 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
352
344
|
this.__patchToolbar();
|
|
353
345
|
this.__patchKeyboard();
|
|
354
346
|
|
|
355
|
-
/* c8 ignore next 3 */
|
|
356
|
-
if (isFirefox) {
|
|
357
|
-
this.__patchFirefoxFocus();
|
|
358
|
-
}
|
|
359
|
-
|
|
360
347
|
this.__setDirection(this.__dir);
|
|
361
348
|
|
|
362
349
|
const editorContent = editor.querySelector('.ql-editor');
|
|
@@ -380,23 +367,21 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
380
367
|
}
|
|
381
368
|
});
|
|
382
369
|
|
|
383
|
-
const TAB_KEY = 9;
|
|
384
|
-
|
|
385
370
|
editorContent.addEventListener('keydown', (e) => {
|
|
386
371
|
if (e.key === 'Escape') {
|
|
387
372
|
if (!this.__tabBindings) {
|
|
388
|
-
this.__tabBindings = this._editor.keyboard.bindings
|
|
389
|
-
this._editor.keyboard.bindings
|
|
373
|
+
this.__tabBindings = this._editor.keyboard.bindings.Tab;
|
|
374
|
+
this._editor.keyboard.bindings.Tab = null;
|
|
390
375
|
}
|
|
391
376
|
} else if (this.__tabBindings) {
|
|
392
|
-
this._editor.keyboard.bindings
|
|
377
|
+
this._editor.keyboard.bindings.Tab = this.__tabBindings;
|
|
393
378
|
this.__tabBindings = null;
|
|
394
379
|
}
|
|
395
380
|
});
|
|
396
381
|
|
|
397
382
|
editorContent.addEventListener('blur', () => {
|
|
398
383
|
if (this.__tabBindings) {
|
|
399
|
-
this._editor.keyboard.bindings
|
|
384
|
+
this._editor.keyboard.bindings.Tab = this.__tabBindings;
|
|
400
385
|
this.__tabBindings = null;
|
|
401
386
|
}
|
|
402
387
|
});
|
|
@@ -422,13 +407,35 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
422
407
|
// Flush pending htmlValue only once the editor is fully initialized
|
|
423
408
|
this.__flushPendingHtmlValue();
|
|
424
409
|
|
|
425
|
-
this
|
|
426
|
-
this
|
|
410
|
+
this.querySelector('[slot="color-popup"]').target = this.shadowRoot.querySelector('#btn-color');
|
|
411
|
+
this.querySelector('[slot="background-popup"]').target = this.shadowRoot.querySelector('#btn-background');
|
|
412
|
+
|
|
413
|
+
// Set up tooltip to show when hovering or focusing toolbar buttons
|
|
414
|
+
this._tooltip = document.createElement('vaadin-tooltip');
|
|
415
|
+
this._tooltip.slot = 'tooltip';
|
|
416
|
+
// Set ariaTarget to null, as toolbar buttons already have aria-label,
|
|
417
|
+
// and also cannot be linked with the tooltip being in the light DOM
|
|
418
|
+
this._tooltip.ariaTarget = null;
|
|
419
|
+
this.append(this._tooltip);
|
|
420
|
+
|
|
421
|
+
const buttons = this.shadowRoot.querySelectorAll('[part~="toolbar-button"]');
|
|
422
|
+
buttons.forEach((button) => {
|
|
423
|
+
button.addEventListener('mouseenter', this.__showTooltip.bind(this));
|
|
424
|
+
button.addEventListener('focusin', this.__showTooltip.bind(this));
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
427
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
428
|
+
/** @private */
|
|
429
|
+
__showTooltip({ type, target }) {
|
|
430
|
+
// Only show tooltip when focused with keyboard
|
|
431
|
+
if (type === 'focusin' && !isKeyboardActive()) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this._tooltip.target = target;
|
|
435
|
+
this._tooltip.text = target.ariaLabel;
|
|
436
|
+
this._tooltip._stateController.open({
|
|
437
|
+
focus: type === 'focusin',
|
|
438
|
+
hover: type === 'mouseenter',
|
|
432
439
|
});
|
|
433
440
|
}
|
|
434
441
|
|
|
@@ -483,7 +490,7 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
483
490
|
buttons[index].focus();
|
|
484
491
|
}
|
|
485
492
|
// Esc and Tab focuses the content
|
|
486
|
-
if (e.keyCode === 27 || (e.
|
|
493
|
+
if (e.keyCode === 27 || (e.key === 'Tab' && !e.shiftKey)) {
|
|
487
494
|
e.preventDefault();
|
|
488
495
|
this._editor.focus();
|
|
489
496
|
}
|
|
@@ -512,69 +519,6 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
512
519
|
this._toolbarState = STATE.DEFAULT;
|
|
513
520
|
}
|
|
514
521
|
|
|
515
|
-
/** @private */
|
|
516
|
-
__createFakeFocusTarget() {
|
|
517
|
-
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
|
518
|
-
const elem = document.createElement('textarea');
|
|
519
|
-
// Reset box model
|
|
520
|
-
elem.style.border = '0';
|
|
521
|
-
elem.style.padding = '0';
|
|
522
|
-
elem.style.margin = '0';
|
|
523
|
-
// Move element out of screen horizontally
|
|
524
|
-
elem.style.position = 'absolute';
|
|
525
|
-
elem.style[isRTL ? 'right' : 'left'] = '-9999px';
|
|
526
|
-
// Move element to the same position vertically
|
|
527
|
-
const yPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|
528
|
-
elem.style.top = `${yPosition}px`;
|
|
529
|
-
return elem;
|
|
530
|
-
}
|
|
531
|
-
|
|
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
|
-
|
|
578
522
|
/** @private */
|
|
579
523
|
__patchToolbar() {
|
|
580
524
|
const toolbar = this._editor.getModule('toolbar');
|
|
@@ -589,7 +533,6 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
589
533
|
toolbar.controls.forEach((pair) => {
|
|
590
534
|
const input = pair[1];
|
|
591
535
|
const isActive = input.classList.contains('ql-active');
|
|
592
|
-
input.toggleAttribute('on', isActive);
|
|
593
536
|
input.part.toggle('toolbar-button-pressed', isActive);
|
|
594
537
|
});
|
|
595
538
|
};
|
|
@@ -602,19 +545,13 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
602
545
|
this._toolbar.querySelector('button:not([tabindex])').focus();
|
|
603
546
|
};
|
|
604
547
|
|
|
605
|
-
const keyboard = this._editor.
|
|
606
|
-
const bindings = keyboard.bindings[TAB_KEY];
|
|
607
|
-
|
|
608
|
-
// Exclude Quill shift-tab bindings, except for code block,
|
|
609
|
-
// as some of those are breaking when on a newline in the list
|
|
610
|
-
// https://github.com/vaadin/vaadin-rich-text-editor/issues/67
|
|
611
|
-
const originalBindings = bindings.filter((b) => !b.shiftKey || (b.format && b.format['code-block']));
|
|
612
|
-
const moveFocusBinding = { key: TAB_KEY, shiftKey: true, handler: focusToolbar };
|
|
548
|
+
const keyboard = this._editor.keyboard;
|
|
613
549
|
|
|
614
|
-
|
|
550
|
+
// Shift + Tab focuses a toolbar button unless we are in list / code block
|
|
551
|
+
keyboard.addBinding({ key: 'Tab', shiftKey: true, handler: focusToolbar });
|
|
615
552
|
|
|
616
553
|
// Alt-f10 focuses a toolbar button
|
|
617
|
-
keyboard.addBinding({ key:
|
|
554
|
+
keyboard.addBinding({ key: 'F10', altKey: true, handler: focusToolbar });
|
|
618
555
|
}
|
|
619
556
|
|
|
620
557
|
/** @private */
|
|
@@ -653,6 +590,7 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
653
590
|
_applyLink(link) {
|
|
654
591
|
if (link) {
|
|
655
592
|
this._markToolbarClicked();
|
|
593
|
+
this._editor.focus();
|
|
656
594
|
this._editor.format('link', link, SOURCE.USER);
|
|
657
595
|
this._editor.getModule('toolbar').update(this._editor.selection.savedRange);
|
|
658
596
|
}
|
|
@@ -721,12 +659,14 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
721
659
|
if (e.keyCode === 13) {
|
|
722
660
|
e.preventDefault();
|
|
723
661
|
e.stopPropagation();
|
|
724
|
-
this
|
|
662
|
+
this._onLinkEditConfirm();
|
|
663
|
+
this._closeLinkDialog();
|
|
725
664
|
}
|
|
726
665
|
}
|
|
727
666
|
|
|
728
667
|
/** @private */
|
|
729
668
|
__onColorClick() {
|
|
669
|
+
this._tooltip.opened = false;
|
|
730
670
|
this._colorEditing = true;
|
|
731
671
|
}
|
|
732
672
|
|
|
@@ -735,6 +675,7 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
735
675
|
const color = event.detail.color;
|
|
736
676
|
this._colorValue = color === '#000000' ? null : color;
|
|
737
677
|
this._markToolbarClicked();
|
|
678
|
+
this._editor.focus();
|
|
738
679
|
this._editor.format('color', this._colorValue, SOURCE.USER);
|
|
739
680
|
this._toolbar.style.setProperty('--_color-value', this._colorValue);
|
|
740
681
|
this._colorEditing = false;
|
|
@@ -742,6 +683,7 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
742
683
|
|
|
743
684
|
/** @private */
|
|
744
685
|
__onBackgroundClick() {
|
|
686
|
+
this._tooltip.opened = false;
|
|
745
687
|
this._backgroundEditing = true;
|
|
746
688
|
}
|
|
747
689
|
|
|
@@ -750,6 +692,7 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
750
692
|
const color = event.detail.color;
|
|
751
693
|
this._backgroundValue = color === '#ffffff' ? null : color;
|
|
752
694
|
this._markToolbarClicked();
|
|
695
|
+
this._editor.focus();
|
|
753
696
|
this._editor.format('background', this._backgroundValue, SOURCE.USER);
|
|
754
697
|
this._toolbar.style.setProperty('--_background-value', this._backgroundValue);
|
|
755
698
|
this._backgroundEditing = false;
|
|
@@ -757,30 +700,66 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
757
700
|
|
|
758
701
|
/** @private */
|
|
759
702
|
__updateHtmlValue() {
|
|
760
|
-
|
|
761
|
-
let content =
|
|
762
|
-
|
|
763
|
-
// Remove Quill classes, e.g. ql-syntax, except for align
|
|
703
|
+
// We have to use this instead of `innerHTML` to get correct tags like `<pre>` etc.
|
|
704
|
+
let content = this._editor.getSemanticHTML();
|
|
705
|
+
// Remove Quill classes, e.g. ql-syntax, except for align and indent
|
|
764
706
|
content = content.replace(/class="([^"]*)"/gu, (_match, group1) => {
|
|
765
707
|
const classes = group1.split(' ').filter((className) => {
|
|
766
|
-
return !className.startsWith('ql-') || className.startsWith('ql-align');
|
|
708
|
+
return !className.startsWith('ql-') || className.startsWith('ql-align') || className.startsWith('ql-indent');
|
|
767
709
|
});
|
|
768
710
|
return `class="${classes.join(' ')}"`;
|
|
769
711
|
});
|
|
770
|
-
//
|
|
771
|
-
content =
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
712
|
+
// Process align and indent classes
|
|
713
|
+
content = this.__processQuillClasses(content);
|
|
714
|
+
this._setHtmlValue(content);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** @private */
|
|
718
|
+
__processQuillClasses(content) {
|
|
719
|
+
const tempDiv = document.createElement('div');
|
|
720
|
+
tempDiv.innerHTML = content;
|
|
721
|
+
// Process only elements with align or indent classes
|
|
722
|
+
const elementsToProcess = tempDiv.querySelectorAll('[class*="ql-align"], [class*="ql-indent"]');
|
|
723
|
+
elementsToProcess.forEach((element) => {
|
|
724
|
+
this.__processAlignClasses(element);
|
|
725
|
+
this.__processIndentClasses(element);
|
|
726
|
+
element.removeAttribute('class');
|
|
779
727
|
});
|
|
728
|
+
return tempDiv.innerHTML;
|
|
729
|
+
}
|
|
780
730
|
|
|
781
|
-
|
|
731
|
+
/** @private */
|
|
732
|
+
__processAlignClasses(element) {
|
|
733
|
+
let styleText = element.getAttribute('style') || '';
|
|
734
|
+
const alignments = [this.__dir === 'rtl' ? 'left' : 'right', 'center', 'justify'];
|
|
735
|
+
alignments.forEach((align) => {
|
|
736
|
+
if (element.classList.contains(`ql-align-${align}`)) {
|
|
737
|
+
const newStyle = `text-align: ${align}`;
|
|
738
|
+
styleText = styleText ? `${styleText}; ${newStyle}` : newStyle;
|
|
739
|
+
element.setAttribute('style', styleText);
|
|
740
|
+
element.classList.remove(`ql-align-${align}`);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
782
744
|
|
|
783
|
-
|
|
745
|
+
/** @private */
|
|
746
|
+
__processIndentClasses(element) {
|
|
747
|
+
const indentClass = Array.from(element.classList).find((className) => className.startsWith('ql-indent-'));
|
|
748
|
+
if (indentClass) {
|
|
749
|
+
const level = parseInt(indentClass.replace('ql-indent-', '').trim(), 10);
|
|
750
|
+
const tabs = '\t'.repeat(level);
|
|
751
|
+
// Add tabs to content
|
|
752
|
+
const firstChild = element.firstChild;
|
|
753
|
+
if (firstChild && firstChild.nodeType === Node.TEXT_NODE) {
|
|
754
|
+
firstChild.textContent = tabs + firstChild.textContent;
|
|
755
|
+
} else if (element.childNodes.length > 0) {
|
|
756
|
+
const tabNode = document.createTextNode(tabs);
|
|
757
|
+
element.insertBefore(tabNode, element.firstChild);
|
|
758
|
+
} else {
|
|
759
|
+
element.textContent = tabs;
|
|
760
|
+
}
|
|
761
|
+
element.classList.remove(indentClass);
|
|
762
|
+
}
|
|
784
763
|
}
|
|
785
764
|
|
|
786
765
|
/**
|
|
@@ -827,7 +806,7 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
827
806
|
htmlValue = htmlValue.replaceAll(/>[^<]*</gu, (match) => match.replaceAll(character, replacement)); // NOSONAR
|
|
828
807
|
});
|
|
829
808
|
|
|
830
|
-
const deltaFromHtml = this._editor.clipboard.convert(htmlValue);
|
|
809
|
+
const deltaFromHtml = this._editor.clipboard.convert({ html: htmlValue });
|
|
831
810
|
|
|
832
811
|
// Restore whitespace characters after the conversion
|
|
833
812
|
Object.entries(whitespaceCharacters).forEach(([character, replacement]) => {
|
|
@@ -868,10 +847,7 @@ export const RichTextEditorMixin = (superClass) =>
|
|
|
868
847
|
timeOut.after(timeout),
|
|
869
848
|
() => {
|
|
870
849
|
const formatting = Array.from(this.shadowRoot.querySelectorAll('[part="toolbar"] .ql-active'))
|
|
871
|
-
.map((button) =>
|
|
872
|
-
const tooltip = this.shadowRoot.querySelector(`[for="${button.id}"]`);
|
|
873
|
-
return tooltip.text;
|
|
874
|
-
})
|
|
850
|
+
.map((button) => button.getAttribute('aria-label'))
|
|
875
851
|
.join(', ');
|
|
876
852
|
announcer.textContent = formatting;
|
|
877
853
|
},
|
|
@@ -8,51 +8,83 @@
|
|
|
8
8
|
* See https://vaadin.com/commercial-license-and-service-terms for the full
|
|
9
9
|
* license.
|
|
10
10
|
*/
|
|
11
|
-
import { css, html, LitElement } from 'lit';
|
|
11
|
+
import { css, html, LitElement, render } from 'lit';
|
|
12
12
|
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
13
13
|
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
|
|
14
14
|
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
|
|
15
15
|
import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
|
|
16
16
|
import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
|
|
17
|
-
import {
|
|
17
|
+
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
|
|
18
18
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
19
|
-
import {
|
|
19
|
+
import { richTextEditorPopupOverlayStyles } from './styles/vaadin-rich-text-editor-popup-overlay-base-styles.js';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* An element used internally by `<vaadin-rich-text-editor>`. Not intended to be used separately.
|
|
23
23
|
*
|
|
24
24
|
* @customElement
|
|
25
25
|
* @extends HTMLElement
|
|
26
|
-
* @mixes RichTextEditorPopupMixin
|
|
27
26
|
* @private
|
|
28
27
|
*/
|
|
29
|
-
class RichTextEditorPopup extends
|
|
28
|
+
class RichTextEditorPopup extends PolylitMixin(LitElement) {
|
|
30
29
|
static get is() {
|
|
31
30
|
return 'vaadin-rich-text-editor-popup';
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
static get styles() {
|
|
35
34
|
return css`
|
|
36
|
-
:host
|
|
37
|
-
|
|
35
|
+
:host([opened]),
|
|
36
|
+
:host([opening]),
|
|
37
|
+
:host([closing]) {
|
|
38
|
+
display: contents !important;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:host,
|
|
42
|
+
:host([hidden]) {
|
|
43
|
+
display: none !important;
|
|
38
44
|
}
|
|
39
45
|
`;
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
static get properties() {
|
|
49
|
+
return {
|
|
50
|
+
target: {
|
|
51
|
+
type: Object,
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
opened: {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
reflectToAttribute: true,
|
|
57
|
+
notify: true,
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
colors: {
|
|
61
|
+
type: Array,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static get observers() {
|
|
67
|
+
return ['__openedOrTargetChanged(opened, target)', '__colorsChanged(colors)'];
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
/** @protected */
|
|
43
71
|
render() {
|
|
44
72
|
return html`
|
|
45
73
|
<vaadin-rich-text-editor-popup-overlay
|
|
46
|
-
|
|
74
|
+
id="overlay"
|
|
75
|
+
.owner="${this}"
|
|
47
76
|
.opened="${this.opened}"
|
|
48
77
|
.positionTarget="${this.target}"
|
|
49
78
|
no-vertical-overlap
|
|
50
79
|
horizontal-align="start"
|
|
51
80
|
vertical-align="top"
|
|
52
81
|
focus-trap
|
|
82
|
+
exportparts="overlay, content"
|
|
53
83
|
@opened-changed="${this._onOpenedChanged}"
|
|
54
84
|
@vaadin-overlay-escape-press="${this._onOverlayEscapePress}"
|
|
55
|
-
|
|
85
|
+
>
|
|
86
|
+
<slot></slot>
|
|
87
|
+
</vaadin-rich-text-editor-popup-overlay>
|
|
56
88
|
`;
|
|
57
89
|
}
|
|
58
90
|
|
|
@@ -60,6 +92,39 @@ class RichTextEditorPopup extends RichTextEditorPopupMixin(PolylitMixin(LitEleme
|
|
|
60
92
|
_onOpenedChanged(event) {
|
|
61
93
|
this.opened = event.detail.value;
|
|
62
94
|
}
|
|
95
|
+
|
|
96
|
+
/** @private */
|
|
97
|
+
_onOverlayEscapePress() {
|
|
98
|
+
this.target.focus();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @private */
|
|
102
|
+
_onColorClick(e) {
|
|
103
|
+
const { color } = e.target.dataset;
|
|
104
|
+
this.dispatchEvent(new CustomEvent('color-selected', { detail: { color } }));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @private */
|
|
108
|
+
__colorsChanged(colors) {
|
|
109
|
+
render(
|
|
110
|
+
html`
|
|
111
|
+
${colors.map(
|
|
112
|
+
(color) => html`
|
|
113
|
+
<button data-color="${color}" style="background: ${color}" @click="${this._onColorClick}"></button>
|
|
114
|
+
`,
|
|
115
|
+
)}
|
|
116
|
+
`,
|
|
117
|
+
this,
|
|
118
|
+
{ host: this },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @private */
|
|
123
|
+
__openedOrTargetChanged(opened, target) {
|
|
124
|
+
if (target) {
|
|
125
|
+
target.setAttribute('aria-expanded', opened ? 'true' : 'false');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
63
128
|
}
|
|
64
129
|
|
|
65
130
|
defineCustomElement(RichTextEditorPopup);
|
|
@@ -78,25 +143,44 @@ export { RichTextEditorPopup };
|
|
|
78
143
|
* @private
|
|
79
144
|
*/
|
|
80
145
|
class RichTextEditorPopupOverlay extends PositionMixin(
|
|
81
|
-
OverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LitElement)))),
|
|
146
|
+
OverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))))),
|
|
82
147
|
) {
|
|
83
148
|
static get is() {
|
|
84
149
|
return 'vaadin-rich-text-editor-popup-overlay';
|
|
85
150
|
}
|
|
86
151
|
|
|
87
152
|
static get styles() {
|
|
88
|
-
return
|
|
153
|
+
return richTextEditorPopupOverlayStyles;
|
|
89
154
|
}
|
|
90
155
|
|
|
91
156
|
/** @protected */
|
|
92
157
|
render() {
|
|
93
158
|
return html`
|
|
94
|
-
<div id="backdrop" part="backdrop" hidden></div>
|
|
95
159
|
<div part="overlay" id="overlay">
|
|
96
|
-
<div part="content" id="content"
|
|
160
|
+
<div part="content" id="content">
|
|
161
|
+
<slot></slot>
|
|
162
|
+
</div>
|
|
97
163
|
</div>
|
|
98
164
|
`;
|
|
99
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Override method from OverlayFocusMixin to use owner as content root
|
|
169
|
+
* @protected
|
|
170
|
+
* @override
|
|
171
|
+
*/
|
|
172
|
+
get _contentRoot() {
|
|
173
|
+
return this.owner;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Override method from OverlayFocusMixin to use owner as focus trap root
|
|
178
|
+
* @protected
|
|
179
|
+
* @override
|
|
180
|
+
*/
|
|
181
|
+
get _focusTrapRoot() {
|
|
182
|
+
return this.owner;
|
|
183
|
+
}
|
|
100
184
|
}
|
|
101
185
|
|
|
102
186
|
defineCustomElement(RichTextEditorPopupOverlay);
|
|
@@ -29,23 +29,22 @@ export interface RichTextEditorEventMap extends HTMLElementEventMap, RichTextEdi
|
|
|
29
29
|
* It provides a set of toolbar controls to apply formatting on the content,
|
|
30
30
|
* which is stored and can be accessed as HTML5 or JSON string.
|
|
31
31
|
*
|
|
32
|
-
* ```
|
|
32
|
+
* ```html
|
|
33
33
|
* <vaadin-rich-text-editor></vaadin-rich-text-editor>
|
|
34
34
|
* ```
|
|
35
35
|
*
|
|
36
36
|
* Vaadin Rich Text Editor focuses on the structure, not the styling of content.
|
|
37
|
-
* Therefore, the semantic HTML5 tags such as
|
|
37
|
+
* Therefore, the semantic HTML5 tags such as `<h1>`, `<strong>` and `<ul>` are used,
|
|
38
38
|
* and CSS usage is limited to most common cases, like horizontal text alignment.
|
|
39
39
|
*
|
|
40
40
|
* ### Styling
|
|
41
41
|
*
|
|
42
42
|
* The following state attributes are available for styling:
|
|
43
43
|
*
|
|
44
|
-
* Attribute | Description
|
|
45
|
-
*
|
|
46
|
-
* `disabled` | Set to a disabled text editor
|
|
47
|
-
* `readonly` | Set to a readonly text editor
|
|
48
|
-
* `on` | Set to a toolbar button applied to the selected text | toolbar-button
|
|
44
|
+
* Attribute | Description
|
|
45
|
+
* -------------|------------------------------
|
|
46
|
+
* `disabled` | Set to a disabled text editor
|
|
47
|
+
* `readonly` | Set to a readonly text editor
|
|
49
48
|
*
|
|
50
49
|
* The following shadow DOM parts are available for styling:
|
|
51
50
|
*
|
|
@@ -54,12 +53,13 @@ export interface RichTextEditorEventMap extends HTMLElementEventMap, RichTextEdi
|
|
|
54
53
|
* `content` | The content wrapper
|
|
55
54
|
* `toolbar` | The toolbar wrapper
|
|
56
55
|
* `toolbar-group` | The group for toolbar controls
|
|
57
|
-
* `toolbar-group-history` | The group for
|
|
56
|
+
* `toolbar-group-history` | The group for history controls
|
|
58
57
|
* `toolbar-group-emphasis` | The group for emphasis controls
|
|
59
58
|
* `toolbar-group-heading` | The group for heading controls
|
|
60
59
|
* `toolbar-group-style` | The group for style controls
|
|
61
60
|
* `toolbar-group-glyph-transformation` | The group for glyph transformation controls
|
|
62
|
-
* `toolbar-group-
|
|
61
|
+
* `toolbar-group-list` | The group for list controls
|
|
62
|
+
* `toolbar-group-indent` | The group for indentation controls
|
|
63
63
|
* `toolbar-group-alignment` | The group for alignment controls
|
|
64
64
|
* `toolbar-group-rich-text` | The group for rich text controls
|
|
65
65
|
* `toolbar-group-block` | The group for preformatted block controls
|
|
@@ -81,6 +81,8 @@ export interface RichTextEditorEventMap extends HTMLElementEventMap, RichTextEdi
|
|
|
81
81
|
* `toolbar-button-superscript` | The "superscript" button
|
|
82
82
|
* `toolbar-button-list-ordered` | The "ordered list" button
|
|
83
83
|
* `toolbar-button-list-bullet` | The "bullet list" button
|
|
84
|
+
* `toolbar-button-outdent` | The "decrease indentation" button
|
|
85
|
+
* `toolbar-button-indent` | The "increase indentation" button
|
|
84
86
|
* `toolbar-button-align-left` | The "left align" button
|
|
85
87
|
* `toolbar-button-align-center` | The "center align" button
|
|
86
88
|
* `toolbar-button-align-right` | The "right align" button
|