From 62769a3fc9c36220c8ff253056e5cb79be4717a5 Mon Sep 17 00:00:00 2001 From: web-padawan Date: Tue, 2 Oct 2018 17:02:15 +0300 Subject: [PATCH 01/12] Shadow DOM support and tests --- core/emitter.js | 29 +++++++++++++++++++------ core/selection.js | 13 +++++++----- modules/toolbar.js | 4 +++- test/unit/core/selection.js | 41 ++++++++++++++++++++++++++++++++++++ test/unit/modules/toolbar.js | 32 ++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 12 deletions(-) diff --git a/core/emitter.js b/core/emitter.js index 0f9ba4f06a..b54a76b6ad 100644 --- a/core/emitter.js +++ b/core/emitter.js @@ -4,14 +4,13 @@ import logger from './logger'; const debug = logger('quill:events'); const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click']; +const EMITTERS = []; +const supportsRootNode = ('getRootNode' in document); EVENTS.forEach(eventName => { document.addEventListener(eventName, (...args) => { - Array.from(document.querySelectorAll('.ql-container')).forEach(node => { - const quill = instances.get(node); - if (quill && quill.emitter) { - quill.emitter.handleDOM(...args); - } + EMITTERS.forEach((em) => { + em.handleDOM(...args); }); }); }); @@ -20,6 +19,7 @@ class Emitter extends EventEmitter { constructor() { super(); this.listeners = {}; + EMITTERS.push(this); this.on('error', debug.error); } @@ -29,8 +29,25 @@ class Emitter extends EventEmitter { } handleDOM(event, ...args) { + const target = (event.composedPath ? event.composedPath()[0] : event.target); + const containsNode = (node, target) => { + if (!supportsRootNode || target.getRootNode() === document) { + return node.contains(target); + } + + while (!node.contains(target)) { + const root = target.getRootNode(); + if (!root || !root.host) { + return false; + } + target = root.host; + } + + return true; + }; + (this.listeners[event.type] || []).forEach(({ node, handler }) => { - if (event.target === node || node.contains(event.target)) { + if (target === node || containsNode(node, target)) { handler(event, ...args); } }); diff --git a/core/selection.js b/core/selection.js index b099fad6e4..ed30e51833 100644 --- a/core/selection.js +++ b/core/selection.js @@ -20,6 +20,7 @@ class Selection { this.composing = false; this.mouseDown = false; this.root = this.scroll.domNode; + this.rootDocument = (this.root.getRootNode ? this.root.getRootNode() : document); this.cursor = this.scroll.create('cursor', this); // savedRange is last non-null range this.savedRange = new Range(0, 0); @@ -27,7 +28,7 @@ class Selection { this.lastNative = null; this.handleComposition(); this.handleDragging(); - this.emitter.listenDOM('selectionchange', document, () => { + this.emitter.listenDOM('selectionchange', this.rootDocument, () => { if (!this.mouseDown && !this.composing) { setTimeout(this.update.bind(this, Emitter.sources.USER), 1); } @@ -184,7 +185,7 @@ class Selection { } getNativeRange() { - const selection = document.getSelection(); + const selection = this.rootDocument.getSelection(); if (selection == null || selection.rangeCount <= 0) return null; const nativeRange = selection.getRangeAt(0); if (nativeRange == null) return null; @@ -202,8 +203,8 @@ class Selection { hasFocus() { return ( - document.activeElement === this.root || - contains(this.root, document.activeElement) + this.rootDocument.activeElement === this.root || + contains(this.root, this.rootDocument.activeElement) ); } @@ -325,7 +326,7 @@ class Selection { ) { return; } - const selection = document.getSelection(); + const selection = this.rootDocument.getSelection(); if (selection == null) return; if (startNode != null) { if (!this.hasFocus()) this.root.focus(); @@ -416,6 +417,8 @@ class Selection { } } +// TODO ShadowDom consider? https://github.com/ing-bank/lion/blob/master/packages/overlays/src/utils/deep-contains.js +// TODO note that handleDOM has a contains impl as well.. function contains(parent, descendant) { try { // Firefox inserts inaccessible nodes around video elements diff --git a/modules/toolbar.js b/modules/toolbar.js index a16923ec0a..0dfad947d4 100644 --- a/modules/toolbar.js +++ b/modules/toolbar.js @@ -4,6 +4,7 @@ import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; +const supportsRootNode = ('getRootNode' in document); const debug = logger('quill:toolbar'); class Toolbar extends Module { @@ -15,7 +16,8 @@ class Toolbar extends Module { quill.container.parentNode.insertBefore(container, quill.container); this.container = container; } else if (typeof this.options.container === 'string') { - this.container = document.querySelector(this.options.container); + const rootDocument = (supportsRootNode ? quill.container.getRootNode() : document); + this.container = rootDocument.querySelector(this.options.container); } else { this.container = this.options.container; } diff --git a/test/unit/core/selection.js b/test/unit/core/selection.js index 164f5f8bed..0f16ccc6c0 100644 --- a/test/unit/core/selection.js +++ b/test/unit/core/selection.js @@ -41,6 +41,47 @@ describe('Selection', function() { }); }); + describe('shadow root', function() { + // Some browsers don't support shadow DOM + if (!document.head.attachShadow) { + return; + } + + let container; + let root; + + beforeEach(function() { + root = document.createElement('div'); + root.attachShadow({ mode: 'open' }); + root.shadowRoot.innerHTML = '
'; + + document.body.appendChild(root); + + container = root.shadowRoot.firstChild; + }); + + afterEach(function() { + document.body.removeChild(root); + }); + + it('getRange()', function() { + let selection = this.initialize(Selection, '

0123

', container); + selection.setNativeRange(container.firstChild.firstChild, 1); + let [range, ] = selection.getRange(); + expect(range.index).toEqual(1); + expect(range.length).toEqual(0); + }); + + it('setRange()', function() { + let selection = this.initialize(Selection, '', container); + let expected = new Range(0); + selection.setRange(expected); + let [range, ] = selection.getRange(); + expect(range).toEqual(expected); + expect(selection.hasFocus()).toBe(true); + }); + }); + describe('getRange()', function() { it('empty document', function() { const selection = this.initialize(Selection, ''); diff --git a/test/unit/modules/toolbar.js b/test/unit/modules/toolbar.js index 7e04fc911d..80c095dbf6 100644 --- a/test/unit/modules/toolbar.js +++ b/test/unit/modules/toolbar.js @@ -106,6 +106,38 @@ describe('Toolbar', function() { }); }); + describe('shadow dom', function() { + // Some browsers don't support shadow DOM + if (!document.head.attachShadow) { + return; + } + + let container; + let editor; + + beforeEach(function() { + container = document.createElement('div'); + container.attachShadow({ mode: 'open' }); + container.shadowRoot.innerHTML = ` +
+
`; + + editor = new Quill(container.shadowRoot.querySelector('.editor'), { + modules: { + toolbar: '.toolbar' + } + }); + }); + + it('should initialise', function() { + const editorDiv = container.shadowRoot.querySelector('.editor'); + const toolbarDiv = container.shadowRoot.querySelector('.toolbar'); + expect(editorDiv.className).toBe('editor ql-container'); + expect(toolbarDiv.className).toBe('toolbar ql-toolbar'); + expect(editor.container).toBe(editorDiv); + }); + }); + describe('active', function() { beforeEach(function() { const container = this.initialize( From 17300c8be88265f401447b0933339f3692786b0d Mon Sep 17 00:00:00 2001 From: web-padawan Date: Wed, 3 Oct 2018 14:52:07 +0300 Subject: [PATCH 02/12] Add getSelection() polyfill for Safari shadow DOM --- core/emitter.js | 4 +- core/selection.js | 9 +- core/shadow-selection-polyfill.js | 345 ++++++++++++++++++++++++++++++ 3 files changed, 352 insertions(+), 6 deletions(-) create mode 100644 core/shadow-selection-polyfill.js diff --git a/core/emitter.js b/core/emitter.js index b54a76b6ad..715939e76f 100644 --- a/core/emitter.js +++ b/core/emitter.js @@ -1,9 +1,11 @@ import EventEmitter from 'eventemitter3'; import instances from './instances'; import logger from './logger'; +import { SHADOW_SELECTIONCHANGE } from './shadow-selection-polyfill'; const debug = logger('quill:events'); -const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click']; + +const EVENTS = [SHADOW_SELECTIONCHANGE, 'mousedown', 'mouseup', 'click']; const EMITTERS = []; const supportsRootNode = ('getRootNode' in document); diff --git a/core/selection.js b/core/selection.js index ed30e51833..943ddc01ca 100644 --- a/core/selection.js +++ b/core/selection.js @@ -3,6 +3,7 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import Emitter from './emitter'; import logger from './logger'; +import { SHADOW_SELECTIONCHANGE, getRange } from './shadow-selection-polyfill'; const debug = logger('quill:selection'); @@ -28,7 +29,7 @@ class Selection { this.lastNative = null; this.handleComposition(); this.handleDragging(); - this.emitter.listenDOM('selectionchange', this.rootDocument, () => { + this.emitter.listenDOM(SHADOW_SELECTIONCHANGE, this.rootDocument, () => { if (!this.mouseDown && !this.composing) { setTimeout(this.update.bind(this, Emitter.sources.USER), 1); } @@ -185,9 +186,7 @@ class Selection { } getNativeRange() { - const selection = this.rootDocument.getSelection(); - if (selection == null || selection.rangeCount <= 0) return null; - const nativeRange = selection.getRangeAt(0); + const nativeRange = getRange(this.rootDocument); if (nativeRange == null) return null; const range = this.normalizeNative(nativeRange); debug.info('getNativeRange', range); @@ -326,7 +325,7 @@ class Selection { ) { return; } - const selection = this.rootDocument.getSelection(); + const selection = typeof this.rootDocument.getSelection === 'function' ? this.rootDocument.getSelection() : document.getSelection(); if (selection == null) return; if (startNode != null) { if (!this.hasFocus()) this.root.focus(); diff --git a/core/shadow-selection-polyfill.js b/core/shadow-selection-polyfill.js new file mode 100644 index 0000000000..d94a86df0a --- /dev/null +++ b/core/shadow-selection-polyfill.js @@ -0,0 +1,345 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +// NOTE: copied from https://github.com/GoogleChromeLabs/shadow-selection-polyfill + +export const SHADOW_SELECTIONCHANGE = '-shadow-selectionchange'; + +const hasShadow = 'attachShadow' in Element.prototype && 'getRootNode' in Element.prototype; +const hasSelection = !!(hasShadow && document.createElement('div').attachShadow({ mode: 'open' }).getSelection); +const hasShady = window.ShadyDOM && window.ShadyDOM.inUse; +const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) || + /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; +const useDocument = !hasShadow || hasShady || (!hasSelection && !isSafari); + +const validNodeTypes = [Node.ELEMENT_NODE, Node.TEXT_NODE, Node.DOCUMENT_FRAGMENT_NODE]; +function isValidNode(node) { + return validNodeTypes.includes(node.nodeType); +} + +function findNode(s, parentNode, isLeft) { + const nodes = parentNode.childNodes || parentNode.children; + if (!nodes) { + return parentNode; // found it, probably text + } + + for (let i = 0; i < nodes.length; ++i) { + const j = isLeft ? i : (nodes.length - 1 - i); + const childNode = nodes[j]; + if (!isValidNode(childNode)) { + continue; // eslint-disable-line no-continue + } + + if (s.containsNode(childNode, true)) { + if (s.containsNode(childNode, false)) { + return childNode; + } + return findNode(s, childNode, isLeft); + } + } + return parentNode; +} + +/** + * @param {function(!Event)} fn to add to selectionchange internals + */ +const addInternalListener = (() => { + if (hasSelection || useDocument) { + // getSelection exists or document API can be used + document.addEventListener('selectionchange', () => { + document.dispatchEvent(new CustomEvent(SHADOW_SELECTIONCHANGE)); + }); + return () => {}; + } + + let withinInternals = false; + const handlers = []; + + document.addEventListener('selectionchange', (ev) => { + if (withinInternals) { + return; + } + document.dispatchEvent(new CustomEvent(SHADOW_SELECTIONCHANGE)); + withinInternals = true; + window.setTimeout(() => { + withinInternals = false; + }, 2); // FIXME: should be > 1 to prevent infinite Selection.update() loop + handlers.forEach((fn) => fn(ev)); + }); + + return (fn) => handlers.push(fn); +})(); + +let wasCaret = false; +let resolveTask = null; +addInternalListener(() => { + const s = window.getSelection(); + if (s.type === 'Caret') { + wasCaret = true; + } else if (wasCaret && !resolveTask) { + resolveTask = Promise.resolve(true).then(() => { + wasCaret = false; + resolveTask = null; + }); + } +}); + + +/** + * @param {!Selection} s the window selection to use + * @param {!Node} node the node to walk from + * @param {boolean} walkForward should this walk in natural direction + * @return {boolean} whether the selection contains the following node (even partially) + */ +function containsNextElement(s, node, walkForward) { + const start = node; + while (node = walkFromNode(node, walkForward)) { // eslint-disable-line no-cond-assign + // walking (left) can contain our own parent, which we don't want + if (!node.contains(start)) { + break; + } + } + if (!node) { + return false; + } + // we look for Element as .containsNode says true for _every_ text node, and we only care about + // elements themselves + return node instanceof Element && s.containsNode(node, true); +} + + +/** + * @param {!Selection} s the window selection to use + * @param {!Node} leftNode the left node + * @param {!Node} rightNode the right node + * @return {boolean|undefined} whether this has natural direction + */ +function getSelectionDirection(s, leftNode, rightNode) { + if (s.type !== 'Range') { + return undefined; // no direction + } + const measure = () => s.toString().length; + + const initialSize = measure(); + + if (initialSize === 1 && wasCaret && leftNode === rightNode) { + // nb. We need to reset a single selection as Safari _always_ tells us the cursor was dragged + // left to right (maybe RTL on those devices). + // To be fair, Chrome has the same bug. + s.extend(leftNode, 0); + s.collapseToEnd(); + return undefined; + } + + let updatedSize; + + // Try extending forward and seeing what happens. + s.modify('extend', 'forward', 'character'); + updatedSize = measure(); + + if (updatedSize > initialSize || containsNextElement(s, rightNode, true)) { + s.modify('extend', 'backward', 'character'); + return true; + } else if (updatedSize < initialSize || !s.containsNode(leftNode)) { + s.modify('extend', 'backward', 'character'); + return false; + } + + // Maybe we were at the end of something. Extend backwards. + // TODO(samthor): We seem to be able to get away without the 'backwards' case. + s.modify('extend', 'backward', 'character'); + updatedSize = measure(); + + if (updatedSize > initialSize || containsNextElement(s, leftNode, false)) { + s.modify('extend', 'forward', 'character'); + return false; + } else if (updatedSize < initialSize || !s.containsNode(rightNode)) { + s.modify('extend', 'forward', 'character'); + return true; + } + + // This is likely a select-all. + return undefined; +} + +/** + * Returns the next valid node (element or text). This is needed as Safari doesn't support + * TreeWalker inside Shadow DOM. Don't escape shadow roots. + * + * @param {!Node} node to start from + * @param {boolean} walkForward should this walk in natural direction + * @return {Node} node found, if any + */ +function walkFromNode(node, walkForward) { + if (!walkForward) { + return node.previousSibling || node.parentNode || null; + } + while (node) { + if (node.nextSibling) { + return node.nextSibling; + } + node = node.parentNode; + } + return null; +} + +/** + * @param {!Node} node to check for initial space + * @return {number} count of initial space + */ +function initialSpace(node) { + if (node.nodeType !== Node.TEXT_NODE) { + return 0; + } + return /^\s*/.exec(node.textContent)[0].length; +} + +/** + * @param {!Node} node to check for trailing space + * @return {number} count of ignored trailing space + */ +function ignoredTrailingSpace(node) { + if (node.nodeType !== Node.TEXT_NODE) { + return 0; + } + const trailingSpaceCount = /\s*$/.exec(node.textContent)[0].length; + if (!trailingSpaceCount) { + return 0; + } + return trailingSpaceCount - 1; // always allow single last +} + +const cachedRange = new Map(); +export function getRange(root) { + if (hasSelection || useDocument) { + const s = (useDocument ? document : root).getSelection(); + return s.rangeCount ? s.getRangeAt(0) : null; + } + + const thisFrame = cachedRange.get(root); + if (thisFrame) { + return thisFrame; + } + + const result = internalGetShadowSelection(root); + + cachedRange.set(root, result.range); + window.setTimeout(() => { + cachedRange.delete(root); + }, 0); + return result.range; +} + +const fakeSelectionNode = document.createTextNode(''); +export function internalGetShadowSelection(root) { + const range = document.createRange(); + + const s = window.getSelection(); + if (!s.containsNode(root.host, true)) { + return {range: null, mode: 'none'}; + } + + // TODO: inserting fake nodes isn't ideal, but containsNode doesn't work on nearby adjacent + // text nodes (in fact it returns true for all text nodes on the page?!). + + // insert a fake 'before' node to see if it's selected + root.insertBefore(fakeSelectionNode, root.childNodes[0]); + const includesBeforeRoot = s.containsNode(fakeSelectionNode); + fakeSelectionNode.remove(); + if (includesBeforeRoot) { + return {range: null, mode: 'outside-before'}; + } + + // insert a fake 'after' node to see if it's selected + root.appendChild(fakeSelectionNode); + const includesAfterRoot = s.containsNode(fakeSelectionNode); + fakeSelectionNode.remove(); + if (includesAfterRoot) { + return {range: null, mode: 'outside-after'}; + } + + const measure = () => s.toString().length; + if (!(s.type === 'Caret' || s.type === 'Range')) { + throw new TypeError('unexpected type: ' + s.type); + } + + const leftNode = findNode(s, root, true); + let rightNode; + let isNaturalDirection; + if (s.type === 'Range') { + rightNode = findNode(s, root, false); // get right node here _before_ getSelectionDirection + isNaturalDirection = getSelectionDirection(s, leftNode, rightNode); + // isNaturalDirection means "going right" + } + + if (s.type === 'Caret') { + // we might transition to being a caret, so don't check initial value + s.extend(leftNode, 0); + const at = measure(); + s.collapseToEnd(); + + range.setStart(leftNode, at); + range.setEnd(leftNode, at); + return {range, mode: 'caret'}; + } else if (isNaturalDirection === undefined) { + if (s.type !== 'Range') { + throw new TypeError('unexpected type: ' + s.type); + } + // This occurs when we can't move because we can't extend left or right to measure the + // direction we're moving in. Good news though: we don't need to _change_ the selection + // to measure it, so just return immediately. + range.setStart(leftNode, 0); + range.setEnd(rightNode, rightNode.length); + return {range, mode: 'all'}; + } + + const size = measure(); + let offsetLeft, offsetRight; + + // only one newline/space char is cared about + const validRightLength = rightNode.length - ignoredTrailingSpace(rightNode); + + if (isNaturalDirection) { + // walk in the opposite direction first + s.extend(leftNode, 0); + offsetLeft = measure() + initialSpace(leftNode); // measure doesn't include initial space + + // then in our actual direction + s.extend(rightNode, validRightLength); + offsetRight = validRightLength - (measure() - size); + + // then revert to the original position + s.extend(rightNode, offsetRight); + } else { + // walk in the opposite direction first + s.extend(rightNode, validRightLength); + offsetRight = validRightLength - measure(); + + // then in our actual direction + s.extend(leftNode, 0); + offsetLeft = measure() - size + initialSpace(leftNode); // doesn't include initial space + + // then revert to the original position + s.extend(leftNode, offsetLeft); + } + + range.setStart(leftNode, offsetLeft); + range.setEnd(rightNode, offsetRight); + return { + mode: isNaturalDirection ? 'right' : 'left', + range, + }; +} From acc6ab2fe59c445d15480263d28190395604b211 Mon Sep 17 00:00:00 2001 From: web-padawan Date: Wed, 10 Oct 2018 09:36:56 +0300 Subject: [PATCH 03/12] Add vaadin-quill: exclude themes and pickers --- _develop/webpack.config.js | 2 + vaadin-quill.js | 104 +++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 vaadin-quill.js diff --git a/_develop/webpack.config.js b/_develop/webpack.config.js index dbe7eb1201..4a34b7d588 100644 --- a/_develop/webpack.config.js +++ b/_develop/webpack.config.js @@ -19,6 +19,7 @@ const constantPack = new webpack.DefinePlugin({ const source = [ 'quill.js', 'core.js', + 'vaadin-quill.js', 'blots', 'core', 'formats', @@ -102,6 +103,7 @@ const baseConfig = { entry: { 'quill.js': ['./quill.js'], 'quill.core.js': ['./core.js'], + 'vaadin-quill.js': ['./vaadin-quill.js'], 'quill.core': './assets/core.styl', 'quill.bubble': './assets/bubble.styl', 'quill.snow': './assets/snow.styl', diff --git a/vaadin-quill.js b/vaadin-quill.js new file mode 100644 index 0000000000..7680748ceb --- /dev/null +++ b/vaadin-quill.js @@ -0,0 +1,104 @@ +import Quill from './core'; + +import { AlignClass, AlignStyle } from './formats/align'; +import { DirectionAttribute, DirectionClass, DirectionStyle } from './formats/direction'; +import { IndentClass as Indent } from './formats/indent'; + +import Blockquote from './formats/blockquote'; +import Header from './formats/header'; +import List, { ListItem } from './formats/list'; + +import { BackgroundClass, BackgroundStyle } from './formats/background'; +import { ColorClass, ColorStyle } from './formats/color'; +import { FontClass, FontStyle } from './formats/font'; +import { SizeClass, SizeStyle } from './formats/size'; + +import Bold from './formats/bold'; +import Italic from './formats/italic'; +import Link from './formats/link'; +import Script from './formats/script'; +import Strike from './formats/strike'; +import Underline from './formats/underline'; + +import Image from './formats/image'; +import Video from './formats/video'; + +import CodeBlock, { Code as InlineCode } from './formats/code'; + +import Formula from './modules/formula'; +import Syntax from './modules/syntax'; +import Toolbar from './modules/toolbar'; + +// import Icons from './ui/icons'; +// import Picker from './ui/picker'; +// import ColorPicker from './ui/color-picker'; +// import IconPicker from './ui/icon-picker'; +// import Tooltip from './ui/tooltip'; + +// import BubbleTheme from './themes/bubble'; +// import SnowTheme from './themes/snow'; + + +Quill.register({ + 'attributors/attribute/direction': DirectionAttribute, + + 'attributors/class/align': AlignClass, + 'attributors/class/background': BackgroundClass, + 'attributors/class/color': ColorClass, + 'attributors/class/direction': DirectionClass, + 'attributors/class/font': FontClass, + 'attributors/class/size': SizeClass, + + 'attributors/style/align': AlignStyle, + 'attributors/style/background': BackgroundStyle, + 'attributors/style/color': ColorStyle, + 'attributors/style/direction': DirectionStyle, + 'attributors/style/font': FontStyle, + 'attributors/style/size': SizeStyle +}, true); + + +Quill.register({ + 'formats/align': AlignClass, + 'formats/direction': DirectionClass, + 'formats/indent': Indent, + + 'formats/background': BackgroundStyle, + 'formats/color': ColorStyle, + 'formats/font': FontClass, + 'formats/size': SizeClass, + + 'formats/blockquote': Blockquote, + 'formats/code-block': CodeBlock, + 'formats/header': Header, + 'formats/list': List, + + 'formats/bold': Bold, + 'formats/code': InlineCode, + 'formats/italic': Italic, + 'formats/link': Link, + 'formats/script': Script, + 'formats/strike': Strike, + 'formats/underline': Underline, + + 'formats/image': Image, + 'formats/video': Video, + + 'formats/list/item': ListItem, + + 'modules/formula': Formula, + 'modules/syntax': Syntax, + 'modules/toolbar': Toolbar + + // 'themes/bubble': BubbleTheme, + // 'themes/snow': SnowTheme, + + // 'ui/icons': Icons, + // 'ui/picker': Picker, + // 'ui/icon-picker': IconPicker, + // 'ui/color-picker': ColorPicker, + // 'ui/tooltip': Tooltip +}, true); + + +export default Quill; From f46c982b292b76736be4dcfbabca3218884bd524 Mon Sep 17 00:00:00 2001 From: web-padawan Date: Thu, 11 Oct 2018 12:59:59 +0300 Subject: [PATCH 04/12] Add minify config for custom build, exclude syntax and formula --- _develop/webpack.config.js | 5 ++++- vaadin-quill.js | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/_develop/webpack.config.js b/_develop/webpack.config.js index 4a34b7d588..55bfed2035 100644 --- a/_develop/webpack.config.js +++ b/_develop/webpack.config.js @@ -155,7 +155,10 @@ module.exports = env => { return { ...prodConfig, mode: 'production', - entry: { 'quill.min.js': './quill.js' }, + entry: { + 'quill.min.js': './quill.js', + 'vaadin-quill.min.js': './vaadin-quill.js', + }, devtool: 'source-map', }; } diff --git a/vaadin-quill.js b/vaadin-quill.js index 7680748ceb..7d2c566cc0 100644 --- a/vaadin-quill.js +++ b/vaadin-quill.js @@ -25,8 +25,8 @@ import Video from './formats/video'; import CodeBlock, { Code as InlineCode } from './formats/code'; -import Formula from './modules/formula'; -import Syntax from './modules/syntax'; +// import Formula from './modules/formula'; +// import Syntax from './modules/syntax'; import Toolbar from './modules/toolbar'; // import Icons from './ui/icons'; @@ -86,8 +86,8 @@ Quill.register({ 'formats/list/item': ListItem, - 'modules/formula': Formula, - 'modules/syntax': Syntax, + // 'modules/formula': Formula, + // 'modules/syntax': Syntax, 'modules/toolbar': Toolbar // 'themes/bubble': BubbleTheme, From 1e1a366896ffda4c168a90efa7947803c2738e9b Mon Sep 17 00:00:00 2001 From: web-padawan Date: Tue, 16 Oct 2018 13:49:45 +0300 Subject: [PATCH 05/12] Remove quill selection polyfill caching --- core/shadow-selection-polyfill.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/shadow-selection-polyfill.js b/core/shadow-selection-polyfill.js index d94a86df0a..7fd1ebdd89 100644 --- a/core/shadow-selection-polyfill.js +++ b/core/shadow-selection-polyfill.js @@ -222,24 +222,14 @@ function ignoredTrailingSpace(node) { return trailingSpaceCount - 1; // always allow single last } -const cachedRange = new Map(); export function getRange(root) { if (hasSelection || useDocument) { const s = (useDocument ? document : root).getSelection(); return s.rangeCount ? s.getRangeAt(0) : null; } - const thisFrame = cachedRange.get(root); - if (thisFrame) { - return thisFrame; - } - const result = internalGetShadowSelection(root); - cachedRange.set(root, result.range); - window.setTimeout(() => { - cachedRange.delete(root); - }, 0); return result.range; } From 5047648bbab76847acfab93b764cb0f6f6743cf0 Mon Sep 17 00:00:00 2001 From: web-padawan Date: Wed, 31 Oct 2018 14:04:34 +0200 Subject: [PATCH 06/12] Restore selection caching, add option to reset it --- core/selection.js | 5 ++--- core/shadow-selection-polyfill.js | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/core/selection.js b/core/selection.js index 943ddc01ca..00be42d920 100644 --- a/core/selection.js +++ b/core/selection.js @@ -3,7 +3,7 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import Emitter from './emitter'; import logger from './logger'; -import { SHADOW_SELECTIONCHANGE, getRange } from './shadow-selection-polyfill'; +import { SHADOW_SELECTIONCHANGE, getRange, addRange } from './shadow-selection-polyfill'; const debug = logger('quill:selection'); @@ -353,8 +353,7 @@ class Selection { const range = document.createRange(); range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); - selection.removeAllRanges(); - selection.addRange(range); + addRange(this.rootDocument, selection, range); } } else { selection.removeAllRanges(); diff --git a/core/shadow-selection-polyfill.js b/core/shadow-selection-polyfill.js index 7fd1ebdd89..cffcf3302f 100644 --- a/core/shadow-selection-polyfill.js +++ b/core/shadow-selection-polyfill.js @@ -222,13 +222,33 @@ function ignoredTrailingSpace(node) { return trailingSpaceCount - 1; // always allow single last } +const cachedRange = new Map(); + +export function addRange(root, selection, range) { + if (cachedRange.has(root)) { + cachedRange.delete(root); + cachedRange.set(root, range); + } + selection.removeAllRanges(); + selection.addRange(range); +} + export function getRange(root) { if (hasSelection || useDocument) { const s = (useDocument ? document : root).getSelection(); return s.rangeCount ? s.getRangeAt(0) : null; } + const thisFrame = cachedRange.get(root); + if (thisFrame) { + return thisFrame; + } + const result = internalGetShadowSelection(root); + cachedRange.set(root, result.range); + window.setTimeout(() => { + cachedRange.delete(root); + }, 0); return result.range; } From c2d87655dbadcf04b673a90daf3fe45e67b449c6 Mon Sep 17 00:00:00 2001 From: web-padawan Date: Thu, 15 Nov 2018 16:17:09 +0200 Subject: [PATCH 07/12] Do not add global update listener if polyfill is used --- core/selection.js | 14 ++++++++------ core/shadow-selection-polyfill.js | 8 +++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/core/selection.js b/core/selection.js index 00be42d920..906f8e774f 100644 --- a/core/selection.js +++ b/core/selection.js @@ -3,7 +3,7 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import Emitter from './emitter'; import logger from './logger'; -import { SHADOW_SELECTIONCHANGE, getRange, addRange } from './shadow-selection-polyfill'; +import { SHADOW_SELECTIONCHANGE, getRange, addRange, usePolyfill } from './shadow-selection-polyfill'; const debug = logger('quill:selection'); @@ -29,11 +29,13 @@ class Selection { this.lastNative = null; this.handleComposition(); this.handleDragging(); - this.emitter.listenDOM(SHADOW_SELECTIONCHANGE, this.rootDocument, () => { - if (!this.mouseDown && !this.composing) { - setTimeout(this.update.bind(this, Emitter.sources.USER), 1); - } - }); + if (!usePolyfill) { + this.emitter.listenDOM(SHADOW_SELECTIONCHANGE, this.rootDocument, () => { + if (!this.mouseDown && !this.composing) { + setTimeout(this.update.bind(this, Emitter.sources.USER), 1); + } + }); + } this.emitter.on(Emitter.events.SCROLL_BEFORE_UPDATE, () => { if (!this.hasFocus()) return; const native = this.getNativeRange(); diff --git a/core/shadow-selection-polyfill.js b/core/shadow-selection-polyfill.js index cffcf3302f..0f39065bc8 100644 --- a/core/shadow-selection-polyfill.js +++ b/core/shadow-selection-polyfill.js @@ -25,6 +25,8 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) || /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const useDocument = !hasShadow || hasShady || (!hasSelection && !isSafari); +export const usePolyfill = !(hasSelection || useDocument); + const validNodeTypes = [Node.ELEMENT_NODE, Node.TEXT_NODE, Node.DOCUMENT_FRAGMENT_NODE]; function isValidNode(node) { return validNodeTypes.includes(node.nodeType); @@ -57,7 +59,7 @@ function findNode(s, parentNode, isLeft) { * @param {function(!Event)} fn to add to selectionchange internals */ const addInternalListener = (() => { - if (hasSelection || useDocument) { + if (!usePolyfill) { // getSelection exists or document API can be used document.addEventListener('selectionchange', () => { document.dispatchEvent(new CustomEvent(SHADOW_SELECTIONCHANGE)); @@ -76,7 +78,7 @@ const addInternalListener = (() => { withinInternals = true; window.setTimeout(() => { withinInternals = false; - }, 2); // FIXME: should be > 1 to prevent infinite Selection.update() loop + }, 0); handlers.forEach((fn) => fn(ev)); }); @@ -234,7 +236,7 @@ export function addRange(root, selection, range) { } export function getRange(root) { - if (hasSelection || useDocument) { + if (!usePolyfill) { const s = (useDocument ? document : root).getSelection(); return s.rangeCount ? s.getRangeAt(0) : null; } From 9ae45e082fac316b6af8e95ccf601bc6e09dccde Mon Sep 17 00:00:00 2001 From: web-padawan Date: Mon, 20 Jul 2020 11:21:16 +0300 Subject: [PATCH 08/12] Update shadow selection polyfill --- core/shadow-selection-polyfill.js | 323 +++++++++++++++++------------- 1 file changed, 184 insertions(+), 139 deletions(-) diff --git a/core/shadow-selection-polyfill.js b/core/shadow-selection-polyfill.js index 0f39065bc8..d554e41b51 100644 --- a/core/shadow-selection-polyfill.js +++ b/core/shadow-selection-polyfill.js @@ -27,11 +27,48 @@ const useDocument = !hasShadow || hasShady || (!hasSelection && !isSafari); export const usePolyfill = !(hasSelection || useDocument); +const invalidPartialElements = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|script|source|style|template|track|wbr)$/; + const validNodeTypes = [Node.ELEMENT_NODE, Node.TEXT_NODE, Node.DOCUMENT_FRAGMENT_NODE]; function isValidNode(node) { return validNodeTypes.includes(node.nodeType); } +export function findCaretFocus(s, node) { + const pending = []; + const pushAll = (nodeList) => { + for (let i = 0; i < nodeList.length; ++i) { + if (nodeList[i].shadowRoot) { + pending.push(nodeList[i].shadowRoot); + } + } + }; + + // We're told by Safari that a node containing a child with a Shadow Root is selected, but check + // the node directly too (just in case they change their mind later). + if (node.shadowRoot) { + pending.push(node.shadowRoot); + } + pushAll(node.childNodes); + + while (pending.length) { + const root = pending.shift(); + + for (let i = 0; i < root.childNodes.length; ++i) { + if (s.containsNode(root.childNodes[i], true)) { + return root; + } + } + + // The selection must be inside a further Shadow Root, but there's no good way to get a list of + // them. Safari won't tell you what regular node contains the root which has a selection. So, + // unfortunately if you stack them this will be slow(-ish). + pushAll(root.querySelectorAll('*')); + } + + return null; +} + function findNode(s, parentNode, isLeft) { const nodes = parentNode.childNodes || parentNode.children; if (!nodes) { @@ -49,16 +86,18 @@ function findNode(s, parentNode, isLeft) { if (s.containsNode(childNode, false)) { return childNode; } - return findNode(s, childNode, isLeft); + // Special-case elements that cannot have feasible children. + if (!invalidPartialElements.exec(childNode.localName || '')) { + return findNode(s, childNode, isLeft); + } } } return parentNode; } -/** - * @param {function(!Event)} fn to add to selectionchange internals - */ -const addInternalListener = (() => { +let recentCaretRange = {node: null, offset: -1}; + +(function() { if (!usePolyfill) { // getSelection exists or document API can be used document.addEventListener('selectionchange', () => { @@ -68,36 +107,33 @@ const addInternalListener = (() => { } let withinInternals = false; - const handlers = []; - document.addEventListener('selectionchange', (ev) => { + document.addEventListener('selectionchange', () => { if (withinInternals) { return; } - document.dispatchEvent(new CustomEvent(SHADOW_SELECTIONCHANGE)); + withinInternals = true; - window.setTimeout(() => { - withinInternals = false; - }, 0); - handlers.forEach((fn) => fn(ev)); - }); - return (fn) => handlers.push(fn); -})(); + const s = window.getSelection(); + if (s.type === 'Caret') { + const root = findCaretFocus(s, s.anchorNode); + if (root instanceof window.ShadowRoot) { + const range = getRange(root); + if (range) { + const node = range.startContainer; + const offset = range.startOffset; + recentCaretRange = {node, offset}; + } + } + } -let wasCaret = false; -let resolveTask = null; -addInternalListener(() => { - const s = window.getSelection(); - if (s.type === 'Caret') { - wasCaret = true; - } else if (wasCaret && !resolveTask) { - resolveTask = Promise.resolve(true).then(() => { - wasCaret = false; - resolveTask = null; + document.dispatchEvent(new CustomEvent('-shadow-selectionchange')); + window.requestAnimationFrame(() => { + withinInternals = false; }); - } -}); + }); +})(); /** @@ -137,15 +173,6 @@ function getSelectionDirection(s, leftNode, rightNode) { const initialSize = measure(); - if (initialSize === 1 && wasCaret && leftNode === rightNode) { - // nb. We need to reset a single selection as Safari _always_ tells us the cursor was dragged - // left to right (maybe RTL on those devices). - // To be fair, Chrome has the same bug. - s.extend(leftNode, 0); - s.collapseToEnd(); - return undefined; - } - let updatedSize; // Try extending forward and seeing what happens. @@ -160,8 +187,7 @@ function getSelectionDirection(s, leftNode, rightNode) { return false; } - // Maybe we were at the end of something. Extend backwards. - // TODO(samthor): We seem to be able to get away without the 'backwards' case. + // Maybe we were at the end of something. Extend backwards instead. s.modify('extend', 'backward', 'character'); updatedSize = measure(); @@ -198,31 +224,6 @@ function walkFromNode(node, walkForward) { return null; } -/** - * @param {!Node} node to check for initial space - * @return {number} count of initial space - */ -function initialSpace(node) { - if (node.nodeType !== Node.TEXT_NODE) { - return 0; - } - return /^\s*/.exec(node.textContent)[0].length; -} - -/** - * @param {!Node} node to check for trailing space - * @return {number} count of ignored trailing space - */ -function ignoredTrailingSpace(node) { - if (node.nodeType !== Node.TEXT_NODE) { - return 0; - } - const trailingSpaceCount = /\s*$/.exec(node.textContent)[0].length; - if (!trailingSpaceCount) { - return 0; - } - return trailingSpaceCount - 1; // always allow single last -} const cachedRange = new Map(); @@ -236,8 +237,19 @@ export function addRange(root, selection, range) { } export function getRange(root) { - if (!usePolyfill) { - const s = (useDocument ? document : root).getSelection(); + if (hasShady) { + const s = document.getSelection(); + return s.rangeCount ? s.getRangeAt(0) : null; + } else if (useDocument) { + // Document pierces Shadow Root for selection, so actively filter it down to the right node. + // This is only for Firefox, which does not allow selection across Shadow Root boundaries. + const s = document.getSelection(); + if (s.containsNode(root, true)) { + return s.getRangeAt(0); + } + return null; + } else if (hasSelection) { + const s = root.getSelection(); return s.rangeCount ? s.getRangeAt(0) : null; } @@ -255,103 +267,136 @@ export function getRange(root) { return result.range; } -const fakeSelectionNode = document.createTextNode(''); -export function internalGetShadowSelection(root) { - const range = document.createRange(); - +/* eslint-disable complexity */ +function internalGetShadowSelection(root) { + // nb. We used to check whether the selection contained the host, but this broke in Safari 13. + // This is "nicely formatted" whitespace as per the browser's renderer. This is fine, and we only + // provide selection information at this granularity. const s = window.getSelection(); - if (!s.containsNode(root.host, true)) { - return {range: null, mode: 'none'}; - } - - // TODO: inserting fake nodes isn't ideal, but containsNode doesn't work on nearby adjacent - // text nodes (in fact it returns true for all text nodes on the page?!). - // insert a fake 'before' node to see if it's selected - root.insertBefore(fakeSelectionNode, root.childNodes[0]); - const includesBeforeRoot = s.containsNode(fakeSelectionNode); - fakeSelectionNode.remove(); - if (includesBeforeRoot) { - return {range: null, mode: 'outside-before'}; + if (s.type === 'None') { + return {range: null, type: 'none'}; + } else if (!(s.type === 'Caret' || s.type === 'Range')) { + throw new TypeError('unexpected type: ' + s.type); } - // insert a fake 'after' node to see if it's selected - root.appendChild(fakeSelectionNode); - const includesAfterRoot = s.containsNode(fakeSelectionNode); - fakeSelectionNode.remove(); - if (includesAfterRoot) { - return {range: null, mode: 'outside-after'}; + const leftNode = findNode(s, root, true); + if (leftNode === root) { + return {range: null, mode: 'none'}; } - const measure = () => s.toString().length; - if (!(s.type === 'Caret' || s.type === 'Range')) { - throw new TypeError('unexpected type: ' + s.type); - } + const range = document.createRange(); - const leftNode = findNode(s, root, true); - let rightNode; + let rightNode = null; let isNaturalDirection; if (s.type === 'Range') { rightNode = findNode(s, root, false); // get right node here _before_ getSelectionDirection isNaturalDirection = getSelectionDirection(s, leftNode, rightNode); + // isNaturalDirection means "going right" + + if (isNaturalDirection === undefined) { + // This occurs when we can't move because we can't extend left or right to measure the + // direction we're moving in... because it's the entire range. Hooray! + range.setStart(leftNode, 0); + range.setEnd(rightNode, rightNode.length); + return {range, mode: 'all'}; + } } - if (s.type === 'Caret') { - // we might transition to being a caret, so don't check initial value - s.extend(leftNode, 0); - const at = measure(); - s.collapseToEnd(); - - range.setStart(leftNode, at); - range.setEnd(leftNode, at); - return {range, mode: 'caret'}; - } else if (isNaturalDirection === undefined) { - if (s.type !== 'Range') { - throw new TypeError('unexpected type: ' + s.type); + const initialSize = s.toString().length; + + // Dumbest possible approach: remove characters from left side until no more selection, + // re-add. + + // Try right side first, as we can trim characters until selection gets shorter. + + let leftOffset = 0; + let rightOffset = 0; + + if (rightNode === null) { + // This is a caret selection, do nothing. + } else if (rightNode.nodeType === Node.TEXT_NODE) { + const rightText = rightNode.textContent; + const existingNextSibling = rightNode.nextSibling; + + for (let i = rightText.length - 1; i >= 0; --i) { + rightNode.splitText(i); + const updatedSize = s.toString().length; + if (updatedSize !== initialSize) { + rightOffset = i + 1; + break; + } + } + + // We don't use .normalize() here, as the user might already have a weird node arrangement + // they need to maintain. + rightNode.insertData(rightNode.length, rightText.substr(rightNode.length)); + while (rightNode.nextSibling !== existingNextSibling) { + rightNode.nextSibling.remove(); } - // This occurs when we can't move because we can't extend left or right to measure the - // direction we're moving in. Good news though: we don't need to _change_ the selection - // to measure it, so just return immediately. - range.setStart(leftNode, 0); - range.setEnd(rightNode, rightNode.length); - return {range, mode: 'all'}; } - const size = measure(); - let offsetLeft, offsetRight; + if (leftNode.nodeType === Node.TEXT_NODE) { + if (leftNode !== rightNode) { + // If we're at the end of a text node, it's impossible to extend the selection, so add an + // extra character to select (that we delete later). + leftNode.appendData('?'); + s.collapseToStart(); + s.modify('extend', 'right', 'character'); + } - // only one newline/space char is cared about - const validRightLength = rightNode.length - ignoredTrailingSpace(rightNode); + const leftText = leftNode.textContent; + const existingNextSibling = leftNode.nextSibling; - if (isNaturalDirection) { - // walk in the opposite direction first - s.extend(leftNode, 0); - offsetLeft = measure() + initialSpace(leftNode); // measure doesn't include initial space + const start = (leftNode === rightNode ? rightOffset : leftText.length - 1); - // then in our actual direction - s.extend(rightNode, validRightLength); - offsetRight = validRightLength - (measure() - size); + for (let i = start; i >= 0; --i) { + leftNode.splitText(i); + if (s.toString() === '') { + leftOffset = i; + break; + } + } - // then revert to the original position - s.extend(rightNode, offsetRight); - } else { - // walk in the opposite direction first - s.extend(rightNode, validRightLength); - offsetRight = validRightLength - measure(); + // As above, we don't want to use .normalize(). + leftNode.insertData(leftNode.length, leftText.substr(leftNode.length)); + while (leftNode.nextSibling !== existingNextSibling) { + leftNode.nextSibling.remove(); + } - // then in our actual direction - s.extend(leftNode, 0); - offsetLeft = measure() - size + initialSpace(leftNode); // doesn't include initial space + if (leftNode !== rightNode) { + leftNode.deleteData(leftNode.length - 1, 1); + } + + if (rightNode === null) { + rightNode = leftNode; + rightOffset = leftOffset; + } - // then revert to the original position - s.extend(leftNode, offsetLeft); + } else if (rightNode === null) { + rightNode = leftNode; } - range.setStart(leftNode, offsetLeft); - range.setEnd(rightNode, offsetRight); - return { - mode: isNaturalDirection ? 'right' : 'left', - range, - }; + // Work around common browser bug. Single character selction is always seen as 'forward'. Check + // if it's actually supposed to be backward. + if (initialSize === 1 && recentCaretRange && recentCaretRange.node === leftNode) { + if (recentCaretRange.offset > leftOffset && isNaturalDirection) { + isNaturalDirection = false; + } + } + + if (isNaturalDirection === true) { + s.collapse(leftNode, leftOffset); + s.extend(rightNode, rightOffset); + } else if (isNaturalDirection === false) { + s.collapse(rightNode, rightOffset); + s.extend(leftNode, leftOffset); + } else { + s.setPosition(leftNode, leftOffset); + } + + range.setStart(leftNode, leftOffset); + range.setEnd(rightNode, rightOffset); + return {range, mode: 'normal'}; } From 22bb93f58e5be0e56eecc1c3ed20f3069897388e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Thu, 16 Sep 2021 14:48:13 +0200 Subject: [PATCH 09/12] fix: change shadow selection polyfill implementation (#2) Co-authored-by: diegocardoso --- core/emitter.js | 4 +- core/selection.js | 40 +-- core/shadow-selection-polyfill.js | 427 +++++------------------------- 3 files changed, 90 insertions(+), 381 deletions(-) diff --git a/core/emitter.js b/core/emitter.js index 715939e76f..b54a76b6ad 100644 --- a/core/emitter.js +++ b/core/emitter.js @@ -1,11 +1,9 @@ import EventEmitter from 'eventemitter3'; import instances from './instances'; import logger from './logger'; -import { SHADOW_SELECTIONCHANGE } from './shadow-selection-polyfill'; const debug = logger('quill:events'); - -const EVENTS = [SHADOW_SELECTIONCHANGE, 'mousedown', 'mouseup', 'click']; +const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click']; const EMITTERS = []; const supportsRootNode = ('getRootNode' in document); diff --git a/core/selection.js b/core/selection.js index 906f8e774f..db59d41f69 100644 --- a/core/selection.js +++ b/core/selection.js @@ -3,7 +3,7 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import Emitter from './emitter'; import logger from './logger'; -import { SHADOW_SELECTIONCHANGE, getRange, addRange, usePolyfill } from './shadow-selection-polyfill'; +import { ShadowSelection } from './shadow-selection-polyfill'; const debug = logger('quill:selection'); @@ -29,18 +29,25 @@ class Selection { this.lastNative = null; this.handleComposition(); this.handleDragging(); - if (!usePolyfill) { - this.emitter.listenDOM(SHADOW_SELECTIONCHANGE, this.rootDocument, () => { - if (!this.mouseDown && !this.composing) { - setTimeout(this.update.bind(this, Emitter.sources.USER), 1); - } - }); - } - this.emitter.on(Emitter.events.SCROLL_BEFORE_UPDATE, () => { + this.emitter.listenDOM('selectionchange', document, () => { + if (!this.mouseDown && !this.composing) { + setTimeout(this.update.bind(this, Emitter.sources.USER), 1); + } + }); + this.emitter.on(Emitter.events.SCROLL_BEFORE_UPDATE, (_, mutations) => { if (!this.hasFocus()) return; const native = this.getNativeRange(); if (native == null) return; - if (native.start.node === this.cursor.textNode) return; // cursor.restore() will handle + + // We might need to hack the offset on Safari, when we are dealing with the first character of a row. + // This likely happens because of a race condition between quill's update method being called before the + // selectionchange event being fired in the selection polyfill. + const hackOffset = (native.start.offset === 0 && + native.start.offset === native.end.offset && + this.rootDocument.getSelection() instanceof ShadowSelection && + mutations.some((a) => a.type === 'characterData' && a.oldValue === '')) ? 1 : 0; + if (native.start.node === this.cursor.textNode) return; // cursor.restore() will handle + // TODO unclear if this has negative side effects this.emitter.once(Emitter.events.SCROLL_UPDATE, () => { try { if ( @@ -49,9 +56,9 @@ class Selection { ) { this.setNativeRange( native.start.node, - native.start.offset, + native.start.offset + hackOffset, native.end.node, - native.end.offset, + native.end.offset + hackOffset ); } this.update(Emitter.sources.SILENT); @@ -188,7 +195,9 @@ class Selection { } getNativeRange() { - const nativeRange = getRange(this.rootDocument); + const selection = this.rootDocument.getSelection(); + if (selection == null || selection.rangeCount <= 0) return null; + const nativeRange = selection.getRangeAt(0); if (nativeRange == null) return null; const range = this.normalizeNative(nativeRange); debug.info('getNativeRange', range); @@ -327,7 +336,7 @@ class Selection { ) { return; } - const selection = typeof this.rootDocument.getSelection === 'function' ? this.rootDocument.getSelection() : document.getSelection(); + const selection = this.rootDocument.getSelection(); if (selection == null) return; if (startNode != null) { if (!this.hasFocus()) this.root.focus(); @@ -355,7 +364,8 @@ class Selection { const range = document.createRange(); range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); - addRange(this.rootDocument, selection, range); + selection.removeAllRanges(); + selection.addRange(range); } } else { selection.removeAllRanges(); diff --git a/core/shadow-selection-polyfill.js b/core/shadow-selection-polyfill.js index d554e41b51..260c70df38 100644 --- a/core/shadow-selection-polyfill.js +++ b/core/shadow-selection-polyfill.js @@ -1,402 +1,103 @@ -/** - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ +// see https://github.com/GoogleChromeLabs/shadow-selection-polyfill/issues/11 +const SUPPORTS_SHADOW_SELECTION = typeof window.ShadowRoot.prototype.getSelection === 'function'; +const SUPPORTS_BEFORE_INPUT = window.InputEvent && typeof window.InputEvent.prototype.getTargetRanges === 'function'; +const IS_FIREFOX = window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1; +const IS_MSIE = !!(window.navigator.userAgent.match(/Trident/) && !window.navigator.userAgent.match(/MSIE/)); +const IS_EDGE = window.navigator.userAgent.match(/Edge/); -// NOTE: copied from https://github.com/GoogleChromeLabs/shadow-selection-polyfill - -export const SHADOW_SELECTIONCHANGE = '-shadow-selectionchange'; - -const hasShadow = 'attachShadow' in Element.prototype && 'getRootNode' in Element.prototype; -const hasSelection = !!(hasShadow && document.createElement('div').attachShadow({ mode: 'open' }).getSelection); -const hasShady = window.ShadyDOM && window.ShadyDOM.inUse; -const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) || - /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; -const useDocument = !hasShadow || hasShady || (!hasSelection && !isSafari); - -export const usePolyfill = !(hasSelection || useDocument); - -const invalidPartialElements = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|script|source|style|template|track|wbr)$/; - -const validNodeTypes = [Node.ELEMENT_NODE, Node.TEXT_NODE, Node.DOCUMENT_FRAGMENT_NODE]; -function isValidNode(node) { - return validNodeTypes.includes(node.nodeType); -} - -export function findCaretFocus(s, node) { - const pending = []; - const pushAll = (nodeList) => { - for (let i = 0; i < nodeList.length; ++i) { - if (nodeList[i].shadowRoot) { - pending.push(nodeList[i].shadowRoot); - } - } - }; - - // We're told by Safari that a node containing a child with a Shadow Root is selected, but check - // the node directly too (just in case they change their mind later). - if (node.shadowRoot) { - pending.push(node.shadowRoot); +let processing = false; +export class ShadowSelection { + constructor() { + this._ranges = []; } - pushAll(node.childNodes); - - while (pending.length) { - const root = pending.shift(); - for (let i = 0; i < root.childNodes.length; ++i) { - if (s.containsNode(root.childNodes[i], true)) { - return root; - } - } - - // The selection must be inside a further Shadow Root, but there's no good way to get a list of - // them. Safari won't tell you what regular node contains the root which has a selection. So, - // unfortunately if you stack them this will be slow(-ish). - pushAll(root.querySelectorAll('*')); + get rangeCount() { + return this._ranges.length; } - return null; -} - -function findNode(s, parentNode, isLeft) { - const nodes = parentNode.childNodes || parentNode.children; - if (!nodes) { - return parentNode; // found it, probably text + getRangeAt(index) { + return this._ranges[index]; } - for (let i = 0; i < nodes.length; ++i) { - const j = isLeft ? i : (nodes.length - 1 - i); - const childNode = nodes[j]; - if (!isValidNode(childNode)) { - continue; // eslint-disable-line no-continue - } - - if (s.containsNode(childNode, true)) { - if (s.containsNode(childNode, false)) { - return childNode; - } - // Special-case elements that cannot have feasible children. - if (!invalidPartialElements.exec(childNode.localName || '')) { - return findNode(s, childNode, isLeft); - } + addRange(range) { + this._ranges.push(range); + if (!processing) { + let windowSel = window.getSelection(); + windowSel.removeAllRanges(); + windowSel.addRange(range); } } - return parentNode; -} - -let recentCaretRange = {node: null, offset: -1}; -(function() { - if (!usePolyfill) { - // getSelection exists or document API can be used - document.addEventListener('selectionchange', () => { - document.dispatchEvent(new CustomEvent(SHADOW_SELECTIONCHANGE)); - }); - return () => {}; + removeAllRanges() { + this._ranges = []; } - let withinInternals = false; - - document.addEventListener('selectionchange', () => { - if (withinInternals) { - return; - } - - withinInternals = true; - - const s = window.getSelection(); - if (s.type === 'Caret') { - const root = findCaretFocus(s, s.anchorNode); - if (root instanceof window.ShadowRoot) { - const range = getRange(root); - if (range) { - const node = range.startContainer; - const offset = range.startOffset; - recentCaretRange = {node, offset}; - } - } - } - - document.dispatchEvent(new CustomEvent('-shadow-selectionchange')); - window.requestAnimationFrame(() => { - withinInternals = false; - }); - }); -})(); - - -/** - * @param {!Selection} s the window selection to use - * @param {!Node} node the node to walk from - * @param {boolean} walkForward should this walk in natural direction - * @return {boolean} whether the selection contains the following node (even partially) - */ -function containsNextElement(s, node, walkForward) { - const start = node; - while (node = walkFromNode(node, walkForward)) { // eslint-disable-line no-cond-assign - // walking (left) can contain our own parent, which we don't want - if (!node.contains(start)) { - break; - } - } - if (!node) { - return false; - } - // we look for Element as .containsNode says true for _every_ text node, and we only care about - // elements themselves - return node instanceof Element && s.containsNode(node, true); + // todo: implement remaining `Selection` methods and properties. } +function getActiveElement() { + let active = document.activeElement; -/** - * @param {!Selection} s the window selection to use - * @param {!Node} leftNode the left node - * @param {!Node} rightNode the right node - * @return {boolean|undefined} whether this has natural direction - */ -function getSelectionDirection(s, leftNode, rightNode) { - if (s.type !== 'Range') { - return undefined; // no direction - } - const measure = () => s.toString().length; - - const initialSize = measure(); - - let updatedSize; - - // Try extending forward and seeing what happens. - s.modify('extend', 'forward', 'character'); - updatedSize = measure(); - - if (updatedSize > initialSize || containsNextElement(s, rightNode, true)) { - s.modify('extend', 'backward', 'character'); - return true; - } else if (updatedSize < initialSize || !s.containsNode(leftNode)) { - s.modify('extend', 'backward', 'character'); - return false; - } - - // Maybe we were at the end of something. Extend backwards instead. - s.modify('extend', 'backward', 'character'); - updatedSize = measure(); - - if (updatedSize > initialSize || containsNextElement(s, leftNode, false)) { - s.modify('extend', 'forward', 'character'); - return false; - } else if (updatedSize < initialSize || !s.containsNode(rightNode)) { - s.modify('extend', 'forward', 'character'); - return true; - } - - // This is likely a select-all. - return undefined; -} - -/** - * Returns the next valid node (element or text). This is needed as Safari doesn't support - * TreeWalker inside Shadow DOM. Don't escape shadow roots. - * - * @param {!Node} node to start from - * @param {boolean} walkForward should this walk in natural direction - * @return {Node} node found, if any - */ -function walkFromNode(node, walkForward) { - if (!walkForward) { - return node.previousSibling || node.parentNode || null; - } - while (node) { - if (node.nextSibling) { - return node.nextSibling; + /* eslint-disable no-constant-condition */ + while (true) { + if (active && active.shadowRoot && active.shadowRoot.activeElement) { + active = active.shadowRoot.activeElement; + } else { + break; } - node = node.parentNode; } - return null; -} - - -const cachedRange = new Map(); -export function addRange(root, selection, range) { - if (cachedRange.has(root)) { - cachedRange.delete(root); - cachedRange.set(root, range); - } - selection.removeAllRanges(); - selection.addRange(range); + return active; } -export function getRange(root) { - if (hasShady) { - const s = document.getSelection(); - return s.rangeCount ? s.getRangeAt(0) : null; - } else if (useDocument) { - // Document pierces Shadow Root for selection, so actively filter it down to the right node. - // This is only for Firefox, which does not allow selection across Shadow Root boundaries. - const s = document.getSelection(); - if (s.containsNode(root, true)) { - return s.getRangeAt(0); - } - return null; - } else if (hasSelection) { - const s = root.getSelection(); - return s.rangeCount ? s.getRangeAt(0) : null; +if ((IS_FIREFOX || IS_MSIE || IS_EDGE) && !SUPPORTS_SHADOW_SELECTION) { + window.ShadowRoot.prototype.getSelection = function() { + return document.getSelection(); } - - const thisFrame = cachedRange.get(root); - if (thisFrame) { - return thisFrame; - } - - const result = internalGetShadowSelection(root); - cachedRange.set(root, result.range); - window.setTimeout(() => { - cachedRange.delete(root); - }, 0); - - return result.range; } -/* eslint-disable complexity */ -function internalGetShadowSelection(root) { - // nb. We used to check whether the selection contained the host, but this broke in Safari 13. - // This is "nicely formatted" whitespace as per the browser's renderer. This is fine, and we only - // provide selection information at this granularity. - const s = window.getSelection(); - - if (s.type === 'None') { - return {range: null, type: 'none'}; - } else if (!(s.type === 'Caret' || s.type === 'Range')) { - throw new TypeError('unexpected type: ' + s.type); - } - - const leftNode = findNode(s, root, true); - if (leftNode === root) { - return {range: null, mode: 'none'}; - } - - const range = document.createRange(); - - let rightNode = null; - let isNaturalDirection; - if (s.type === 'Range') { - rightNode = findNode(s, root, false); // get right node here _before_ getSelectionDirection - isNaturalDirection = getSelectionDirection(s, leftNode, rightNode); +if (!IS_FIREFOX && !SUPPORTS_SHADOW_SELECTION && SUPPORTS_BEFORE_INPUT) { + let selection = new ShadowSelection(); - // isNaturalDirection means "going right" - - if (isNaturalDirection === undefined) { - // This occurs when we can't move because we can't extend left or right to measure the - // direction we're moving in... because it's the entire range. Hooray! - range.setStart(leftNode, 0); - range.setEnd(rightNode, rightNode.length); - return {range, mode: 'all'}; - } + window.ShadowRoot.prototype.getSelection = function() { + return selection; } - const initialSize = s.toString().length; - - // Dumbest possible approach: remove characters from left side until no more selection, - // re-add. + window.addEventListener('selectionchange', () => { + if (!processing) { + processing = true; - // Try right side first, as we can trim characters until selection gets shorter. + const active = getActiveElement(); - let leftOffset = 0; - let rightOffset = 0; - - if (rightNode === null) { - // This is a caret selection, do nothing. - } else if (rightNode.nodeType === Node.TEXT_NODE) { - const rightText = rightNode.textContent; - const existingNextSibling = rightNode.nextSibling; - - for (let i = rightText.length - 1; i >= 0; --i) { - rightNode.splitText(i); - const updatedSize = s.toString().length; - if (updatedSize !== initialSize) { - rightOffset = i + 1; - break; + if (active && (active.getAttribute('contenteditable') === 'true')) { + document.execCommand('indent'); + } else { + selection.removeAllRanges(); } - } - // We don't use .normalize() here, as the user might already have a weird node arrangement - // they need to maintain. - rightNode.insertData(rightNode.length, rightText.substr(rightNode.length)); - while (rightNode.nextSibling !== existingNextSibling) { - rightNode.nextSibling.remove(); + processing = false; } - } + }, true); - if (leftNode.nodeType === Node.TEXT_NODE) { - if (leftNode !== rightNode) { - // If we're at the end of a text node, it's impossible to extend the selection, so add an - // extra character to select (that we delete later). - leftNode.appendData('?'); - s.collapseToStart(); - s.modify('extend', 'right', 'character'); - } + window.addEventListener('beforeinput', (event) => { + if (processing) { + const ranges = event.getTargetRanges(); + const range = ranges[0]; - const leftText = leftNode.textContent; - const existingNextSibling = leftNode.nextSibling; + const newRange = new Range(); - const start = (leftNode === rightNode ? rightOffset : leftText.length - 1); + newRange.setStart(range.startContainer, range.startOffset); + newRange.setEnd(range.endContainer, range.endOffset); - for (let i = start; i >= 0; --i) { - leftNode.splitText(i); - if (s.toString() === '') { - leftOffset = i; - break; - } - } + selection.removeAllRanges(); + selection.addRange(newRange); - // As above, we don't want to use .normalize(). - leftNode.insertData(leftNode.length, leftText.substr(leftNode.length)); - while (leftNode.nextSibling !== existingNextSibling) { - leftNode.nextSibling.remove(); + event.preventDefault(); + event.stopImmediatePropagation(); } + }, true); - if (leftNode !== rightNode) { - leftNode.deleteData(leftNode.length - 1, 1); - } - - if (rightNode === null) { - rightNode = leftNode; - rightOffset = leftOffset; - } - - } else if (rightNode === null) { - rightNode = leftNode; - } - - // Work around common browser bug. Single character selction is always seen as 'forward'. Check - // if it's actually supposed to be backward. - if (initialSize === 1 && recentCaretRange && recentCaretRange.node === leftNode) { - if (recentCaretRange.offset > leftOffset && isNaturalDirection) { - isNaturalDirection = false; - } - } - - if (isNaturalDirection === true) { - s.collapse(leftNode, leftOffset); - s.extend(rightNode, rightOffset); - } else if (isNaturalDirection === false) { - s.collapse(rightNode, rightOffset); - s.extend(leftNode, leftOffset); - } else { - s.setPosition(leftNode, leftOffset); - } - - range.setStart(leftNode, leftOffset); - range.setEnd(rightNode, rightOffset); - return {range, mode: 'normal'}; + window.addEventListener('selectstart', () => { + selection.removeAllRanges(); + }, true); } From 1442e3e8d4f6211ad0a85e6ef36ffe1f870971f3 Mon Sep 17 00:00:00 2001 From: Rob Resendez Date: Thu, 6 Jan 2022 20:35:48 -0600 Subject: [PATCH 10/12] lint errors fixed, compiling works, 415/418 tests pass (TODO) --- core/emitter.js | 20 ++++++++++---------- core/selection.js | 19 ++++++++++++------- modules/clipboard.js | 3 ++- modules/toolbar.js | 6 ++++-- package.json | 1 + test/unit/core/selection.js | 10 +++++----- test/unit/modules/toolbar.js | 4 ++-- 7 files changed, 36 insertions(+), 27 deletions(-) diff --git a/core/emitter.js b/core/emitter.js index b54a76b6ad..419fa6d453 100644 --- a/core/emitter.js +++ b/core/emitter.js @@ -1,15 +1,15 @@ import EventEmitter from 'eventemitter3'; -import instances from './instances'; +// import instances from './instances'; import logger from './logger'; const debug = logger('quill:events'); const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click']; const EMITTERS = []; -const supportsRootNode = ('getRootNode' in document); +const supportsRootNode = 'getRootNode' in document; EVENTS.forEach(eventName => { document.addEventListener(eventName, (...args) => { - EMITTERS.forEach((em) => { + EMITTERS.forEach(em => { em.handleDOM(...args); }); }); @@ -29,18 +29,18 @@ class Emitter extends EventEmitter { } handleDOM(event, ...args) { - const target = (event.composedPath ? event.composedPath()[0] : event.target); - const containsNode = (node, target) => { - if (!supportsRootNode || target.getRootNode() === document) { - return node.contains(target); + const target = event.composedPath ? event.composedPath()[0] : event.target; + const containsNode = (node, targetNode) => { + if (!supportsRootNode || targetNode.getRootNode() === document) { + return node.contains(targetNode); } - while (!node.contains(target)) { - const root = target.getRootNode(); + while (!node.contains(targetNode)) { + const root = targetNode.getRootNode(); if (!root || !root.host) { return false; } - target = root.host; + targetNode = root.host; } return true; diff --git a/core/selection.js b/core/selection.js index db59d41f69..b724db96b0 100644 --- a/core/selection.js +++ b/core/selection.js @@ -21,7 +21,9 @@ class Selection { this.composing = false; this.mouseDown = false; this.root = this.scroll.domNode; - this.rootDocument = (this.root.getRootNode ? this.root.getRootNode() : document); + this.rootDocument = this.root.getRootNode + ? this.root.getRootNode() + : document; this.cursor = this.scroll.create('cursor', this); // savedRange is last non-null range this.savedRange = new Range(0, 0); @@ -42,11 +44,14 @@ class Selection { // We might need to hack the offset on Safari, when we are dealing with the first character of a row. // This likely happens because of a race condition between quill's update method being called before the // selectionchange event being fired in the selection polyfill. - const hackOffset = (native.start.offset === 0 && - native.start.offset === native.end.offset && - this.rootDocument.getSelection() instanceof ShadowSelection && - mutations.some((a) => a.type === 'characterData' && a.oldValue === '')) ? 1 : 0; - if (native.start.node === this.cursor.textNode) return; // cursor.restore() will handle + const hackOffset = + native.start.offset === 0 && + native.start.offset === native.end.offset && + this.rootDocument.getSelection() instanceof ShadowSelection && + mutations.some(a => a.type === 'characterData' && a.oldValue === '') + ? 1 + : 0; + if (native.start.node === this.cursor.textNode) return; // cursor.restore() will handle // TODO unclear if this has negative side effects this.emitter.once(Emitter.events.SCROLL_UPDATE, () => { try { @@ -58,7 +63,7 @@ class Selection { native.start.node, native.start.offset + hackOffset, native.end.node, - native.end.offset + hackOffset + native.end.offset + hackOffset, ); } this.update(Emitter.sources.SILENT); diff --git a/modules/clipboard.js b/modules/clipboard.js index 8eadd406a6..fc5a81d088 100644 --- a/modules/clipboard.js +++ b/modules/clipboard.js @@ -155,7 +155,8 @@ class Clipboard extends Module { if (!html && files.length > 0) { this.quill.uploader.upload(range, files); return; - } else if (html && files.length > 0) { + } + if (html && files.length > 0) { const doc = new DOMParser().parseFromString(html, 'text/html'); if ( doc.body.childElementCount === 1 && diff --git a/modules/toolbar.js b/modules/toolbar.js index 0dfad947d4..e4d753adb9 100644 --- a/modules/toolbar.js +++ b/modules/toolbar.js @@ -4,7 +4,7 @@ import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -const supportsRootNode = ('getRootNode' in document); +const supportsRootNode = 'getRootNode' in document; const debug = logger('quill:toolbar'); class Toolbar extends Module { @@ -16,7 +16,9 @@ class Toolbar extends Module { quill.container.parentNode.insertBefore(container, quill.container); this.container = container; } else if (typeof this.options.container === 'string') { - const rootDocument = (supportsRootNode ? quill.container.getRootNode() : document); + const rootDocument = supportsRootNode + ? quill.container.getRootNode() + : document; this.container = rootDocument.querySelector(this.options.container); } else { this.container = this.options.container; diff --git a/package.json b/package.json index 53492be397..1372397a99 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ } }, "eslintIgnore": [ + "core/shadow-selection-polyfill.js", "dist/", "docs/", "node_modules/" diff --git a/test/unit/core/selection.js b/test/unit/core/selection.js index 0f16ccc6c0..a29eeacf31 100644 --- a/test/unit/core/selection.js +++ b/test/unit/core/selection.js @@ -65,18 +65,18 @@ describe('Selection', function() { }); it('getRange()', function() { - let selection = this.initialize(Selection, '

0123

', container); + const selection = this.initialize(Selection, '

0123

', container); selection.setNativeRange(container.firstChild.firstChild, 1); - let [range, ] = selection.getRange(); + const [range] = selection.getRange(); expect(range.index).toEqual(1); expect(range.length).toEqual(0); }); it('setRange()', function() { - let selection = this.initialize(Selection, '', container); - let expected = new Range(0); + const selection = this.initialize(Selection, '', container); + const expected = new Range(0); selection.setRange(expected); - let [range, ] = selection.getRange(); + const [range] = selection.getRange(); expect(range).toEqual(expected); expect(selection.hasFocus()).toBe(true); }); diff --git a/test/unit/modules/toolbar.js b/test/unit/modules/toolbar.js index 80c095dbf6..e5d960886e 100644 --- a/test/unit/modules/toolbar.js +++ b/test/unit/modules/toolbar.js @@ -124,8 +124,8 @@ describe('Toolbar', function() { editor = new Quill(container.shadowRoot.querySelector('.editor'), { modules: { - toolbar: '.toolbar' - } + toolbar: '.toolbar', + }, }); }); From ab35b457e931ecf52c62529941cf27a719c17f54 Mon Sep 17 00:00:00 2001 From: Rob Resendez Date: Thu, 6 Jan 2022 20:54:51 -0600 Subject: [PATCH 11/12] Revert "Add minify config for custom build, exclude syntax and formula" This reverts commit f46c982b292b76736be4dcfbabca3218884bd524. --- _develop/webpack.config.js | 5 +---- vaadin-quill.js | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/_develop/webpack.config.js b/_develop/webpack.config.js index 55bfed2035..4a34b7d588 100644 --- a/_develop/webpack.config.js +++ b/_develop/webpack.config.js @@ -155,10 +155,7 @@ module.exports = env => { return { ...prodConfig, mode: 'production', - entry: { - 'quill.min.js': './quill.js', - 'vaadin-quill.min.js': './vaadin-quill.js', - }, + entry: { 'quill.min.js': './quill.js' }, devtool: 'source-map', }; } diff --git a/vaadin-quill.js b/vaadin-quill.js index 7d2c566cc0..7680748ceb 100644 --- a/vaadin-quill.js +++ b/vaadin-quill.js @@ -25,8 +25,8 @@ import Video from './formats/video'; import CodeBlock, { Code as InlineCode } from './formats/code'; -// import Formula from './modules/formula'; -// import Syntax from './modules/syntax'; +import Formula from './modules/formula'; +import Syntax from './modules/syntax'; import Toolbar from './modules/toolbar'; // import Icons from './ui/icons'; @@ -86,8 +86,8 @@ Quill.register({ 'formats/list/item': ListItem, - // 'modules/formula': Formula, - // 'modules/syntax': Syntax, + 'modules/formula': Formula, + 'modules/syntax': Syntax, 'modules/toolbar': Toolbar // 'themes/bubble': BubbleTheme, From 25335dcd7edb4f1cf0e0750d869f17d96d69d9bc Mon Sep 17 00:00:00 2001 From: Rob Resendez Date: Thu, 6 Jan 2022 20:55:41 -0600 Subject: [PATCH 12/12] Revert "Add vaadin-quill: exclude themes and pickers" This reverts commit acc6ab2fe59c445d15480263d28190395604b211. --- _develop/webpack.config.js | 2 - vaadin-quill.js | 104 ------------------------------------- 2 files changed, 106 deletions(-) delete mode 100644 vaadin-quill.js diff --git a/_develop/webpack.config.js b/_develop/webpack.config.js index 4a34b7d588..dbe7eb1201 100644 --- a/_develop/webpack.config.js +++ b/_develop/webpack.config.js @@ -19,7 +19,6 @@ const constantPack = new webpack.DefinePlugin({ const source = [ 'quill.js', 'core.js', - 'vaadin-quill.js', 'blots', 'core', 'formats', @@ -103,7 +102,6 @@ const baseConfig = { entry: { 'quill.js': ['./quill.js'], 'quill.core.js': ['./core.js'], - 'vaadin-quill.js': ['./vaadin-quill.js'], 'quill.core': './assets/core.styl', 'quill.bubble': './assets/bubble.styl', 'quill.snow': './assets/snow.styl', diff --git a/vaadin-quill.js b/vaadin-quill.js deleted file mode 100644 index 7680748ceb..0000000000 --- a/vaadin-quill.js +++ /dev/null @@ -1,104 +0,0 @@ -import Quill from './core'; - -import { AlignClass, AlignStyle } from './formats/align'; -import { DirectionAttribute, DirectionClass, DirectionStyle } from './formats/direction'; -import { IndentClass as Indent } from './formats/indent'; - -import Blockquote from './formats/blockquote'; -import Header from './formats/header'; -import List, { ListItem } from './formats/list'; - -import { BackgroundClass, BackgroundStyle } from './formats/background'; -import { ColorClass, ColorStyle } from './formats/color'; -import { FontClass, FontStyle } from './formats/font'; -import { SizeClass, SizeStyle } from './formats/size'; - -import Bold from './formats/bold'; -import Italic from './formats/italic'; -import Link from './formats/link'; -import Script from './formats/script'; -import Strike from './formats/strike'; -import Underline from './formats/underline'; - -import Image from './formats/image'; -import Video from './formats/video'; - -import CodeBlock, { Code as InlineCode } from './formats/code'; - -import Formula from './modules/formula'; -import Syntax from './modules/syntax'; -import Toolbar from './modules/toolbar'; - -// import Icons from './ui/icons'; -// import Picker from './ui/picker'; -// import ColorPicker from './ui/color-picker'; -// import IconPicker from './ui/icon-picker'; -// import Tooltip from './ui/tooltip'; - -// import BubbleTheme from './themes/bubble'; -// import SnowTheme from './themes/snow'; - - -Quill.register({ - 'attributors/attribute/direction': DirectionAttribute, - - 'attributors/class/align': AlignClass, - 'attributors/class/background': BackgroundClass, - 'attributors/class/color': ColorClass, - 'attributors/class/direction': DirectionClass, - 'attributors/class/font': FontClass, - 'attributors/class/size': SizeClass, - - 'attributors/style/align': AlignStyle, - 'attributors/style/background': BackgroundStyle, - 'attributors/style/color': ColorStyle, - 'attributors/style/direction': DirectionStyle, - 'attributors/style/font': FontStyle, - 'attributors/style/size': SizeStyle -}, true); - - -Quill.register({ - 'formats/align': AlignClass, - 'formats/direction': DirectionClass, - 'formats/indent': Indent, - - 'formats/background': BackgroundStyle, - 'formats/color': ColorStyle, - 'formats/font': FontClass, - 'formats/size': SizeClass, - - 'formats/blockquote': Blockquote, - 'formats/code-block': CodeBlock, - 'formats/header': Header, - 'formats/list': List, - - 'formats/bold': Bold, - 'formats/code': InlineCode, - 'formats/italic': Italic, - 'formats/link': Link, - 'formats/script': Script, - 'formats/strike': Strike, - 'formats/underline': Underline, - - 'formats/image': Image, - 'formats/video': Video, - - 'formats/list/item': ListItem, - - 'modules/formula': Formula, - 'modules/syntax': Syntax, - 'modules/toolbar': Toolbar - - // 'themes/bubble': BubbleTheme, - // 'themes/snow': SnowTheme, - - // 'ui/icons': Icons, - // 'ui/picker': Picker, - // 'ui/icon-picker': IconPicker, - // 'ui/color-picker': ColorPicker, - // 'ui/tooltip': Tooltip -}, true); - - -export default Quill;