From 21868969cd00434d02eeedb1fae3f0b2a3af0366 Mon Sep 17 00:00:00 2001 From: web-padawan Date: Tue, 2 Oct 2018 17:02:15 +0300 Subject: [PATCH] Shadow DOM support and tests --- core/emitter.js | 29 +++++++++++++++++++------ core/selection.js | 9 ++++---- modules/toolbar.js | 5 +++-- test/unit/core/selection.js | 41 ++++++++++++++++++++++++++++++++++++ test/unit/modules/toolbar.js | 32 ++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 12 deletions(-) diff --git a/core/emitter.js b/core/emitter.js index 5814f127c1..5c20eda1a8 100644 --- a/core/emitter.js +++ b/core/emitter.js @@ -4,14 +4,13 @@ import logger from './logger'; let debug = logger('quill:events'); const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click']; +const EMITTERS = []; +const supportsRootNode = ('getRootNode' in document); EVENTS.forEach(function(eventName) { document.addEventListener(eventName, (...args) => { - [].slice.call(document.querySelectorAll('.ql-container')).forEach((node) => { - // TODO use WeakMap - if (node.__quill && node.__quill.emitter) { - node.__quill.emitter.handleDOM(...args); - } + EMITTERS.forEach((em) => { + em.handleDOM(...args); }); }); }); @@ -21,6 +20,7 @@ class Emitter extends EventEmitter { constructor() { super(); this.listeners = {}; + EMITTERS.push(this); this.on('error', debug.error); } @@ -30,8 +30,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(function({ 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 92131a74bf..c74d04a010 100644 --- a/core/selection.js +++ b/core/selection.js @@ -22,12 +22,13 @@ class Selection { this.composing = false; this.mouseDown = false; this.root = this.scroll.domNode; + this.rootDocument = (this.root.getRootNode ? this.root.getRootNode() : document); this.cursor = Parchment.create('cursor', this); // savedRange is last non-null range this.lastRange = this.savedRange = new Range(0, 0); this.handleComposition(); this.handleDragging(); - this.emitter.listenDOM('selectionchange', document, () => { + this.emitter.listenDOM('selectionchange', this.rootDocument, () => { if (!this.mouseDown) { setTimeout(this.update.bind(this, Emitter.sources.USER), 1); } @@ -157,7 +158,7 @@ class Selection { } getNativeRange() { - let selection = document.getSelection(); + let selection = this.rootDocument.getSelection(); if (selection == null || selection.rangeCount <= 0) return null; let nativeRange = selection.getRangeAt(0); if (nativeRange == null) return null; @@ -174,7 +175,7 @@ class Selection { } hasFocus() { - return document.activeElement === this.root; + return this.rootDocument.activeElement === this.root; } normalizedToRange(range) { @@ -268,7 +269,7 @@ class Selection { if (startNode != null && (this.root.parentNode == null || startNode.parentNode == null || endNode.parentNode == null)) { return; } - let selection = document.getSelection(); + let selection = this.rootDocument.getSelection(); if (selection == null) return; if (startNode != null) { if (!this.hasFocus()) this.root.focus(); diff --git a/modules/toolbar.js b/modules/toolbar.js index 8cbae0809f..2b9d3ad3a0 100644 --- a/modules/toolbar.js +++ b/modules/toolbar.js @@ -4,9 +4,9 @@ import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; +const supportsRootNode = ('getRootNode' in document); let debug = logger('quill:toolbar'); - class Toolbar extends Module { constructor(quill, options) { super(quill, options); @@ -16,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 a9a157b3d0..1a7a8efb71 100644 --- a/test/unit/core/selection.js +++ b/test/unit/core/selection.js @@ -37,6 +37,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() { let selection = this.initialize(Selection, ''); diff --git a/test/unit/modules/toolbar.js b/test/unit/modules/toolbar.js index 83cb1215e0..bbefc56814 100644 --- a/test/unit/modules/toolbar.js +++ b/test/unit/modules/toolbar.js @@ -97,6 +97,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() { let container = this.initialize(HTMLElement, `