diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index dd27838efc0..0069d1bf4af 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -24,7 +24,12 @@ import { triggerListeners, updateEditor, } from './LexicalUpdates'; -import {createUID, dispatchCommand, markAllNodesAsDirty} from './LexicalUtils'; +import { + createUID, + dispatchCommand, + getDefaultView, + markAllNodesAsDirty, +} from './LexicalUtils'; import {DecoratorNode} from './nodes/LexicalDecoratorNode'; import {LineBreakNode} from './nodes/LexicalLineBreakNode'; import {ParagraphNode} from './nodes/LexicalParagraphNode'; @@ -450,6 +455,7 @@ export class LexicalEditor { _onError: ErrorHandler; _htmlConversions: DOMConversionCache; _readOnly: boolean; + _window: null | Window; constructor( editorState: EditorState, @@ -508,6 +514,7 @@ export class LexicalEditor { this._htmlConversions = htmlConversions; this._readOnly = false; this._headless = false; + this._window = null; } isComposing(): boolean { @@ -695,11 +702,13 @@ export class LexicalEditor { } if (nextRootElement !== null) { + const windowObj = getDefaultView(nextRootElement); const style = nextRootElement.style; style.userSelect = 'text'; style.whiteSpace = 'pre-wrap'; style.wordBreak = 'break-word'; nextRootElement.setAttribute('data-lexical-editor', 'true'); + this._window = windowObj; this._dirtyType = FULL_RECONCILE; initMutationObserver(this); @@ -711,6 +720,8 @@ export class LexicalEditor { if (!this._config.disableEvents) { addRootElementEvents(nextRootElement, this); } + } else { + this._window = null; } triggerListeners('root', this, false, nextRootElement, prevRootElement); diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index 7e530b202a5..1dce73c15f8 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -86,6 +86,7 @@ import { getDOMTextNode, getEditorsToPropagate, getNearestEditorFromDOMNode, + getWindow, isBackspace, isBold, isCopy, @@ -237,7 +238,7 @@ function onSelectionChange( // If we have marked a collapsed selection format, and we're // within the given time range – then attempt to use that format // instead of getting the format from the anchor node. - const windowEvent = window.event; + const windowEvent = getWindow(editor).event; const currentTimeStamp = windowEvent ? windowEvent.timeStamp : performance.now(); @@ -372,7 +373,7 @@ function onBeforeInput(event: InputEvent, editor: LexicalEditor): void { // user has dom.event.clipboardevents.enabled disabled in // about:config. In that case, we need to process the // pasted content in the DOM mutation phase. - (IS_FIREFOX && isFirefoxClipboardEvents()) + (IS_FIREFOX && isFirefoxClipboardEvents(editor)) ) { return; } else if (inputType === 'insertCompositionText') { diff --git a/packages/lexical/src/LexicalMutations.ts b/packages/lexical/src/LexicalMutations.ts index c2d995aa796..4d99a42f580 100644 --- a/packages/lexical/src/LexicalMutations.ts +++ b/packages/lexical/src/LexicalMutations.ts @@ -31,6 +31,7 @@ import { $getNearestNodeFromDOMNode, $updateTextNodeFromDOMContent, getNodeFromDOMNode, + getWindow, internalGetRoot, isFirefoxClipboardEvents, } from './LexicalUtils'; @@ -48,9 +49,9 @@ function updateTimeStamp(event: Event) { lastTextEntryTimeStamp = event.timeStamp; } -function initTextEntryListener(): void { +function initTextEntryListener(editor: LexicalEditor): void { if (lastTextEntryTimeStamp === 0) { - window.addEventListener('textInput', updateTimeStamp, true); + getWindow(editor).addEventListener('textInput', updateTimeStamp, true); } } @@ -292,7 +293,7 @@ export function $flushMutations( $setSelection(selection); } - if (IS_FIREFOX && isFirefoxClipboardEvents()) { + if (IS_FIREFOX && isFirefoxClipboardEvents(editor)) { selection.insertRawText(possibleTextForFirefoxPaste); } } @@ -312,7 +313,7 @@ export function flushRootMutations(editor: LexicalEditor): void { } export function initMutationObserver(editor: LexicalEditor): void { - initTextEntryListener(); + initTextEntryListener(editor); editor._observer = new MutationObserver( (mutations: Array, observer: MutationObserver) => { $flushMutations(editor, mutations, observer); diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index 1c172b7ec14..78f90e9b2ef 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -2281,6 +2281,10 @@ export function internalCreateRangeSelection( domSelection: Selection | null, editor: LexicalEditor, ): null | RangeSelection { + const windowObj = editor._window; + if (windowObj === null) { + return null; + } // When we create a selection, we try to use the previous // selection where possible, unless an actual user selection // change has occurred. When we do need to create a new selection @@ -2295,7 +2299,7 @@ export function internalCreateRangeSelection( // reconciliation unless there are dirty nodes that need // reconciling. - const windowEvent = window.event; + const windowEvent = windowObj.event; const eventType = windowEvent ? windowEvent.type : undefined; const isSelectionChange = eventType === 'selectionchange'; const useDOMSelection = diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index f3b8526fb31..f59010f85ee 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1091,8 +1091,8 @@ export function $getDecoratorNode( return null; } -export function isFirefoxClipboardEvents(): boolean { - const event = window.event; +export function isFirefoxClipboardEvents(editor: LexicalEditor): boolean { + const event = getWindow(editor).event; const inputType = event && (event as InputEvent).inputType; return ( inputType === 'insertFromPaste' || @@ -1148,7 +1148,7 @@ export function scrollIntoViewIfNeeded( if (element !== null) { const rect = element.getBoundingClientRect(); - if (rect.bottom > window.innerHeight) { + if (rect.bottom > getWindow(editor).innerHeight) { element.scrollIntoView(false); } else if (rect.top < 0) { element.scrollIntoView(); @@ -1212,3 +1212,16 @@ function $hasAncestor(child: LexicalNode, targetNode: LexicalNode): boolean { } return false; } + +export function getDefaultView(domElem: HTMLElement): Window | null { + const ownerDoc = domElem.ownerDocument; + return (ownerDoc && ownerDoc.defaultView) || null; +} + +export function getWindow(editor: LexicalEditor): Window { + const windowObj = editor._window; + if (windowObj === null) { + invariant(false, 'window object not found'); + } + return windowObj; +} diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 8025779caff..1a26586f20c 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -76,5 +76,7 @@ "74": "Create node: Type %s in node %s does not match registered node %s with the same type", "75": "Reconciliation: could not find DOM element for node key %s", "76": "append: attempting to append self", - "77": "LexicalAutoLinkPlugin: AutoLinkNode not registered on editor" + "77": "LexicalAutoLinkPlugin: AutoLinkNode not registered on editor", + "78": "window object not found", + "79": "MarkdownShortcuts: missing dependency for transformer. Ensure node dependency is included in editor initial config." }