diff --git a/packages/quill/src/core/emitter.ts b/packages/quill/src/core/emitter.ts index 7e981ed47b..6076ee7581 100644 --- a/packages/quill/src/core/emitter.ts +++ b/packages/quill/src/core/emitter.ts @@ -5,16 +5,30 @@ import logger from './logger.js'; const debug = logger('quill:events'); const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click']; -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); - } +// Documents where we've already registered Quill's global event listeners. +// WeakSet so entries are dropped when a document is GC'd (e.g. JSDOM teardown). +const registeredDocuments = new WeakSet(); + +// Registers global event listeners on the given document if not already present. +// Called lazily from the Quill constructor to avoid touching `document` at import +// time, which would crash in non-browser environments (Node, SSR). +function ensureDocumentListeners(doc: Document) { + if (registeredDocuments.has(doc)) return; + registeredDocuments.add(doc); + + // Queries are scoped to `doc` rather than the global `document` so + // listeners stay correct in multi-document scenarios (iframes, JSDOM). + EVENTS.forEach((eventName) => { + doc.addEventListener(eventName, (...args) => { + Array.from(doc.querySelectorAll('.ql-container')).forEach((node) => { + const quill = instances.get(node); + if (quill && quill.emitter) { + quill.emitter.handleDOM(...args); + } + }); }); }); -}); +} class Emitter extends EventEmitter { static events = { @@ -72,4 +86,5 @@ class Emitter extends EventEmitter { export type EmitterSource = (typeof Emitter.sources)[keyof typeof Emitter.sources]; +export { ensureDocumentListeners }; export default Emitter; diff --git a/packages/quill/src/core/quill.ts b/packages/quill/src/core/quill.ts index 60744570f0..a84c7df48c 100644 --- a/packages/quill/src/core/quill.ts +++ b/packages/quill/src/core/quill.ts @@ -10,7 +10,7 @@ import type History from '../modules/history.js'; import type Keyboard from '../modules/keyboard.js'; import type Uploader from '../modules/uploader.js'; import Editor from './editor.js'; -import Emitter from './emitter.js'; +import Emitter, { ensureDocumentListeners } from './emitter.js'; import type { EmitterSource } from './emitter.js'; import instances from './instances.js'; import logger from './logger.js'; @@ -209,6 +209,7 @@ class Quill { this.container.classList.add('ql-container'); this.container.innerHTML = ''; instances.set(this.container, this); + ensureDocumentListeners(this.container.ownerDocument); this.root = this.addContainer('ql-editor'); this.root.classList.add('ql-blank'); this.emitter = new Emitter(); diff --git a/packages/quill/src/modules/clipboard.ts b/packages/quill/src/modules/clipboard.ts index 78ea670839..4e6f948626 100644 --- a/packages/quill/src/modules/clipboard.ts +++ b/packages/quill/src/modules/clipboard.ts @@ -29,14 +29,21 @@ const debug = logger('quill:clipboard'); type Selector = string | Node['TEXT_NODE'] | Node['ELEMENT_NODE']; type Matcher = (node: Node, delta: Delta, scroll: ScrollBlot) => Delta; +// Constants from the browser's global "Node" object. +// Strong types ensure the constants are accurate, +// while avoiding an import time dependency on browser provided global. +// Avoiding this dependency allows this code to be imported more easily in non-browser environments such as tests and SSR. +const TEXT_NODE: typeof Node.TEXT_NODE = 3; +const ELEMENT_NODE: typeof Node.ELEMENT_NODE = 1; + const CLIPBOARD_CONFIG: [Selector, Matcher][] = [ - [Node.TEXT_NODE, matchText], - [Node.TEXT_NODE, matchNewline], + [TEXT_NODE, matchText], + [TEXT_NODE, matchNewline], ['br', matchBreak], - [Node.ELEMENT_NODE, matchNewline], - [Node.ELEMENT_NODE, matchBlot], - [Node.ELEMENT_NODE, matchAttributor], - [Node.ELEMENT_NODE, matchStyles], + [ELEMENT_NODE, matchNewline], + [ELEMENT_NODE, matchBlot], + [ELEMENT_NODE, matchAttributor], + [ELEMENT_NODE, matchStyles], ['li', matchIndent], ['ol, ul', matchList], ['pre', matchCodeBlock], diff --git a/packages/quill/src/modules/keyboard.ts b/packages/quill/src/modules/keyboard.ts index 7941b370f5..2a0ff8a434 100644 --- a/packages/quill/src/modules/keyboard.ts +++ b/packages/quill/src/modules/keyboard.ts @@ -10,7 +10,10 @@ import type { Range } from '../core/selection.js'; const debug = logger('quill:keyboard'); -const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey'; +const SHORTKEY = + typeof navigator !== 'undefined' && /Mac/i.test(navigator.platform) + ? 'metaKey' + : 'ctrlKey'; export interface Context { collapsed: boolean; diff --git a/packages/quill/src/modules/syntax.ts b/packages/quill/src/modules/syntax.ts index da99fdc41d..621877f230 100644 --- a/packages/quill/src/modules/syntax.ts +++ b/packages/quill/src/modules/syntax.ts @@ -334,7 +334,7 @@ class Syntax extends Module { } Syntax.DEFAULTS = { hljs: (() => { - return window.hljs; + return typeof window !== 'undefined' ? window.hljs : null; })(), interval: 1000, languages: [ diff --git a/packages/quill/src/modules/uiNode.ts b/packages/quill/src/modules/uiNode.ts index 780713a3ba..3e12cb22e0 100644 --- a/packages/quill/src/modules/uiNode.ts +++ b/packages/quill/src/modules/uiNode.ts @@ -2,7 +2,8 @@ import { ParentBlot } from 'parchment'; import Module from '../core/module.js'; import Quill from '../core/quill.js'; -const isMac = /Mac/i.test(navigator.platform); +const isMac = + typeof navigator !== 'undefined' && /Mac/i.test(navigator.platform); // Export for testing export const TTL_FOR_VALID_SELECTION_CHANGE = 100;