pax_global_header00006660000000000000000000000064142320257100014506gustar00rootroot0000000000000052 comment=3c400dbd1e128750ae631e2b7d07ae50a9cea976 prosemirror-view-1.23.13/000077500000000000000000000000001423202571000152105ustar00rootroot00000000000000prosemirror-view-1.23.13/.gitignore000066400000000000000000000001131423202571000171730ustar00rootroot00000000000000/node_modules .tern-port /dist /notes.txt /test/mocha.css webpack.config.jsprosemirror-view-1.23.13/.npmignore000066400000000000000000000000371423202571000172070ustar00rootroot00000000000000/node_modules .tern-port /test prosemirror-view-1.23.13/.npmrc000066400000000000000000000000231423202571000163230ustar00rootroot00000000000000package-lock=false prosemirror-view-1.23.13/.tern-project000066400000000000000000000001571423202571000176300ustar00rootroot00000000000000{ "libs": ["browser"], "plugins": { "node": {}, "complete_strings": {}, "es_modules": {} } } prosemirror-view-1.23.13/CHANGELOG.md000066400000000000000000002034251423202571000170270ustar00rootroot00000000000000## 1.23.13 (2022-04-26) ### Bug fixes Work around a hidden cursor issue in Chrome when a textblock ends in an uneditable node wrapped in a mark. ## 1.23.12 (2022-04-08) ### Bug fixes Fix an issue where enter on Chrome Android could, in textblock nodes rendered with an inner content element, delete the text after the cursor. ## 1.23.11 (2022-03-31) ### Bug fixes Fix an issue where a node view with a separate content wrapper node could sometimes lose its content on Chrome when backspacing out content due to unexpected DOM element recreation. ## 1.23.10 (2022-03-22) ### Bug fixes Fix a crash in `DecorationSet.map` that could occur with some kinds of lift transformations. ## 1.23.9 (2022-03-11) ### Bug fixes Make sure screen readers don't read the `` nodes added as kludge for cursor behavior. ## 1.23.8 (2022-03-10) ### Bug fixes Fix an issue where the editor needlessly interrupted composition with IME systems that keep the cursor at the start of the composition, such as some Pinyin input methods. ## 1.23.7 (2022-02-25) ### Bug fixes Fix a crash when a view was being destroyed during input reading. Fix a crash on Firefox when replacing text in some specific types of document structure. ## 1.23.6 (2022-01-13) ### Bug fixes Fix an issue that could cause pieces of DOM to not be synchronized with the document after some kind of changes around marks. Fix a bug where marks rendered with nested elements would in some situations suppress new input. Disable Chrome-specific drag-selection workaround in non-Chrome browsers because it affected table cell selection in Safari. ## 1.23.5 (2021-12-27) ### Bug fixes Use the `whitespace` node prop where appropriate. ## 1.23.4 (2021-12-22) ### Bug fixes Improve the way the editor handles the mess of events produced when pressing Enter before a word that was just typed on Chrome + GBoard. Fix an issue where compositions right before another instance of the composed text could cause the editor to crash. Fix an issue where, if decorations changed during a pointer drag selection, the selection anchor might move around inappropriately. ## 1.23.3 (2021-11-26) ### Bug fixes The clipboard parser will no longer drop trailing `
` nodes that appear in an inline parent element. ## 1.23.2 (2021-11-19) ### Bug fixes Avoid some unnecessary node redraws when marks are present in sibling nodes. ## 1.23.1 (2021-11-15) ### Bug fixes Restore accidentally reduced lookahead distance in view updating algorithm. ## 1.23.0 (2021-11-11) ### Bug fixes When parsing clipboard content, ignore trailing BR nodes that look like they might be there as a contenteditable kludge. ### New features `EditorView` now exposes an `isDestroyed` property that can be used to test if the view has been destroyed. ## 1.22.0 (2021-11-08) ### Bug fixes Fix an issue where some types of node decoration changes could cause an unnecessary cascade of node redraws. ### New features Widget decorations now accept a `destroy` option, which will be called when the widget is removed from the view. ## 1.21.0 (2021-10-29) ### Bug fixes Fix issue where recent Mobile Safari versions weren't treated as Mac platforms. ### New features Multiple `style` properties provided through the `attributes` prop are now merged. Adjust mac detection for recent changes to navigator.platform on iOS ## 1.20.3 (2021-10-13) ### Bug fixes Stop removing leading/trailing whitespace from pasted plain text. Fix an issue that could cause invalid content to be produced when pasting HTML with isolating nodes in it. ## 1.20.2 (2021-10-07) ### Bug fixes Fix a crash when pasting whitespace-only content as text. ## 1.20.1 (2021-09-09) ### Bug fixes The library accidentally allowed node decorations on text nodes. It no longer does. Fix an issue on Chrome and Safari where coordinates for positions between uneditable nodes and the end of a textblock would return zero-height rectangles. Fix a bug where vertical `endOfTextblock` queries could inappropriately return true when a small line height is used. ## 1.20.0 (2021-09-03) ### New features It is now possible to pass plugins directly to the view with the `plugins` direct prop. ## 1.19.3 (2021-08-20) ### Bug fixes Fix an issue where generic styles for elements could cause separator nodes created by the editor to impact layout. Fix an issue where moving to another tab and back could clear a node selection. ## 1.19.2 (2021-08-19) ### Bug fixes Avoid some bugs around drag-selecting by delaying synchronization between the DOM and the state selection until the end of the drag. ## 1.19.1 (2021-08-16) ### Bug fixes Fix another issue around copy-pasting table structure, causing inappropriate opening of copied cell selections. ## 1.19.0 (2021-08-13) ### Bug fixes Add a DOM attribute to the content element to avoid automatic translation services from messing with the editable text. Fix a bug where copy-pasting table content sometimes carried along superfluous table markup. Fix issue where end-of-textblock detection didn't use the correct selection when in a shadow root. ### New features The `DecorationSource` interface now exposes a `map` method. Add a translate=no attribute to the editor element by default ## 1.18.11 (2021-07-22) ### Bug fixes Work around an issue where Chrome and Safari will replace some spaces with non-breaking spaces when putting HTML on the clipboard. When pasting as plain text (shift-mod-v) apply the marks at the selection to the inserted content. Fix flaky behavior when starting a composition with a selection that spans multiple blocks. ## 1.18.10 (2021-07-15) ### Bug fixes Fix an issue where dragging from just outside a draggable node on Chrome would cause odd dragging behavior. ## 1.18.9 (2021-07-11) ### Bug fixes Fix a bug in the previous release where `handleClickOn` wasn't fired anymore for clicks with the middle or right mouse button. ## 1.18.8 (2021-06-23) ### Bug fixes Work around a Safari bug where it draws the cursor at the start of the line when it is after an uneditable node at the end of the line. Fix an issue where the DOM could get out of sync when editing decorated text. Work around an issue where Firefox draws the cursor on the wrong line when after a newline. Fix a bug where double-clicking with the left mouse button and then pressing another mouse button was treated as a triple click. ## 1.18.7 (2021-05-20) ### Bug fixes Fix a bug where clicking on a textblock that had a node-selected parent didn't set a cursor selection. Fix a bug that caused a workaround for a Chrome Android issue to not work correctly, leading to bad cursor placement after some types of text input. ## 1.18.6 (2021-05-17) ### Bug fixes Fix a crash in mouse click handling introduced in the previous version. ## 1.18.5 (2021-05-17) ### Bug fixes Work around a Firefox bug where backspace sometimes deletes the node after the cursor. Fix a bug that prevented `transformPasted` hooks from being called on content dragged within the editor. Fixes an issue where clicking near a node or other special selection on Chrome would in some cases do nothing. ## 1.18.4 (2021-04-27) ### Bug fixes Fix incorrect drag cursor in Chrome on some platforms. Fix an issue where a race condition could leave a node uneditable when clicked. Fix scroll handling when the editor is placed through a DOM component slot. Fix a typo in the Chrome backspace workaround. Fixes an issue where, when mouseup events weren't being delivered, the editor could leak event handlers. ## 1.18.3 (2021-04-13) ### Bug fixes Fix an issue where, when pressing enter or space at the start of a composition, the cursor would jump to the end of the composition on Chrome Android. Fix an issue that would cause Enter presses to be dropped on Android when in a node whose DOM representation nested more than one element. Fix a bug where pasting specific types of HTML could cause a crash. ## 1.18.2 (2021-03-25) ### Bug fixes Properly handle CSS class name strings with extra spaces in decorations. Fix a performance bug when updating nodes with thousands of children. ## 1.18.1 (2021-03-15) ### Bug fixes Fix the scrolling-into-view logic in the case where a scale transformation is applied to the editor. Strip carriage return chars from text pasted as code Remove carriage return characters when pasting text into code blocks. ## 1.18.0 (2021-03-04) ### Bug fixes Fix a crash in `posAtDOM`. ### New features Node view constructors and `update` methods are now passed the inner decorations of the node. ## 1.17.8 (2021-02-26) ### Bug fixes Fix an issue where some user actions (such as enter on iOS) in a node whose content DOM element isn't it's top element could leave the DOM in a damaged state. ## 1.17.7 (2021-02-22) ### Bug fixes Fix an issue where the `ProseMirror-hideselection` element class would be briefly removed and then restored when moving from one invisible selection to another. Fix an issue where the cursor could end up on the wrong side of a widget with `side` < 0. ## 1.17.6 (2021-02-11) ### Bug fixes Fix an issue where using the vertical arrow keys after select-all didn't update the selection. ## 1.17.5 (2021-02-05) ### Bug fixes Fix an issue where the view could go into an endless DOM flush loop in specific circumstances involving asynchronous DOM mutation. ## 1.17.4 (2021-02-04) ### Bug fixes Add another kludge to work around an issue where Firefox displays the cursor in the wrong place in code blocks. Fix a bug where validation of decorations passed to `DecorationSet.add` sometimes passed the wrong offsets to the validator. Fix bad selection position in empty textblocks. Solves several issues with editing in Firefox Android. ## 1.17.3 (2021-01-29) ### Bug fixes Fix a bug where adding invalid decorations (for example zero-length inline decorations) with `DecorationSet.add` would fail to drop those. ## 1.17.2 (2021-01-12) ### Bug fixes The library will now always let the browser perform its native pasting behavior when the clipboard data is empty and no paste handler handles the event. Fix a bug where `domAtPos` (and thus cursor placement) would pick positions inside uneditable DOM or atom nodes. ## 1.17.1 (2021-01-08) ### Bug fixes Fix a regression in `coordsAtPos` when used on an empty line at the end of a code block. ## 1.17.0 (2021-01-07) ### Bug fixes Fix an issue where starting a composition with stored marks would sometimes create the wrong steps (and thus break the mark) on Chrome. ### New features `EditorView.domAtPos` now takes a second parameter that can be used to control whether it should enter DOM nodes on the side of the given position. ## 1.16.5 (2020-12-11) ### Bug fixes Fix platform detection on recent iPadOS versions, restoring several workarounds for bugs that were accidentally turned off there. ## 1.16.4 (2020-12-02) ### Bug fixes Fix an issue where the cursor ended up in the wrong place when pressing enter in an empty heading on iOS. ## 1.16.3 (2020-11-23) ### Bug fixes Fix an issue where pressing enter at the start of a line in a code block would leave the visible cursor in the wrong place on Firefox. ## 1.16.2 (2020-11-18) ### Bug fixes Fix a bug where overlapping inline decorations would get drawn incorrectly (and even corrupt the drawing of unrelated content). ## 1.16.1 (2020-10-26) ### Bug fixes Fix an issue where the attributes of defining nodes were dropped when copying to the clipboard. ## 1.16.0 (2020-10-01) ### Bug fixes Fix an issue where a drag starting briefly after an aborted drag could confuse the view and break the second drag. Allow callers of coordsAtPos to specify a side ### New features `EditorView.coordsAtPos` now takes a `side` argument that determines which side of the position to look, if ambiguous. ## 1.15.7 (2020-09-11) ### Bug fixes Fix an issue where, when inserting `
` nodes, Safari would briefly show the cursor before the inserted break, though the DOM selection had already been set after it. When dragging inside the editor, whether the operation copies or moves is now determined by the modifiers held on drop, not on drag start. ## 1.15.6 (2020-09-03) ### Bug fixes Fix issue where the DOM selection could end up in an invalid state after a keyboard cursor motion event that had no effect. Fix an issue where some types of drop events would fail to select the dropped content. Work around Safari issues when pressing shift-down with the cursor before an uneditable element. ## 1.15.5 (2020-08-25) ### Bug fixes Fix an issue where mapping a decoration set could corrupt the decoration positions in specific cases. ## 1.15.4 (2020-08-13) ### Bug fixes Fix a crash that occurred when inline decorations covered inline nodes that weren't leaf nodes. ## 1.15.3 (2020-08-11) ### Bug fixes Work around a Firefox issue where the cursor is sometimes shown in the wrong place when directly after a `
` node. The editor will now reset composition when stored marks are set on the state, so that the marks can be added to the next input. Inline decorations are no longer applied to inline nodes that aren't leaves, only to the innermost layer. ## 1.15.2 (2020-07-09) ### Bug fixes Adjust the workaround for Chrome's DOM selection corruption bug to cover more cases. ## 1.15.1 (2020-07-09) ### Bug fixes Work around another issue where Chrome misreports the DOM selection. ## 1.15.0 (2020-06-24) ### Bug fixes Fix an issue where Enter on iOS might be handled twice on slow devices. Pass plain text flag to transformPastedText and clipboardTextParser props Fix a bug where typing in front of a mark could in some circumstances cause the editor to discard the new content. ### New features The `transformPastedText` and `clipboardTextParser` props now receive an extra argument, `plain`, indicating whether the paste was forced as plain text. ## 1.14.13 (2020-06-05) ### Bug fixes Fix a bug where storing DOM nodes directly in widget decorations (not recommended) could cause the view to try and place the same DOM node multiple times. ## 1.14.12 (2020-06-03) ### Bug fixes Fix a crash when the editor tries to read a DOM selection outside of itself. Improve the way inline decorations covering non-leaf inline nodes are rendered. Ensure elt is defined before accessing it in posAtCoords Fix a crash in Safari when the browser's `elementFromPoint` returns null in `posAtCoords`. Handle case where Chrome flips the nesting order of edited inline nodes Fix the issue of `` marks on decorated text being lost during editing because Chrome changes the nesting order of the link and the decoration `` element in the DOM. Fix an issue where, when pressing enter with a bolded virtual keyboard suggestion on Android's Gboard, the cursor would stay on the wrong line. ## 1.14.11 (2020-05-19) ### Bug fixes Fix bug in the way the editor handles Cmd-arrow presses on macOS. ## 1.14.10 (2020-05-18) ### Bug fixes Fix an issue where the editor would override behavior for Cmd-arrow key presses on macOS the wrong way in some situations. Fix handling of copy and paste in IE when top-level elements can't be focused. ## 1.14.9 (2020-05-06) ### Bug fixes Fix a crash on IE, which sets `document.activeElement` to null in some circumstances. ## 1.14.8 (2020-05-01) ### Bug fixes Work around an issue in Safari where you couldn't click inside a selected element to put the cursor there. Fix enter at start of paragraph in iOS inserting two new paragraphs. Scrolling the cursor into view now makes sure it doesn't end up below a scrollbar. ## 1.14.7 (2020-04-20) ### Bug fixes Fix a crash on Chrome during selection updates when `Selection.collapse` inexplicably leaves the selection empty. Update documented type for handlePaste event arg Fix another issue that could break decoration set mapping in deeply nested nodes. ## 1.14.6 (2020-03-25) ### Bug fixes Fix superfluous cursor showing up in Chrome when there is a gap cursor or similar custom empty selection active. Fix an issue where `DecorationSet.remove` would ignore the positions of its argument decorations, and only compare by type. ## 1.14.5 (2020-03-23) ### Bug fixes Work around Chrome Android issue where pasting would close the virtual keyboard. Fix an issue where some kinds of changes would cause nodes to show up twice in the DOM. ## 1.14.4 (2020-03-17) ### Bug fixes Improve return values from `coordsAtPos` on line breaks in Safari and Firefox. Make sure enter on iOS is handled even when the native behavior has no effect. ## 1.14.3 (2020-03-16) ### Bug fixes Fix mismatch between DOM and state selection bug at compositionend in IE11. Make sure `handleDrop`](https://prosemirror.net/docs/ref/#view.EditorProps.handleDrop) is called even when there's nothing on the clipboard. Fix a bug where reconfiguring a view in a way that changed both the active node views and the attributes of the top node left the old attributes active. Work around another case where Chrome lies to the script about its current DOM selection state. Avoid redrawing nodes when both their content and a widget in front of them is updated in the same transaction. Fix issue where scrolling with multiple scrollable containers sometimes moves to the wrong position. ## 1.14.2 (2020-02-10) ### Bug fixes Fix bug when starting a composition after a link, when the composition started with the character that ended the link. ## 1.14.1 (2020-02-08) ### Bug fixes Fix issue where scrolling the cursor into view in a scrollable editor would sometimes inappropriately scroll an outer container as well. ## 1.14.0 (2020-02-07) ### Bug fixes Fix parsing of `tbody`, `tfoot`, and `caption` elements in pasted HTML content. Fix bug in selection-is-at-edge check Fix an issue where moving focus to the editor with the keyboard or the DOM `focus` method would leave the DOM and state selections inconsistent. ### New features Widget decorations can now take an `ignoreSelection` option, that causes the editor to leave selections inside them alone. ## 1.13.11 (2020-01-31) ### Bug fixes Fix an issue that could lead to the editor making regular content uneditable on Safari. ## 1.13.10 (2020-01-29) ### Bug fixes Fix a crash on Firefox when starting a composition after a marked non-text node. ## 1.13.9 (2020-01-29) ### Bug fixes Make sure to reset the selection when the browser moves it into an uneditable node. Fix issue where the editor would fail to create a meaningful DOM selection for a node selection on Safari. Makes sure the iOS virtual keyboard gets its internal state (autocorrection, autocapitalization) updated when the user presses enter. ## 1.13.8 (2020-01-24) ### Bug fixes Fix bug that would sometimes cause widget decorations to be drawn with marks from the node after the text node they were inside of. ## 1.13.7 (2019-12-16) ### Bug fixes Fix a bug that caused the DOM to go out of sync with the decorations when updating inline decorations that added multiple wrapping nodes to a piece of content. ## 1.13.6 (2019-12-13) ### Bug fixes Fix a crash when deleting a list item in Safari while using a parse rule with a `context` property for `
  • ` elements. Work around another case where Chrome reports an incorrect selection. Work around issue where Firefox will insert a stray BR node when deleting a text node in some types of DOM structure. ## 1.13.5 (2019-12-09) ### Bug fixes Fix the way decorations update node styles to allow removing CSS custom properties. Link to https in readme and changelog The `root` accessor on views now makes sure that, when it returns a shadow root, that object has a `getSelection` method. Fix an issue where the DOM selection could get out of sync with ProseMirror's selection state in Edge. ## 1.13.4 (2019-11-20) ### Bug fixes Rename ES module files to use a .js extension, since Webpack gets confused by .mjs ## 1.13.3 (2019-11-19) ### Bug fixes Fix issue where the editor wouldn't update its internal selection when the editor was blurred, its selection was changed programatically, and then the editor was re-focused with its old DOM selection. The file referred to in the package's `module` field now is compiled down to ES5. ## 1.13.2 (2019-11-14) ### Bug fixes Fix issue where `EditorView.focus` would scroll the top of the document into view on Safari. ## 1.13.1 (2019-11-12) ### Bug fixes Work around selection jumping that sometimes occurs on Chrome when focusing the editor. ## 1.13.0 (2019-11-08) ### New features Add a `module` field to package json file. ## 1.12.3 (2019-11-07) ### Bug fixes Fix issue where paste events were stopped when the clipboard parser failed to make sense of the content. Fix issue where the `handlePaste` prop might be called multiple times for a single paste. ## 1.12.2 (2019-11-05) ### Bug fixes Set the editable element to use a `white-space: break-spaces` style so that whitespace at the end of a line properly moves the cursor to the next line. Fix issue where `posAtCoords` could throw an error in some circumstances on Firefox. Don't force focus back on the editor if a node view moves focus in its `setSelection` method. ## 1.12.1 (2019-10-28) ### Bug fixes Reduce unnecessary redraws when typing creates a new text node on Chrome. The default prosemirror.css now also turns off ligatures in Edge. Fix issue where the cursor stays before the typed text in Edge, when typing in an empty paragraph or between hard break nodes. ## 1.12.0 (2019-10-21) ### New features The mutation records passed to [`ignoreMutation`](https://prosemirror.net/docs/ref/#view.NodeView.ignoreMutation) now contain the old attribute value. ## 1.11.7 (2019-10-15) ### Bug fixes Enabling a mark and then starting a composition, on Chrome Android, will no longer cause the cursor to jump to the start of the composition. ## 1.11.6 (2019-10-07) ### Bug fixes Fix workaround for broken IE11 DOM change records when inserting between `
    ` nodes to handle more cases. ## 1.11.5 (2019-10-04) ### Bug fixes Don't leave DOM selection in place when it is inside a node view but not inside its content DOM element. ## 1.11.4 (2019-09-27) ### Bug fixes Fix an IE11 issue where marks would sometimes unexpectedly get dropped when inserting a space after marked text. Fixes an issue where `handleTextInput` wasn't called when typing over a single character with the same character. ## 1.11.3 (2019-09-20) ### Bug fixes Fix an issue where the DOM node representing a mark could be corrupted when the browser decides to replace it with another node but ProseMirror restored the old node after the change. Handle another case where typing over a selection in IE11 confused the editor. ## 1.11.2 (2019-09-17) ### Bug fixes Fix an issue where typing over a decorated piece of text would sometimes just act like deletion. Fix another problem in IE11 with typing over content, where typing over a decorated bit of text caused a crash. ## 1.11.1 (2019-09-16) ### Bug fixes Fix issue where typing over the entire contents of an inline node on IE11 would insert the typed content in the wrong position. ## 1.11.0 (2019-09-16) ### Bug fixes Fix an issue where IE11 would select the entire textblock after deleting content at its start. ### New features View instances now have a public `editable` property that indicates whether they are in editable mode. ## 1.10.3 (2019-09-04) ### Bug fixes Fix a regression in 1.10.2 that broke copying on IE11. ## 1.10.2 (2019-09-03) ### Bug fixes Fix an issue where `posAtCoords` could crash by dereferencing undefined in some circumstances. Fix inserting text next to a hard break in IE11. Fix an issue where typing over a selection would result in two different transactions (once for the deletion, once for the insertion) on IE11. Selecting the word at the start of the document and typing over it no longer causes the text input to appear at the end of the document in IE11. ## 1.10.1 (2019-08-28) ### Bug fixes Copying content will no longer create elements in the main document, which prevents images from loading just because they appear in clipboard content. ## 1.10.0 (2019-08-13) ### Bug fixes Fix an issue that caused the cursor to be scrolled into view when `focus()` was called on IE11. Fix problem where the cursor cycled through pieces of right-to-left text on Firefox during horizontal motion when the gapcursor plugin was enabled. Fix spurious mutation events in Firefox causing mark replacement at end of composition. Restore call to dom.focus on view.focus Fix a bug that could cause node views in front of marked nodes to not be destroyed when deleted, and caused confusion in composition handling in some situations. Cursor wrappers (a kludge to make sure typed text gets wrapping DOM structure corresponding to the current marks) are now created less eagerly, and in a less invasive way, which resolves a number of problems with composition (especially on Safari) and bidirectional text. ### New features Node views can now ignore selection change events through their [`ignoreMutation`](https://prosemirror.net/docs/ref/#view.NodeView.ignoreMutation) callback. ## 1.9.13 (2019-07-29) ### Bug fixes Fix an issue where copying content from a ProseMirror instance into an instance using another schema could, in some circumstances, insert schema-violating content. Fix comparison of decoration sets, which should solve unneccesary re-renders when updating decorations with an identical but newly allocated set. Don't update DOM selection in uneditable editors when the focus is elsewhere Fix a bug where the editor would steal focus from child elements when in non-editable mode. Fix error and corruption in IE11 when backspacing out a single character after a br node. ## 1.9.12 (2019-07-16) ### Bug fixes Fix a crash `posAtCoords` in Firefox when the coordinates are above a text input field. ## 1.9.11 (2019-07-03) ### Bug fixes Fix an issue where the DOM change handler would treat the parsed content as the wrong part of the document. Fix an issue in IE11 where deleting the last character in a textblock causes a crash. Fix an issue where backspacing out the first character in a textblock would cause IE11 to move the selection to some incorrect position. ## 1.9.10 (2019-06-12) ### Bug fixes Fix a crash in `coordsAtPos` caused by use of an incorrect variable name. ## 1.9.9 (2019-06-09) ### Bug fixes Fix arrowing over unselectable inline nodes in Chrome and Safari, which by default introduce an extra needless cursor position before the node. Fix a bug that caused DOM changes to be ignored when happening directly in front of some types of DOM events (such as focus/blur). ## 1.9.8 (2019-05-29) ### Bug fixes Fix an issue where moving focus from a node inside of the editor to the editor itself could sometimes lead to a node selection around the inner node rather than the intended selection (on Chrome). ## 1.9.7 (2019-05-28) ### Bug fixes ProseMirror will no longer try to stabilize the scroll position during updates on browsers that support [scroll anchoring](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-anchor), since it'd inadvertently cancel the browser's behavior. Fix an issue in Safari where the editor would interrupt the composition spacebar menu because it incorrectly interpreted the mutation events fired by the browser as representing a replacement of the selection with identical text. Work around an issue where, on Safari, an IME composition started in an empty textblock would vanish when you press enter. ## 1.9.6 (2019-05-17) ### Bug fixes Fix bug in composition handling when the composition's parent node has an extra wrapper node around its content. ## 1.9.5 (2019-05-14) ### Bug fixes Fix regression in handling text editing events on IE11. ## 1.9.4 (2019-05-13) ### Bug fixes Fix a regression where all plugin views were recreated when calling [`setProps`](https://prosemirror.net/docs/ref/#view.EditorView.setProps). ## 1.9.3 (2019-05-10) ### Bug fixes Fix a bug where, if the document was changed at exactly the right moment, `handleClickOn` could be called with `null` as the node. ## 1.9.2 (2019-05-08) ### Bug fixes Fix a bug where updating to a reconfigured state would not recreate the view's plugin views. ## 1.9.1 (2019-05-04) ### Bug fixes Fix a regression where mouse selection would sometimes raise an error. ## 1.9.0 (2019-05-03) ### New features Changes made during compositions now immediately fire transactions on each update, rather than only a single one at the end of the composition. The view now immediately shows changes to the document or decorations during composition, even if they come from transactions not directly generated by the use's editing. The only exception is decorations that affect the focused text node—those are still delayed to avoid unneccesarily canceling the composition. ## 1.8.9 (2019-04-18) ### Bug fixes Improve display update times for nodes with thousands of children by fix an accidental piece of quadratic complexity. Fixes an issue where changes to the [`nodeViews` prop](https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews) weren't noticed when using [`updateState`](https://prosemirror.net/docs/ref/#view.EditorView.updateState) to update the view. Fix issue where sometimes moving the selection back its last position with the mouse failed to update ProseMirror's selection state. No longer call [`deselectNode`](https://prosemirror.net/docs/ref/#view.NodeView.deselectNode) on already-destroyed node views. ## 1.8.8 (2019-04-11) ### Bug fixes Fix a regression from 1.8.4 that made it return unreasonable rectangles for positions between blocks. ## 1.8.7 (2019-04-09) ### Bug fixes The [`handlePaste`](https://prosemirror.net/docs/ref/#view.EditorProps.handlePaste) prop is now activated even when the default parser can't make any sense of the clipboard content. ## 1.8.6 (2019-04-08) ### Bug fixes Fix a bug where decorations splitting a text node would sometimes confuse the display updater and make decorated nodes disappear. ## 1.8.5 (2019-04-08) ### Bug fixes Multiple [`transformPastedHTML`](https://prosemirror.net/docs/ref/#view.EditorProps.transformPastedHTML) props are now all properly called in order, rather than only the first one. Fixes an issue where invalid change positions were computed when a composition happened concurrently with a change that inserted content at the same position. ## 1.8.4 (2019-03-20) ### Bug fixes [`EditorView.coordsAtPos`](https://prosemirror.net/docs/ref/#view.EditorView.coordsAtPos) is now more accurate in right-to-left text on Chrome and Firefox. [`EditorView.coordsAtPos`](https://prosemirror.net/docs/ref/#view.EditorView.coordsAtPos) returns more accurate coordinates when querying the position directly after a line wrap point. Fix an issue where clicking directly in front of a node selection doesn't clear the node selection markup. ## 1.8.3 (2019-03-04) ### Bug fixes Fix an issue where clicking when there's a non-text selection active sometimes doesn't cause the appropriate new selection. ## 1.8.2 (2019-02-28) ### Bug fixes Fix an issue where a view state update happening between a change to the DOM selection and the corresponding browser event could disrupt mouse selection. ## 1.8.1 (2019-02-22) ### Bug fixes Fix infinite loop in `coordsAtPos`. ## 1.8.0 (2019-02-21) ### Bug fixes Fix a bug where [`endOfTextblock`](https://prosemirror.net/docs/ref/#view.EditorView.endOfTextblock) spuriously returns true when the cursor is in a mark. ### New features [`posAtCoords`](https://prosemirror.net/docs/ref/#view.EditorView.posAtCoords) will no longer return `null` when called with coordinates outside the browser's viewport. (It _will_ still return null for coordinates outside of the editor's bounding box.) ## 1.7.3 (2019-02-20) ### Bug fixes [`endOfTextblock`](https://prosemirror.net/docs/ref/#view.EditorView.endOfTextblock) now works on textblocks that are the editor's top-level node. ## 1.7.2 (2019-02-20) ### Bug fixes Pressing shift-left/right next to a selectable node no longer selects the node instead of creating a text selection across it. ## 1.7.1 (2019-02-04) ### Bug fixes Fix an issue on Safari where an Enter key events that was part of a composition is interpreted as stand-alone Enter press. ## 1.7.0 (2019-01-29) ### Bug fixes Fix an issue where node selections on uneditable nodes couldn't be copied or cut on Chrome. ### New features The editable view now recognizes the [`spanning`](https://prosemirror.net/docs/ref/#model.MarkSpec.spanning) mark property. ## 1.6.8 (2019-01-03) ### Bug fixes When replacing a selection by typing over it with a letter that matches its start or end, the editor now generates a step that covers the whole replacement. Fixes dragging a node when the mouse is in a child DOM element that doesn't represent a document node. Work around Chrome bug in selection management Fixes an issue in Chrome where clicking at the start of a textblock after a selected node would sometimes not move the cursor there. Fix issue where a node view's `getPos` callback could sometimes return `NaN`. Fix an issue where deleting more than 5 nodes might cause the nodes after that to be needlessly redrawn. ## 1.6.7 (2018-11-26) ### Bug fixes Avoids redrawing of content with marks when other content in front of it is deleted. ## 1.6.6 (2018-11-15) ### Bug fixes Work around a Chrome bug where programmatic changes near the cursor sometimes cause the visible and reported selection to disagree. Changing the `nodeView` prop will no longer leave outdated node views in the DOM. Work around an issue where Chrome unfocuses the editor or scrolls way down when pressing down arrow with the cursor between the start of a textblock and an uneditable element. Fix a bug where mapping decoration sets through changes that changed the structure of decorated subtrees sometimes produced corrupted output. ## 1.6.5 (2018-10-29) ### Bug fixes Work around Safari issue where deleting the last bit of text in a table cell creates weird HTML with a BR in a table row. ## 1.6.4 (2018-10-19) ### Bug fixes Fix pasting when both text and files are present on the clipboard. ## 1.6.3 (2018-10-12) ### Bug fixes The editor will no longer try to handle file paste events with the old-browser compatibility kludge (which might cause scrolling and focus flickering). ## 1.6.2 (2018-10-08) ### Bug fixes Fixes an issue where event handlers were leaked when destroying an editor ## 1.6.1 (2018-10-01) ### Bug fixes Fixes situation where a vertical [`endOfTextblock`](https://prosemirror.net/docs/ref/#view.EditorView.endOfTextblock) query could get confused by nearby widgets or complex parent node representation. ## 1.6.0 (2018-09-27) ### Bug fixes Fixes a corner case in which DecorationSet.map would map decorations to incorrect new positions. When the editor contains scrollable elements, scrolling the cursor into view also scrolls those. ### New features The `scrollMargin` and `scrollThreshold` props may now hold `{left, right, top, bottom}` objects to set different margins and thresholds for different sides. Make scrolling from a given start node more robust ## 1.5.3 (2018-09-24) ### Bug fixes The cursor is now scrolled into view after keyboard driven selection changes even when they were handled by the browser. ## 1.5.2 (2018-09-07) ### Bug fixes Improves selection management around widgets with no actual HTML content (possibly drawn using CSS pseudo elements). Fix extra whitespace in pasted HTML caused by previously-collapsed spacing. Slow triple-clicks are no longer treated as two double-clicks in a row. ## 1.5.1 (2018-08-24) ### Bug fixes Fix issue where some DOM selections would cause a non-editable view to crash when reading the selection. ## 1.5.0 (2018-08-21) ### New features Mark views are now passed a boolean that indicates whether the mark's content is inline as third argument. ## 1.4.4 (2018-08-13) ### Bug fixes Fix an issue where a non-empty DOM selection could stick around even though the state's selection is empty. Fix an issue where Firefox would create an extra cursor position when arrow-keying through a widget. ## 1.4.3 (2018-08-12) ### Bug fixes Fix an issue where the editor got stuck believing shift was down (and hence pasting as plain text) when it was unfocused with shift held down. ## 1.4.2 (2018-08-03) ### Bug fixes Fix an issue where reading the selection from the DOM might crash in non-editable mode. ## 1.4.1 (2018-08-02) ### Bug fixes Fixes an issue where backspacing out the last character between the start of a textblock and a widget in Chrome would insert a random hard break. ## 1.4.0 (2018-07-26) ### New features The `dispatchTransaction` prop is now called with `this` bound to the editor view. ## 1.3.8 (2018-07-24) ### Bug fixes Fix an issue where Chrome Android would move the cursor forward by one after backspace-joining two paragraphs. ## 1.3.7 (2018-07-02) ### Bug fixes Fix a crash when scrolling things into view when the editor isn't a child of `document.body`. ## 1.3.6 (2018-06-21) ### Bug fixes Make sure Safari version detection for clipboard support also works in iOS webview. ## 1.3.5 (2018-06-20) ### Bug fixes Use shared implementation of [`dropPoint`](https://prosemirror.net/docs/ref/#transform.dropPoint) to handle finding a drop position. ## 1.3.4 (2018-06-20) ### Bug fixes Enable use of browser clipboard API on Mobile Safari version 11 and up, which makes cut work on that platform and should generally improve clipboard handling. ## 1.3.3 (2018-06-15) ### Bug fixes Fix arrow-left cursor motion from cursor wrapper (for example after a link). Fix selection glitches when shift-selecting around widget decorations. Fix issue where a parsing a code block from the editor DOM might drop newlines in the code. ## 1.3.2 (2018-06-15) ### Bug fixes [`handleKeyDown`](https://prosemirror.net/docs/ref/#view.EditorProps.handleKeyDown) will now get notified of key events happening directly after a composition ends. ## 1.3.1 (2018-06-08) ### Bug fixes The package can now be loaded in a web worker context (where `navigator` is defined but `document` isn't) without crashing. Dropping something like a list item into a textblock will no longer split the textblock. ## 1.3.0 (2018-04-24) ### Bug fixes Fix mouse-selecting (in IE and Edge) from the end of links and other positions that cause a cursor wrapper. [Widget decorations](https://prosemirror.net/docs/ref/#view.Decoration^widget) with the same [key](https://prosemirror.net/docs/ref/#view.Decoration^widget^spec.key) are now considered equivalent, even if their other spec fields differ. ### New features The new [`EditorView.posAtDOM` method](https://prosemirror.net/docs/ref/#view.EditorView.posAtDOM) can be used to find the document position corresponding to a given DOM position. The new [`EditorView.nodeDOM` method](https://prosemirror.net/docs/ref/#view.EditorView.nodeDOM) gives you the DOM node that is used to represent a specific node in the document. [`Decoration.widget`](https://prosemirror.net/docs/ref/#view.Decoration^widget) now accepts a function as second argument, which can be used to delay rendering of the widget until the document is drawn (at which point a reference to the view is available). The `getPos` function passed to a [node view constructor](https://prosemirror.net/docs/ref/#view.editorProps.nodeViews) can now be called immediately (it used to return undefined until rendering had finished). The function used to render a [widget](https://prosemirror.net/docs/ref/#view.Decoration^widget) is now passed a `getPos` method that event handlers can use to figure out where in the DOM the widget is. ## 1.2.0 (2018-03-14) ### Bug fixes Fix a problem where updating the state of a non-editable view would not set the selection, causing problems when the DOM was updated in a way that disrupted the DOM selection. Fix an issue where, on IE and Chrome, starting a drag selection in a position that required a cursor wrapper (on a mark boundary) would sometimes fail to work. Fix crash in key handling when the editor is focused but there is no DOM selection. Fixes a bug that prevented decorations inside node views with a [`contentDOM` property](https://prosemirror.net/docs/ref/#view.NodeView.contentDOM) from being drawn. Fixes an issue where, on Firefox, depending on a race condition, the skipping over insignificant DOM nodes done at keypress was canceled again before the keypress took effect. Fixes an issue where an `:after` pseudo-element on a non-inclusive mark could block the cursor, making it impossible to arrow past it. ### New features The DOM structure for marks is no longer constrained to a single node. [Mark views](https://prosemirror.net/docs/ref/#view.NodeView) can have a `contentDOM` property, and [mark spec](https://prosemirror.net/docs/ref/#model.MarkSpec) `toDOM` methods can return structures with holes. [Widget decorations](https://prosemirror.net/docs/ref/#view.Decoration^widget) are now wrapped in the marks of the node after them when their [`side` option](https://prosemirror.net/docs/ref/#view.Decoration^widget^spec.side) is >= 0. [Widget decorations](https://prosemirror.net/docs/ref/#view.Decoration^widget) may now specify a [`marks` option](https://prosemirror.net/docs/ref/#view.Decoration^widget^spec.marks) to set the precise set of marks they should be wrapped in. ## 1.1.1 (2018-03-01) ### Bug fixes Fixes typo that broke paste. ## 1.1.0 (2018-02-28) ### Bug fixes Fixes issue where dragging a draggable node directly below a selected node would move the old selection rather than the target node. A drop that can't fit the dropped content will no longer dispatch an empty transaction. ### New features Transactions generated for drop now have a `"uiEvent"` metadata field holding `"drop"`. Paste and cut transactions get that field set to `"paste"` or `"cut"`. ## 1.0.11 (2018-02-16) ### Bug fixes Fix issue where the cursor was visible when a node was selected on recent Chrome versions. ## 1.0.10 (2018-01-24) ### Bug fixes Improve preservation of open and closed nodes in slices taken from the clipboard. ## 1.0.9 (2018-01-17) ### Bug fixes Work around a Chrome cursor motion bug by making sure
    nodes don't get a contenteditable=false attribute. ## 1.0.8 (2018-01-09) ### Bug fixes Fix issue where [`Decoration.map`](https://prosemirror.net/docs/ref/#view.DecorationSet.map) would in some situations with nested nodes incorrectly map decoration positions. ## 1.0.7 (2018-01-05) ### Bug fixes Pasting from an external source no longer opens isolating nodes like table cells. ## 1.0.6 (2017-12-26) ### Bug fixes [`DecorationSet.remove`](https://prosemirror.net/docs/ref/#view.DecorationSet.remove) now uses a proper deep compare to determine if widgets are the same (it used to compare by identity). ## 1.0.5 (2017-12-05) ### Bug fixes Fix an issue where deeply nested decorations were mapped incorrectly in corner cases. ## 1.0.4 (2017-11-27) ### Bug fixes Fix a corner-case crash during drop. ## 1.0.3 (2017-11-23) ### Bug fixes Pressing backspace between two identical characters will no longer generate a transaction that deletes the second one. ## 1.0.2 (2017-11-20) ### Bug fixes Fix test for whether a node can be selected when arrowing onto it from the right. Calling [`posAtCoords`](https://prosemirror.net/docs/ref/#view.EditorView.posAtCoords) while a read from the DOM is pending will no longer return a malformed result. ## 1.0.1 (2017-11-10) ### Bug fixes Deleting the last character in a list item no longer results in a spurious hard_break node on Safari. Fixes a crash on IE11 when starting to drag. ## 1.0.0 (2017-10-13) ### Bug fixes Dragging nodes with a node view that handles its own mouse events should work better now. List item DOM nodes are no longer assigned `pointer-events: none` in the default style. Ctrl-clicking list markers now properly selects the list item again. Arrow-down through an empty textblock no longer causes the browser to forget the cursor's horizontal position. Copy-dragging on OS X is now done by holding option, rather than control, following the convention on that system. Fixes a crash related to decoration management. Fixes a problem where using cut on IE11 wouldn't actually remove the selected text. Copy/paste on Edge 15 and up now uses the clipboard API, fixing a problem that made them fail entirely. ### New features The [`dragging`](https://prosemirror.net/docs/ref/#view.EditorView.dragging) property of a view, which contains information about editor content being dragged, is now part of the public interface. ## 0.24.0 (2017-09-25) ### New features The [`clipboardTextParser`](https://prosemirror.net/docs/ref/version/0.24.0.html#view.EditorProps.clipboardTextParser) prop is now passed a context position. ## 0.23.0 (2017-09-13) ### Breaking changes The `onFocus`, `onBlur`, and `handleContextMenu` props are no longer supported. You can achieve their effect with the [`handleDOMEvents`](https://prosemirror.net/docs/ref/version/0.23.0.html#view.EditorProps.handleDOMEvents) prop. ### Bug fixes Fixes occasional crash when reading the selection in Firefox. Putting a table cell on the clipboard now properly wraps it in a table. The view will no longer scroll into view when receiving a state that isn't derived from its previous state. ### New features Transactions caused by a paste now have their "paste" meta property set to true. Adds a new view prop, [`handleScrollToSelection`](https://prosemirror.net/docs/ref/version/0.23.0.html#view.EditorProps.handleScrollToSelection) to override the behavior of scrolling the selection into view. The new editor prop [`clipboardTextSerializer`](https://prosemirror.net/docs/ref/version/0.23.0.html#view.EditorProps.clipboardTextSerializer) allows you to override the way a piece of document is converted to clipboard text. Adds the editor prop [`clipboardTextParser`](https://prosemirror.net/docs/ref/version/0.23.0.html#view.EditorProps.clipboardTextParser), which can be used to define your own parsing strategy for clipboard text content. [`DecorationSet.find`](https://prosemirror.net/docs/ref/version/0.23.0.html#view.DecorationSet.find) now supports passing a predicate to filter decorations by spec. ## 0.22.1 (2017-08-16) ### Bug fixes Invisible selections that don't cover any content (i.e., a cursor) are now properly hidden. Initializing the editor view non-editable no longer causes a crash. ## 0.22.0 (2017-06-29) ### Bug fixes Fix an issue where moving the cursor through a text widget causes the editor to lose the selection in Chrome. Fixes an issue where down-arrow in front of a widget would sometimes not cause any cursor motion on Chrome. [Destroying](https://prosemirror.net/docs/ref/version/0.22.0.html#view.EditorView.destroy) a [mounted](https://prosemirror.net/docs/ref/version/0.22.0.html#view.EditorView.constructor) editor view no longer leaks event handlers. Display updates for regular, non-composition input are now synchronous, which should reduce flickering when, for example, updating decorations in response to typing. ### New features The editor can now be initialized in a document other than the global document (say, an `iframe`). Editor views now have a [`domAtPos` method](https://prosemirror.net/docs/ref/version/0.22.0.html#view.EditorView.domAtPos), which gives you the DOM position corresponding to a given document position. ## 0.21.1 (2017-05-09) ### Bug fixes Copying and pasting table cells on Edge no longer strips the table structure. ## 0.21.0 (2017-05-03) ### Breaking changes The `associative` option to widget decorations is no longer supported. To make a widget left-associative, set its `side` option to a negative number. `associative` will continue to work with a warning until the next release. ### New features [Widget decorations](https://prosemirror.net/docs/ref/version/0.21.0.html#view.Decoration^widget) now support a `side` option that controls which side of them the cursor is drawn, where they move when content is inserted at their position, and the order in which they appear relative to other widgets at the same position. ## 0.20.5 (2017-05-02) ### Bug fixes Fixes an issue where the DOM selection could be shown on the wrong side of hard break or image nodes. ## 0.20.4 (2017-04-24) ### Bug fixes Fix a bug that prevented the DOM selection from being updated when the new position was near the old one in some circumstances. Stop interfering with alt-d keypresses on OS X. Fix issue where reading a DOM change in a previously empty node could crash. Fixes crash when reading a change that removed a decorated text node from the DOM. ## 0.20.3 (2017-04-12) ### Bug fixes Shift-pasting and pasting into a code block now does the right thing on IE and Edge. ## 0.20.2 (2017-04-05) ### Bug fixes Fixes a bug that broke dragging from the editor. ## 0.20.1 (2017-04-04) ### Bug fixes Typing in code blocks no longer replaces newlines with spaces. Copy and paste on Internet Explorer, Edge, and mobile Safari should now behave more like it does on other browsers. Handlers are called, and the changes to the document are made by ProseMirror's code, not the browser. Fixes a problem where triple-clicking the editor would sometimes cause the scroll position to inexplicably jump around on IE11. ## 0.20.0 (2017-04-03) ### Breaking changes The `inclusiveLeft` and `inclusiveRight` options to inline decorations were renamed to [`inclusiveStart`](https://prosemirror.net/docs/ref/version/0.20.0.html#view.Decoration^inline^spec.inclusiveStart) and [`inclusiveEnd`](https://prosemirror.net/docs/ref/version/0.20.0.html#view.Decoration^inline^spec.inclusiveEnd) so that they also make sense in right-to-left text. The old names work with a warning until the next release. The default styling for lists and blockquotes was removed from `prosemirror.css`. (They were moved to the [`example-setup`](https://github.com/ProseMirror/prosemirror-example-setup) module.) ### Bug fixes Fixes reading of selection in Chrome in a shadow DOM. Registering DOM event handlers that the editor doesn't listen to by default with the `handleDOMEvents` prop should work again. Backspacing after turning off a mark now works again in Firefox. ### New features The new props [`handlePaste`](https://prosemirror.net/docs/ref/version/0.20.0.html#view.EditorProps.handlePaste) and [`handleDrop`](https://prosemirror.net/docs/ref/version/0.20.0.html#view.EditorProps.handleDrop) can be used to override drop and paste behavior. ## 0.19.1 (2017-03-18) ### Bug fixes Fixes a number of issues with characters being duplicated or disappearing when typing on mark boundaries. ## 0.19.0 (2017-03-16) ### Breaking changes [`endOfTextblock`](https://prosemirror.net/docs/ref/version/0.19.0.html#view.EditorView.endOfTextblock) no longer always returns false for horizontal motion on non-cursor selections, but checks the position of the selection head instead. ### Bug fixes Typing after adding/removing a mark no longer briefly shows the new text with the wrong marks. [`posAtCoords`](https://prosemirror.net/docs/ref/version/0.19.0.html#view.EditorView.posAtCoords) is now more reliable on modern browsers by using browser APIs. Fix a bug where the view would in some circumstances leave superfluous DOM nodes around inside marks. ### New features You can now override the selection the editor creates for a given DOM selection with the [`createSelectionBetween`](https://prosemirror.net/docs/ref/version/0.19.0.html#view.EditorProps.createSelectionBetween) prop. ## 0.18.0 (2017-02-24) ### Breaking changes `Decoration` objects now store their definition object under [`spec`](https://prosemirror.net/docs/ref/version/0.18.0.html#Decoration.spec), not `options`. The old property name still works, with a warning, until the next release. ### Bug fixes Fix bug where calling [`focus`](https://prosemirror.net/docs/ref/version/0.18.0.html#view.EditorView.focus) when there was a text selection would sometimes result in `state.selection` receiving an incorrect value. [`EditorView.props`](https://prosemirror.net/docs/ref/version/0.18.0.html#view.EditorView.props) now has its `state` property updated when you call `updateState`. Putting decorations on or inside a node view with an `update` method now works. ### New features [Plugin view](https://prosemirror.net/docs/ref/version/0.18.0.html#state.PluginSpec.view) update methods are now passed the view's previous state as second argument. The `place` agument to the [`EditorView` constructor](https://prosemirror.net/docs/ref/version/0.18.0.html#view.EditorView) can now be an object with a `mount` property to directly provide the node that should be made editable. The new [`EditorView.setProps` method](https://prosemirror.net/docs/ref/version/0.18.0.html#view.EditorView.setProps) makes it easier to update individual props. ## 0.17.7 (2017-02-08) ### Bug fixes Fixes crash in the code that maintains the scroll position when the document is empty or hidden. ## 0.17.6 (2017-02-08) ### Bug fixes Transactions that shouldn't [scroll the selection into view](https://prosemirror.net/docs/ref/version/0.17.0.html#state.transaction.scrollIntoView) now no longer do so. ## 0.17.4 (2017-02-02) ### Bug fixes Fixes bug where widget decorations would sometimes get parsed as content when editing near them. The editor now prevents the behavior of Ctrl-d and Ctrl-h on textblock boundaries on OS X, as intended. Make sure long words don't cause a horizontal scrollbar in Firefox Various behavior fixes for IE11. ## 0.17.3 (2017-01-19) ### Bug fixes DOM changes deleting a node's inner wrapping DOM element (for example the `` tag in a schema-basic code block) no longer break the editor. ## 0.17.2 (2017-01-16) ### Bug fixes Call custom click handlers before applying select-node behavior for a ctrl/cmd-click. Fix failure to apply DOM changes that start at document position 0. ## 0.17.1 (2017-01-07) ### Bug fixes Fix issue where a document update that left the selection in the same place sometimes led to an incorrect DOM selection. Make sure [`EditorView.focus`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorView.focus) doesn't cause the browser to scroll the top of the editor into view. ## 0.17.0 (2017-01-05) ### Breaking changes The `handleDOMEvent` prop has been dropped in favor of the [`handleDOMEvents`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorProps.handleDOMEvents) (plural) prop. The `onChange` prop has been replaced by a [`dispatchTransaction`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorProps.dispatchTransaction) prop (which takes a transaction instead of an action). ### New features Added support for a [`handleDOMEvents` prop](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorProps.handleDOMEvents), which allows you to provide handler functions per DOM event, and works even for events that the editor doesn't normally add a handler for. Add view method [`dispatch`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorView.dispatch), which provides a convenient way to dispatch transactions. The [`dispatchTransaction`](https://prosemirror.net/docs/ref/version/0.17.0.html#view.EditorProps.dispatchTransaction) (used to be `onAction`) prop is now optional, and will default to simply applying the transaction to the current view state. [Widget decorations](https://prosemirror.net/docs/ref/version/0.17.0.html#view.Decoration.widget) now accept an option `associative` which can be used to configure on which side of content inserted at their position they end up. Typing immediately after deleting text now preserves the marks of the deleted text. Transactions that update the selection because of mouse or touch input now get a metadata property `pointer` with the value `true`. ## 0.16.0 (2016-12-23) ### Bug fixes Solve problem where setting a node selection would trigger a DOM read, leading to the selection being reset. ## 0.16.0 (2016-12-23) ### Breaking changes The `spellcheck`, `label`, and `class` props are now replaced by an [`attributes` prop](https://prosemirror.net/docs/ref/version/0.16.0.html#view.EditorProps.attributes). ### Bug fixes Ignoring/aborting an action should no longer lead to the DOM being stuck in an outdated state. Typing at the end of a textblock which ends in a non-text node now actually works. DOM nodes for leaf document nodes are now set as non-editable to prevent various issues such as stray cursors inside of them and Firefox adding image resize controls. Inserting a node no longer causes nodes of the same type after it to be neednessly redrawn. ### New features Add a new editor prop [`editable`](https://prosemirror.net/docs/ref/version/0.16.0.html#view.EditorProps.editable) which controls whether the editor's `contentEditable` behavior is enabled. Plugins and props can now set any DOM attribute on the outer editor node using the [`attributes` prop](https://prosemirror.net/docs/ref/version/0.16.0.html#view.EditorProps.attributes). Node view constructors and update methods now have access to the node's wrapping decorations, which can be used to pass information to a node view without encoding it in the document. Attributes added or removed by node and inline [decorations](https://prosemirror.net/docs/ref/version/0.16.0.html#view.Decoration) no longer cause the nodes inside of them to be fully redrawn, making node views more stable and allowing CSS transitions to be used. ## 0.15.2 (2016-12-10) ### Bug fixes The native selection is now appropriately hidden when there is a node selection. ## 0.15.1 (2016-12-10) ### Bug fixes Fix DOM parsing for decorated text nodes. ## 0.15.0 (2016-12-10) ### Breaking changes The editor view no longer wraps its editable DOM element in a wrapper element. The `ProseMirror` CSS class now applies directly to the editable element. The `ProseMirror-content` CSS class is still present for ease of upgrading but will be dropped in the next release. The editor view no longer draws a drop cursor when dragging content over the editor. The new [`prosemirror-dropcursor`](https://github.com/prosemirror/prosemirror-dropcursor) module implements this as a plugin. ### Bug fixes Simple typing and backspacing now gets handled by the browser without ProseMirror redrawing the touched nodes, making spell-checking and various platform-specific input tricks (long-press on OS X, double space on iOS) work in the editor. Improve tracking of DOM nodes that have been touched by user changes, so that [`updateState`](https://prosemirror.net/docs/ref/version/0.15.0.html#view.EditorView.updateState) can reliably fix them. Changes to the document that happen while dragging editor content no longer break moving of the content. Adding or removing a mark directly in the DOM (for example with the bold/italic buttons in iOS' context menu) now produces mark steps, rather than replace steps. Pressing backspace at the start of a paragraph on Android now allows key handlers for backspace to fire. Toggling a mark when there is no selection now works better on mobile platforms. ### New features Introduces an [`endOfTextblock`](https://prosemirror.net/docs/ref/version/0.15.0.html#view.EditorView.endOfTextblock) method on views, which can be used to find out in a bidi- and layout-aware way whether the selection is on the edge of a textblock. ## 0.14.4 (2016-12-02) ### Bug fixes Fix issue where node decorations would stick around in the DOM after the decoration was removed. Setting or removing a node selection in an unfocused editor now properly updates the DOM to show that selection. ## 0.14.2 (2016-11-30) ### Bug fixes FIX: Avoid unneeded selection resets which sometimes confused browsers. ## 0.14.2 (2016-11-29) ### Bug fixes Fix a bug where inverted selections weren't created in the DOM correctly. ## 0.14.1 (2016-11-29) ### Bug fixes Restores previously broken kludge that allows the cursor to appear after non-text content at the end of a line. ## 0.14.0 (2016-11-28) ### Breaking changes Wrapping decorations are now created using the [`nodeName`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.DecorationAttrs.nodeName) property. The `wrapper` property is no longer supported. The `onUnmountDOM` prop is no longer supported (use a node view with a [`destroy`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.NodeView.destroy) method instead). The `domSerializer` prop is no longer supported. Use [node views](https://prosemirror.net/docs/ref/version/0.14.0.html#view.EditorProps.nodeViews) to configure editor-specific node representations. ### New features Widget decorations can now be given a [`key`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.Decoration.widget^options.key) property to prevent unneccesary redraws. The `EditorView` class now has a [`destroy`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.EditorView.destroy) method for cleaning up. The [`handleClickOn`](https://prosemirror.net/docs/ref/version/0.14.0.html#view.EditorProps.handleClickOn) prop and friends now receive a `direct` boolean argument that indicates whether the node was clicked directly. [Widget decorations](https://prosemirror.net/docs/ref/version/0.14.0.html#view.Decoration^widget) now support a `stopEvent` option that can be used to control which DOM events that pass through them should be ignored by the editor view. You can now [specify](https://prosemirror.net/docs/ref/version/0.14.0.html#view.EditorProps.nodeViews) custom [node views](https://prosemirror.net/docs/ref/version/0.14.0.html#view.NodeView) for an editor view, which give you control over the way node of a given type are represented in the DOM. See the related [RFC](https://discuss.prosemirror.net/t/rfc-node-views-to-manage-the-representation-of-nodes/463). ## 0.13.2 (2016-11-15) ### Bug fixes Fixes an issue where widget decorations in the middle of text nodes would sometimes disappear. ## 0.13.1 (2016-11-15) ### Bug fixes Fixes event handler crash (and subsequent bad default behavior) when pasting some types of external HTML into an editor. ## 0.13.0 (2016-11-11) ### Breaking changes Selecting nodes on OS X is now done with cmd-leftclick rather than ctrl-leftclick. ### Bug fixes Pasting text into a code block will now insert the raw text. Widget decorations at the start or end of a textblock no longer block horizontal cursor motion through them. Widget nodes at the end of textblocks are now reliably drawn during display updates. ### New features [`DecorationSet.map`](https://prosemirror.net/docs/ref/version/0.13.0.html#view.DecorationSet.map) now takes an options object which allows you to specify an `onRemove` callback to be notified when remapping drops decorations. The [`transformPastedHTML`](https://prosemirror.net/docs/ref/version/0.13.0.html#view.EditorProps.transformPastedHTML) and [`transformPastedText`](https://prosemirror.net/docs/ref/version/0.13.0.html#view.EditorProps.transformPastedText) props were (re-)added, and can be used to clean up pasted content. ## 0.12.2 (2016-11-02) ### Bug fixes Inline decorations that span across an empty textblock no longer crash the display drawing code. ## 0.12.1 (2016-11-01) ### Bug fixes Use a separate document to parse pasted HTML to better protect against cross-site scripting attacks. Specifying multiple classes in a decoration now actually works. Ignore empty inline decorations when building a decoration set. ## 0.12.0 (2016-10-21) ### Breaking changes The return value of [`EditorView.posAtCoords`](https://prosemirror.net/docs/ref/version/0.12.0.html#view.EditorView.posAtCoords) changed to contain an `inside` property pointing at the innermost node that the coordinates are inside of. (Note that the docs for this method were wrong in the previous release.) ### Bug fixes Reduce reliance on shift-state tracking to minimize damage when it gets out of sync. Fix bug that'd produce bogus document positions for DOM positions inside non-document nodes. Don't treat fast ctrl-clicks as double or triple clicks. ### New features Implement [decorations](https://prosemirror.net/docs/ref/version/0.12.0.html#view.Decoration), a way to influence the way the document is drawn. Add the [`decorations` prop](https://prosemirror.net/docs/ref/version/0.12.0.html#view.EditorProps.decorations) to specify them. ## 0.11.2 (2016-10-04) ### Bug fixes Pass actual event object to [`handleDOMEvent`](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps.handleDOMEvent), rather than just its name. Fix display corruption caused by using the wrong state as previous version during IME. ## 0.11.0 (2016-09-21) ### Breaking changes Moved into a separate module from the old `edit` submodule. Completely new approach to managing the editor's DOM representation and input. Event handlers and options are now replaced by [props](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps). The view's state is now 'shallow', represented entirely by a set of props, one of which holds an editor state value from the [state](https://prosemirror.net/docs/ref/version/0.11.0.html#state) module. When the user interacts with the editor, it will pass an [action](https://prosemirror.net/docs/ref/version/0.11.0.html#state.Action) to its [`onAction`](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps.onAction) prop, which is responsible for triggering an view update. The `markRange` system was dropped, to be replaced in the next release by a 'decoration' system. There is no keymap support in the view module anymore. Use a [keymap](https://prosemirror.net/docs/ref/version/0.11.0.html#keymap) plugin for that. The undo [history](https://prosemirror.net/docs/ref/version/0.11.0.html#history) is now a separate plugin. CSS needed by the editor is no longer injected implicitly into the page. Instead, you should arrange for the `style/prosemirror.css` file to be loaded into your page. ### New features The DOM [parser](https://prosemirror.net/docs/ref/version/0.11.0.html#model.DOMParser) and [serializer](https://prosemirror.net/docs/ref/version/0.11.0.html#model.DOMSerializer) used to interact with the visible DOM and the clipboard can now be customized through [props](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps). You can now provide a catch-all DOM [event handler](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps.handleDOMEvent) to get a first chance at handling DOM events. The [`onUnmountDOM`](https://prosemirror.net/docs/ref/version/0.11.0.html#view.EditorProps.onUnmountDOM) can be used to be notified when a piece of the document DOM is thrown away (in case cleanup is needed). prosemirror-view-1.23.13/CONTRIBUTING.md000066400000000000000000000075161423202571000174520ustar00rootroot00000000000000# How to contribute - [Getting help](#getting-help) - [Submitting bug reports](#submitting-bug-reports) - [Contributing code](#contributing-code) ## Getting help Community discussion, questions, and informal bug reporting is done on the [discuss.ProseMirror forum](http://discuss.prosemirror.net). ## Submitting bug reports Report bugs on the [GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues). Before reporting a bug, please read these pointers. - The issue tracker is for *bugs*, not requests for help. Questions should be asked on the [forum](http://discuss.prosemirror.net). - Include information about the version of the code that exhibits the problem. For browser-related issues, include the browser and browser version on which the problem occurred. - Mention very precisely what went wrong. "X is broken" is not a good bug report. What did you expect to happen? What happened instead? Describe the exact steps a maintainer has to take to make the problem occur. A screencast can be useful, but is no substitute for a textual description. - A great way to make it easy to reproduce your problem, if it can not be trivially reproduced on the website demos, is to submit a script that triggers the issue. ## Contributing code If you want to make a change that involves a significant overhaul of the code or introduces a user-visible new feature, create an [RFC](https://github.com/ProseMirror/rfcs/) first with your proposal. - Make sure you have a [GitHub Account](https://github.com/signup/free) - Fork the relevant repository ([how to fork a repo](https://help.github.com/articles/fork-a-repo)) - Create a local checkout of the code. You can use the [main repository](https://github.com/prosemirror/prosemirror) to easily check out all core modules. - Make your changes, and commit them - Follow the code style of the rest of the project (see below). Run `npm run lint` (in the main repository checkout) to make sure that the linter is happy. - If your changes are easy to test or likely to regress, add tests in the relevant `test/` directory. Either put them in an existing `test-*.js` file, if they fit there, or add a new file. - Make sure all tests pass. Run `npm run test` to verify tests pass (you will need Node.js v6+). - Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)). Don't put more than one feature/fix in a single pull request. By contributing code to ProseMirror you - Agree to license the contributed code under the project's [MIT license](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE). - Confirm that you have the right to contribute and license the code in question. (Either you hold all rights on the code, or the rights holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct agreement with you.) ### Coding standards - ES6 syntax, targeting an ES5 runtime (i.e. don't use library elements added by ES6, don't use ES7/ES.next syntax). - 2 spaces per indentation level, no tabs. - No semicolons except when necessary. - Follow the surrounding code when it comes to spacing, brace placement, etc. - Brace-less single-statement bodies are encouraged (whenever they don't impact readability). - [getdocs](https://github.com/marijnh/getdocs)-style doc comments above items that are part of the public API. - When documenting non-public items, you can put the type after a single colon, so that getdocs doesn't pick it up and add it to the API reference. - The linter (`npm run lint`) complains about unused variables and functions. Prefix their names with an underscore to muffle it. - ProseMirror does *not* follow JSHint or JSLint prescribed style. Patches that try to 'fix' code to pass one of these linters will not be accepted. prosemirror-view-1.23.13/LICENSE000066400000000000000000000021131423202571000162120ustar00rootroot00000000000000Copyright (C) 2015-2017 by Marijn Haverbeke and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. prosemirror-view-1.23.13/README.md000066400000000000000000000026371423202571000164770ustar00rootroot00000000000000# prosemirror-view [ [**WEBSITE**](https://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**GITTER**](https://gitter.im/ProseMirror/prosemirror) | [**CHANGELOG**](https://github.com/ProseMirror/prosemirror-view/blob/master/CHANGELOG.md) ] This is a [core module](https://prosemirror.net/docs/ref/#view) of [ProseMirror](https://prosemirror.net). ProseMirror is a well-behaved rich semantic content editor based on contentEditable, with support for collaborative editing and custom document schemas. This [module](https://prosemirror.net/docs/ref/#view) exports the editor view, which renders the current document in the browser, and handles user events. The [project page](https://prosemirror.net) has more information, a number of [examples](https://prosemirror.net/examples/) and the [documentation](https://prosemirror.net/docs/). This code is released under an [MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE). There's a [forum](http://discuss.prosemirror.net) for general discussion and support requests, and the [Github bug tracker](https://github.com/prosemirror/prosemirror/issues) is the place to report issues. We aim to be an inclusive, welcoming community. To make that explicit, we have a [code of conduct](http://contributor-covenant.org/version/1/1/0/) that applies to communication around the project. prosemirror-view-1.23.13/package.json000066400000000000000000000020241423202571000174740ustar00rootroot00000000000000{ "name": "prosemirror-view", "version": "1.23.13", "description": "ProseMirror's view component", "main": "dist/index.js", "module": "dist/index.es.js", "style": "style/prosemirror.css", "license": "MIT", "maintainers": [ { "name": "Marijn Haverbeke", "email": "marijnh@gmail.com", "web": "http://marijnhaverbeke.nl" } ], "repository": { "type": "git", "url": "git://github.com/prosemirror/prosemirror-view.git" }, "dependencies": { "prosemirror-model": "^1.16.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" }, "devDependencies": { "ist": "^1.0.0", "mocha": "^9.1.2", "moduleserve": "^0.7.0", "prosemirror-test-builder": "^1.0.0", "rollup": "^2.26.3", "@rollup/plugin-buble": "^0.21.3" }, "scripts": { "test": "FIXME autorun browser tests", "test-server": "node test/link-mocha-css.js && moduleserve test --port 8090", "build": "rollup -c", "watch": "rollup -c -w", "prepare": "npm run build" } } prosemirror-view-1.23.13/rollup.config.js000066400000000000000000000005121423202571000203250ustar00rootroot00000000000000module.exports = { input: './src/index.js', output: [{ file: 'dist/index.js', format: 'cjs', sourcemap: true }, { file: 'dist/index.es.js', format: 'es', sourcemap: true }], plugins: [require('@rollup/plugin-buble')()], external(id) { return id[0] != "." && !require("path").isAbsolute(id) } } prosemirror-view-1.23.13/src/000077500000000000000000000000001423202571000157775ustar00rootroot00000000000000prosemirror-view-1.23.13/src/README.md000066400000000000000000000007211423202571000172560ustar00rootroot00000000000000ProseMirror's view module displays a given [editor state](#state.EditorState) in the DOM, and handles user events. Make sure you load `style/prosemirror.css` as a stylesheet when using this module. @EditorView ### Props @EditorProps @DirectEditorProps @NodeView ### Decorations Decorations make it possible to influence the way the document is drawn, without actually changing the document. @Decoration @DecorationAttrs @DecorationSet @DecorationSource prosemirror-view-1.23.13/src/browser.js000066400000000000000000000024461423202571000200260ustar00rootroot00000000000000const result = {} export default result if (typeof navigator != "undefined" && typeof document != "undefined") { const ie_edge = /Edge\/(\d+)/.exec(navigator.userAgent) const ie_upto10 = /MSIE \d/.test(navigator.userAgent) const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent) let ie = result.ie = !!(ie_upto10 || ie_11up || ie_edge) result.ie_version = ie_upto10 ? document.documentMode || 6 : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : null result.gecko = !ie && /gecko\/(\d+)/i.test(navigator.userAgent) result.gecko_version = result.gecko && +(/Firefox\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] let chrome = !ie && /Chrome\/(\d+)/.exec(navigator.userAgent) result.chrome = !!chrome result.chrome_version = chrome && +chrome[1] // Is true for both iOS and iPadOS for convenience result.safari = !ie && /Apple Computer/.test(navigator.vendor) result.ios = result.safari && (/Mobile\/\w+/.test(navigator.userAgent) || navigator.maxTouchPoints > 2) result.mac = result.ios || /Mac/.test(navigator.platform) result.android = /Android \d/.test(navigator.userAgent) result.webkit = "webkitFontSmoothing" in document.documentElement.style result.webkit_version = result.webkit && +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] } prosemirror-view-1.23.13/src/capturekeys.js000066400000000000000000000232501423202571000206760ustar00rootroot00000000000000import {Selection, NodeSelection, TextSelection, AllSelection} from "prosemirror-state" import browser from "./browser" import {domIndex, selectionCollapsed} from "./dom" import {selectionToDOM} from "./selection" function moveSelectionBlock(state, dir) { let {$anchor, $head} = state.selection let $side = dir > 0 ? $anchor.max($head) : $anchor.min($head) let $start = !$side.parent.inlineContent ? $side : $side.depth ? state.doc.resolve(dir > 0 ? $side.after() : $side.before()) : null return $start && Selection.findFrom($start, dir) } function apply(view, sel) { view.dispatch(view.state.tr.setSelection(sel).scrollIntoView()) return true } function selectHorizontally(view, dir, mods) { let sel = view.state.selection if (sel instanceof TextSelection) { if (!sel.empty || mods.indexOf("s") > -1) { return false } else if (view.endOfTextblock(dir > 0 ? "right" : "left")) { let next = moveSelectionBlock(view.state, dir) if (next && (next instanceof NodeSelection)) return apply(view, next) return false } else if (!(browser.mac && mods.indexOf("m") > -1)) { let $head = sel.$head, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter, desc if (!node || node.isText) return false let nodePos = dir < 0 ? $head.pos - node.nodeSize : $head.pos if (!(node.isAtom || (desc = view.docView.descAt(nodePos)) && !desc.contentDOM)) return false if (NodeSelection.isSelectable(node)) { return apply(view, new NodeSelection(dir < 0 ? view.state.doc.resolve($head.pos - node.nodeSize) : $head)) } else if (browser.webkit) { // Chrome and Safari will introduce extra pointless cursor // positions around inline uneditable nodes, so we have to // take over and move the cursor past them (#937) return apply(view, new TextSelection(view.state.doc.resolve(dir < 0 ? nodePos : nodePos + node.nodeSize))) } else { return false } } } else if (sel instanceof NodeSelection && sel.node.isInline) { return apply(view, new TextSelection(dir > 0 ? sel.$to : sel.$from)) } else { let next = moveSelectionBlock(view.state, dir) if (next) return apply(view, next) return false } } function nodeLen(node) { return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length } function isIgnorable(dom) { let desc = dom.pmViewDesc return desc && desc.size == 0 && (dom.nextSibling || dom.nodeName != "BR") } // Make sure the cursor isn't directly after one or more ignored // nodes, which will confuse the browser's cursor motion logic. function skipIgnoredNodesLeft(view) { let sel = view.root.getSelection() let node = sel.focusNode, offset = sel.focusOffset if (!node) return let moveNode, moveOffset, force = false // Gecko will do odd things when the selection is directly in front // of a non-editable node, so in that case, move it into the next // node if possible. Issue prosemirror/prosemirror#832. if (browser.gecko && node.nodeType == 1 && offset < nodeLen(node) && isIgnorable(node.childNodes[offset])) force = true for (;;) { if (offset > 0) { if (node.nodeType != 1) { break } else { let before = node.childNodes[offset - 1] if (isIgnorable(before)) { moveNode = node moveOffset = --offset } else if (before.nodeType == 3) { node = before offset = node.nodeValue.length } else break } } else if (isBlockNode(node)) { break } else { let prev = node.previousSibling while (prev && isIgnorable(prev)) { moveNode = node.parentNode moveOffset = domIndex(prev) prev = prev.previousSibling } if (!prev) { node = node.parentNode if (node == view.dom) break offset = 0 } else { node = prev offset = nodeLen(node) } } } if (force) setSelFocus(view, sel, node, offset) else if (moveNode) setSelFocus(view, sel, moveNode, moveOffset) } // Make sure the cursor isn't directly before one or more ignored // nodes. function skipIgnoredNodesRight(view) { let sel = view.root.getSelection() let node = sel.focusNode, offset = sel.focusOffset if (!node) return let len = nodeLen(node) let moveNode, moveOffset for (;;) { if (offset < len) { if (node.nodeType != 1) break let after = node.childNodes[offset] if (isIgnorable(after)) { moveNode = node moveOffset = ++offset } else break } else if (isBlockNode(node)) { break } else { let next = node.nextSibling while (next && isIgnorable(next)) { moveNode = next.parentNode moveOffset = domIndex(next) + 1 next = next.nextSibling } if (!next) { node = node.parentNode if (node == view.dom) break offset = len = 0 } else { node = next offset = 0 len = nodeLen(node) } } } if (moveNode) setSelFocus(view, sel, moveNode, moveOffset) } function isBlockNode(dom) { let desc = dom.pmViewDesc return desc && desc.node && desc.node.isBlock } function setSelFocus(view, sel, node, offset) { if (selectionCollapsed(sel)) { let range = document.createRange() range.setEnd(node, offset) range.setStart(node, offset) sel.removeAllRanges() sel.addRange(range) } else if (sel.extend) { sel.extend(node, offset) } view.domObserver.setCurSelection() let {state} = view // If no state update ends up happening, reset the selection. setTimeout(() => { if (view.state == state) selectionToDOM(view) }, 50) } // : (EditorState, number) // Check whether vertical selection motion would involve node // selections. If so, apply it (if not, the result is left to the // browser) function selectVertically(view, dir, mods) { let sel = view.state.selection if (sel instanceof TextSelection && !sel.empty || mods.indexOf("s") > -1) return false if (browser.mac && mods.indexOf("m") > -1) return false let {$from, $to} = sel if (!$from.parent.inlineContent || view.endOfTextblock(dir < 0 ? "up" : "down")) { let next = moveSelectionBlock(view.state, dir) if (next && (next instanceof NodeSelection)) return apply(view, next) } if (!$from.parent.inlineContent) { let side = dir < 0 ? $from : $to let beyond = sel instanceof AllSelection ? Selection.near(side, dir) : Selection.findFrom(side, dir) return beyond ? apply(view, beyond) : false } return false } function stopNativeHorizontalDelete(view, dir) { if (!(view.state.selection instanceof TextSelection)) return true let {$head, $anchor, empty} = view.state.selection if (!$head.sameParent($anchor)) return true if (!empty) return false if (view.endOfTextblock(dir > 0 ? "forward" : "backward")) return true let nextNode = !$head.textOffset && (dir < 0 ? $head.nodeBefore : $head.nodeAfter) if (nextNode && !nextNode.isText) { let tr = view.state.tr if (dir < 0) tr.delete($head.pos - nextNode.nodeSize, $head.pos) else tr.delete($head.pos, $head.pos + nextNode.nodeSize) view.dispatch(tr) return true } return false } function switchEditable(view, node, state) { view.domObserver.stop() node.contentEditable = state view.domObserver.start() } // Issue #867 / #1090 / https://bugs.chromium.org/p/chromium/issues/detail?id=903821 // In which Safari (and at some point in the past, Chrome) does really // wrong things when the down arrow is pressed when the cursor is // directly at the start of a textblock and has an uneditable node // after it function safariDownArrowBug(view) { if (!browser.safari || view.state.selection.$head.parentOffset > 0) return let {focusNode, focusOffset} = view.root.getSelection() if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 && focusNode.firstChild && focusNode.firstChild.contentEditable == "false") { let child = focusNode.firstChild switchEditable(view, child, true) setTimeout(() => switchEditable(view, child, false), 20) } } // A backdrop key mapping used to make sure we always suppress keys // that have a dangerous default effect, even if the commands they are // bound to return false, and to make sure that cursor-motion keys // find a cursor (as opposed to a node selection) when pressed. For // cursor-motion keys, the code in the handlers also takes care of // block selections. function getMods(event) { let result = "" if (event.ctrlKey) result += "c" if (event.metaKey) result += "m" if (event.altKey) result += "a" if (event.shiftKey) result += "s" return result } export function captureKeyDown(view, event) { let code = event.keyCode, mods = getMods(event) if (code == 8 || (browser.mac && code == 72 && mods == "c")) { // Backspace, Ctrl-h on Mac return stopNativeHorizontalDelete(view, -1) || skipIgnoredNodesLeft(view) } else if (code == 46 || (browser.mac && code == 68 && mods == "c")) { // Delete, Ctrl-d on Mac return stopNativeHorizontalDelete(view, 1) || skipIgnoredNodesRight(view) } else if (code == 13 || code == 27) { // Enter, Esc return true } else if (code == 37) { // Left arrow return selectHorizontally(view, -1, mods) || skipIgnoredNodesLeft(view) } else if (code == 39) { // Right arrow return selectHorizontally(view, 1, mods) || skipIgnoredNodesRight(view) } else if (code == 38) { // Up arrow return selectVertically(view, -1, mods) || skipIgnoredNodesLeft(view) } else if (code == 40) { // Down arrow return safariDownArrowBug(view) || selectVertically(view, 1, mods) || skipIgnoredNodesRight(view) } else if (mods == (browser.mac ? "m" : "c") && (code == 66 || code == 73 || code == 89 || code == 90)) { // Mod-[biyz] return true } return false } prosemirror-view-1.23.13/src/clipboard.js000066400000000000000000000236601423202571000203030ustar00rootroot00000000000000import {Slice, Fragment, DOMParser, DOMSerializer} from "prosemirror-model" import browser from "./browser" export function serializeForClipboard(view, slice) { let context = [], {content, openStart, openEnd} = slice while (openStart > 1 && openEnd > 1 && content.childCount == 1 && content.firstChild.childCount == 1) { openStart-- openEnd-- let node = content.firstChild context.push(node.type.name, node.attrs != node.type.defaultAttrs ? node.attrs : null) content = node.content } let serializer = view.someProp("clipboardSerializer") || DOMSerializer.fromSchema(view.state.schema) let doc = detachedDoc(), wrap = doc.createElement("div") wrap.appendChild(serializer.serializeFragment(content, {document: doc})) let firstChild = wrap.firstChild, needsWrap while (firstChild && firstChild.nodeType == 1 && (needsWrap = wrapMap[firstChild.nodeName.toLowerCase()])) { for (let i = needsWrap.length - 1; i >= 0; i--) { let wrapper = doc.createElement(needsWrap[i]) while (wrap.firstChild) wrapper.appendChild(wrap.firstChild) wrap.appendChild(wrapper) if (needsWrap[i] != "tbody") { openStart++ openEnd++ } } firstChild = wrap.firstChild } if (firstChild && firstChild.nodeType == 1) firstChild.setAttribute("data-pm-slice", `${openStart} ${openEnd} ${JSON.stringify(context)}`) let text = view.someProp("clipboardTextSerializer", f => f(slice)) || slice.content.textBetween(0, slice.content.size, "\n\n") return {dom: wrap, text} } // : (EditorView, string, string, ?bool, ResolvedPos) → ?Slice // Read a slice of content from the clipboard (or drop data). export function parseFromClipboard(view, text, html, plainText, $context) { let dom, inCode = $context.parent.type.spec.code, slice if (!html && !text) return null let asText = text && (plainText || inCode || !html) if (asText) { view.someProp("transformPastedText", f => { text = f(text, inCode || plainText) }) if (inCode) return text ? new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0) : Slice.empty let parsed = view.someProp("clipboardTextParser", f => f(text, $context, plainText)) if (parsed) { slice = parsed } else { let marks = $context.marks() let {schema} = view.state, serializer = DOMSerializer.fromSchema(schema) dom = document.createElement("div") text.split(/(?:\r\n?|\n)+/).forEach(block => { let p = dom.appendChild(document.createElement("p")) if (block) p.appendChild(serializer.serializeNode(schema.text(block, marks))) }) } } else { view.someProp("transformPastedHTML", f => { html = f(html) }) dom = readHTML(html) if (browser.webkit) restoreReplacedSpaces(dom) } let contextNode = dom && dom.querySelector("[data-pm-slice]") let sliceData = contextNode && /^(\d+) (\d+) (.*)/.exec(contextNode.getAttribute("data-pm-slice")) if (!slice) { let parser = view.someProp("clipboardParser") || view.someProp("domParser") || DOMParser.fromSchema(view.state.schema) slice = parser.parseSlice(dom, { preserveWhitespace: !!(asText || sliceData), context: $context, ruleFromNode(dom) { if (dom.nodeName == "BR" && !dom.nextSibling && dom.parentNode && !inlineParents.test(dom.parentNode.nodeName)) return {ignore: true} } }) } if (sliceData) { slice = addContext(closeSlice(slice, +sliceData[1], +sliceData[2]), sliceData[3]) } else { // HTML wasn't created by ProseMirror. Make sure top-level siblings are coherent slice = Slice.maxOpen(normalizeSiblings(slice.content, $context), true) if (slice.openStart || slice.openEnd) { let openStart = 0, openEnd = 0 for (let node = slice.content.firstChild; openStart < slice.openStart && !node.type.spec.isolating; openStart++, node = node.firstChild) {} for (let node = slice.content.lastChild; openEnd < slice.openEnd && !node.type.spec.isolating; openEnd++, node = node.lastChild) {} slice = closeSlice(slice, openStart, openEnd) } } view.someProp("transformPasted", f => { slice = f(slice) }) return slice } const inlineParents = /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i // Takes a slice parsed with parseSlice, which means there hasn't been // any content-expression checking done on the top nodes, tries to // find a parent node in the current context that might fit the nodes, // and if successful, rebuilds the slice so that it fits into that parent. // // This addresses the problem that Transform.replace expects a // coherent slice, and will fail to place a set of siblings that don't // fit anywhere in the schema. function normalizeSiblings(fragment, $context) { if (fragment.childCount < 2) return fragment for (let d = $context.depth; d >= 0; d--) { let parent = $context.node(d) let match = parent.contentMatchAt($context.index(d)) let lastWrap, result = [] fragment.forEach(node => { if (!result) return let wrap = match.findWrapping(node.type), inLast if (!wrap) return result = null if (inLast = result.length && lastWrap.length && addToSibling(wrap, lastWrap, node, result[result.length - 1], 0)) { result[result.length - 1] = inLast } else { if (result.length) result[result.length - 1] = closeRight(result[result.length - 1], lastWrap.length) let wrapped = withWrappers(node, wrap) result.push(wrapped) match = match.matchType(wrapped.type, wrapped.attrs) lastWrap = wrap } }) if (result) return Fragment.from(result) } return fragment } function withWrappers(node, wrap, from = 0) { for (let i = wrap.length - 1; i >= from; i--) node = wrap[i].create(null, Fragment.from(node)) return node } // Used to group adjacent nodes wrapped in similar parents by // normalizeSiblings into the same parent node function addToSibling(wrap, lastWrap, node, sibling, depth) { if (depth < wrap.length && depth < lastWrap.length && wrap[depth] == lastWrap[depth]) { let inner = addToSibling(wrap, lastWrap, node, sibling.lastChild, depth + 1) if (inner) return sibling.copy(sibling.content.replaceChild(sibling.childCount - 1, inner)) let match = sibling.contentMatchAt(sibling.childCount) if (match.matchType(depth == wrap.length - 1 ? node.type : wrap[depth + 1])) return sibling.copy(sibling.content.append(Fragment.from(withWrappers(node, wrap, depth + 1)))) } } function closeRight(node, depth) { if (depth == 0) return node let fragment = node.content.replaceChild(node.childCount - 1, closeRight(node.lastChild, depth - 1)) let fill = node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true) return node.copy(fragment.append(fill)) } function closeRange(fragment, side, from, to, depth, openEnd) { let node = side < 0 ? fragment.firstChild : fragment.lastChild, inner = node.content if (depth < to - 1) inner = closeRange(inner, side, from, to, depth + 1, openEnd) if (depth >= from) inner = side < 0 ? node.contentMatchAt(0).fillBefore(inner, fragment.childCount > 1 || openEnd <= depth).append(inner) : inner.append(node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true)) return fragment.replaceChild(side < 0 ? 0 : fragment.childCount - 1, node.copy(inner)) } function closeSlice(slice, openStart, openEnd) { if (openStart < slice.openStart) slice = new Slice(closeRange(slice.content, -1, openStart, slice.openStart, 0, slice.openEnd), openStart, slice.openEnd) if (openEnd < slice.openEnd) slice = new Slice(closeRange(slice.content, 1, openEnd, slice.openEnd, 0, 0), slice.openStart, openEnd) return slice } // Trick from jQuery -- some elements must be wrapped in other // elements for innerHTML to work. I.e. if you do `div.innerHTML = // ".."` the table cells are ignored. const wrapMap = { thead: ["table"], tbody: ["table"], tfoot: ["table"], caption: ["table"], colgroup: ["table"], col: ["table", "colgroup"], tr: ["table", "tbody"], td: ["table", "tbody", "tr"], th: ["table", "tbody", "tr"] } let _detachedDoc = null function detachedDoc() { return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title")) } function readHTML(html) { let metas = /^(\s*]*>)*/.exec(html) if (metas) html = html.slice(metas[0].length) let elt = detachedDoc().createElement("div") let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()]) html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "").reverse().join("") elt.innerHTML = html if (wrap) for (let i = 0; i < wrap.length; i++) elt = elt.querySelector(wrap[i]) || elt return elt } // Webkit browsers do some hard-to-predict replacement of regular // spaces with non-breaking spaces when putting content on the // clipboard. This tries to convert such non-breaking spaces (which // will be wrapped in a plain span on Chrome, a span with class // Apple-converted-space on Safari) back to regular spaces. function restoreReplacedSpaces(dom) { let nodes = dom.querySelectorAll(browser.chrome ? "span:not([class]):not([style])" : "span.Apple-converted-space") for (let i = 0; i < nodes.length; i++) { let node = nodes[i] if (node.childNodes.length == 1 && node.textContent == "\u00a0" && node.parentNode) node.parentNode.replaceChild(dom.ownerDocument.createTextNode(" "), node) } } function addContext(slice, context) { if (!slice.size) return slice let schema = slice.content.firstChild.type.schema, array try { array = JSON.parse(context) } catch(e) { return slice } let {content, openStart, openEnd} = slice for (let i = array.length - 2; i >= 0; i -= 2) { let type = schema.nodes[array[i]] if (!type || type.hasRequiredAttrs()) break content = Fragment.from(type.create(array[i + 1], content)) openStart++; openEnd++ } return new Slice(content, openStart, openEnd) } prosemirror-view-1.23.13/src/decoration.js000066400000000000000000000631211423202571000204670ustar00rootroot00000000000000function compareObjs(a, b) { if (a == b) return true for (let p in a) if (a[p] !== b[p]) return false for (let p in b) if (!(p in a)) return false return true } class WidgetType { constructor(toDOM, spec) { this.spec = spec || noSpec this.side = this.spec.side || 0 this.toDOM = toDOM } map(mapping, span, offset, oldOffset) { let {pos, deleted} = mapping.mapResult(span.from + oldOffset, this.side < 0 ? -1 : 1) return deleted ? null : new Decoration(pos - offset, pos - offset, this) } valid() { return true } eq(other) { return this == other || (other instanceof WidgetType && (this.spec.key && this.spec.key == other.spec.key || this.toDOM == other.toDOM && compareObjs(this.spec, other.spec))) } destroy(node) { if (this.spec.destroy) this.spec.destroy(node) } } class InlineType { constructor(attrs, spec) { this.spec = spec || noSpec this.attrs = attrs } map(mapping, span, offset, oldOffset) { let from = mapping.map(span.from + oldOffset, this.spec.inclusiveStart ? -1 : 1) - offset let to = mapping.map(span.to + oldOffset, this.spec.inclusiveEnd ? 1 : -1) - offset return from >= to ? null : new Decoration(from, to, this) } valid(_, span) { return span.from < span.to } eq(other) { return this == other || (other instanceof InlineType && compareObjs(this.attrs, other.attrs) && compareObjs(this.spec, other.spec)) } static is(span) { return span.type instanceof InlineType } } class NodeType { constructor(attrs, spec) { this.spec = spec || noSpec this.attrs = attrs } map(mapping, span, offset, oldOffset) { let from = mapping.mapResult(span.from + oldOffset, 1) if (from.deleted) return null let to = mapping.mapResult(span.to + oldOffset, -1) if (to.deleted || to.pos <= from.pos) return null return new Decoration(from.pos - offset, to.pos - offset, this) } valid(node, span) { let {index, offset} = node.content.findIndex(span.from), child return offset == span.from && !(child = node.child(index)).isText && offset + child.nodeSize == span.to } eq(other) { return this == other || (other instanceof NodeType && compareObjs(this.attrs, other.attrs) && compareObjs(this.spec, other.spec)) } } // ::- Decoration objects can be provided to the view through the // [`decorations` prop](#view.EditorProps.decorations). They come in // several variants—see the static members of this class for details. export class Decoration { constructor(from, to, type) { // :: number // The start position of the decoration. this.from = from // :: number // The end position. Will be the same as `from` for [widget // decorations](#view.Decoration^widget). this.to = to this.type = type } copy(from, to) { return new Decoration(from, to, this.type) } eq(other, offset = 0) { return this.type.eq(other.type) && this.from + offset == other.from && this.to + offset == other.to } map(mapping, offset, oldOffset) { return this.type.map(mapping, this, offset, oldOffset) } // :: (number, union<(view: EditorView, getPos: () → number) → dom.Node, dom.Node>, ?Object) → Decoration // Creates a widget decoration, which is a DOM node that's shown in // the document at the given position. It is recommended that you // delay rendering the widget by passing a function that will be // called when the widget is actually drawn in a view, but you can // also directly pass a DOM node. `getPos` can be used to find the // widget's current document position. // // spec::- These options are supported: // // side:: ?number // Controls which side of the document position this widget is // associated with. When negative, it is drawn before a cursor // at its position, and content inserted at that position ends // up after the widget. When zero (the default) or positive, the // widget is drawn after the cursor and content inserted there // ends up before the widget. // // When there are multiple widgets at a given position, their // `side` values determine the order in which they appear. Those // with lower values appear first. The ordering of widgets with // the same `side` value is unspecified. // // When `marks` is null, `side` also determines the marks that // the widget is wrapped in—those of the node before when // negative, those of the node after when positive. // // marks:: ?[Mark] // The precise set of marks to draw around the widget. // // stopEvent:: ?(event: dom.Event) → bool // Can be used to control which DOM events, when they bubble out // of this widget, the editor view should ignore. // // ignoreSelection:: ?bool // When set (defaults to false), selection changes inside the // widget are ignored, and don't cause ProseMirror to try and // re-sync the selection with its selection state. // // key:: ?string // When comparing decorations of this type (in order to decide // whether it needs to be redrawn), ProseMirror will by default // compare the widget DOM node by identity. If you pass a key, // that key will be compared instead, which can be useful when // you generate decorations on the fly and don't want to store // and reuse DOM nodes. Make sure that any widgets with the same // key are interchangeable—if widgets differ in, for example, // the behavior of some event handler, they should get // different keys. // // destroy:: ?(node: dom.Node) // Called when the widget decoration is removed as a result of // mapping static widget(pos, toDOM, spec) { return new Decoration(pos, pos, new WidgetType(toDOM, spec)) } // :: (number, number, DecorationAttrs, ?Object) → Decoration // Creates an inline decoration, which adds the given attributes to // each inline node between `from` and `to`. // // spec::- These options are recognized: // // inclusiveStart:: ?bool // Determines how the left side of the decoration is // [mapped](#transform.Position_Mapping) when content is // inserted directly at that position. By default, the decoration // won't include the new content, but you can set this to `true` // to make it inclusive. // // inclusiveEnd:: ?bool // Determines how the right side of the decoration is mapped. // See // [`inclusiveStart`](#view.Decoration^inline^spec.inclusiveStart). static inline(from, to, attrs, spec) { return new Decoration(from, to, new InlineType(attrs, spec)) } // :: (number, number, DecorationAttrs, ?Object) → Decoration // Creates a node decoration. `from` and `to` should point precisely // before and after a node in the document. That node, and only that // node, will receive the given attributes. // // spec::- // // Optional information to store with the decoration. It // is also used when comparing decorators for equality. static node(from, to, attrs, spec) { return new Decoration(from, to, new NodeType(attrs, spec)) } // :: Object // The spec provided when creating this decoration. Can be useful // if you've stored extra information in that object. get spec() { return this.type.spec } get inline() { return this.type instanceof InlineType } } // DecorationAttrs:: interface // A set of attributes to add to a decorated node. Most properties // simply directly correspond to DOM attributes of the same name, // which will be set to the property's value. These are exceptions: // // class:: ?string // A CSS class name or a space-separated set of class names to be // _added_ to the classes that the node already had. // // style:: ?string // A string of CSS to be _added_ to the node's existing `style` property. // // nodeName:: ?string // When non-null, the target node is wrapped in a DOM element of // this type (and the other attributes are applied to this element). const none = [], noSpec = {} // :: class extends DecorationSource // A collection of [decorations](#view.Decoration), organized in // such a way that the drawing algorithm can efficiently use and // compare them. This is a persistent data structure—it is not // modified, updates create a new value. export class DecorationSet { constructor(local, children) { this.local = local && local.length ? local : none this.children = children && children.length ? children : none } // :: (Node, [Decoration]) → DecorationSet // Create a set of decorations, using the structure of the given // document. static create(doc, decorations) { return decorations.length ? buildTree(decorations, doc, 0, noSpec) : empty } // :: (?number, ?number, ?(spec: Object) → bool) → [Decoration] // Find all decorations in this set which touch the given range // (including decorations that start or end directly at the // boundaries) and match the given predicate on their spec. When // `start` and `end` are omitted, all decorations in the set are // considered. When `predicate` isn't given, all decorations are // assumed to match. find(start, end, predicate) { let result = [] this.findInner(start == null ? 0 : start, end == null ? 1e9 : end, result, 0, predicate) return result } findInner(start, end, result, offset, predicate) { for (let i = 0; i < this.local.length; i++) { let span = this.local[i] if (span.from <= end && span.to >= start && (!predicate || predicate(span.spec))) result.push(span.copy(span.from + offset, span.to + offset)) } for (let i = 0; i < this.children.length; i += 3) { if (this.children[i] < end && this.children[i + 1] > start) { let childOff = this.children[i] + 1 this.children[i + 2].findInner(start - childOff, end - childOff, result, offset + childOff, predicate) } } } // :: (Mapping, Node, ?Object) → DecorationSet // Map the set of decorations in response to a change in the // document. // // options::- An optional set of options. // // onRemove:: ?(decorationSpec: Object) // When given, this function will be called for each decoration // that gets dropped as a result of the mapping, passing the // spec of that decoration. map(mapping, doc, options) { if (this == empty || mapping.maps.length == 0) return this return this.mapInner(mapping, doc, 0, 0, options || noSpec) } mapInner(mapping, node, offset, oldOffset, options) { let newLocal for (let i = 0; i < this.local.length; i++) { let mapped = this.local[i].map(mapping, offset, oldOffset) if (mapped && mapped.type.valid(node, mapped)) (newLocal || (newLocal = [])).push(mapped) else if (options.onRemove) options.onRemove(this.local[i].spec) } if (this.children.length) return mapChildren(this.children, newLocal, mapping, node, offset, oldOffset, options) else return newLocal ? new DecorationSet(newLocal.sort(byPos)) : empty } // :: (Node, [Decoration]) → DecorationSet // Add the given array of decorations to the ones in the set, // producing a new set. Needs access to the current document to // create the appropriate tree structure. add(doc, decorations) { if (!decorations.length) return this if (this == empty) return DecorationSet.create(doc, decorations) return this.addInner(doc, decorations, 0) } addInner(doc, decorations, offset) { let children, childIndex = 0 doc.forEach((childNode, childOffset) => { let baseOffset = childOffset + offset, found if (!(found = takeSpansForNode(decorations, childNode, baseOffset))) return if (!children) children = this.children.slice() while (childIndex < children.length && children[childIndex] < childOffset) childIndex += 3 if (children[childIndex] == childOffset) children[childIndex + 2] = children[childIndex + 2].addInner(childNode, found, baseOffset + 1) else children.splice(childIndex, 0, childOffset, childOffset + childNode.nodeSize, buildTree(found, childNode, baseOffset + 1, noSpec)) childIndex += 3 }) let local = moveSpans(childIndex ? withoutNulls(decorations) : decorations, -offset) for (let i = 0; i < local.length; i++) if (!local[i].type.valid(doc, local[i])) local.splice(i--, 1) return new DecorationSet(local.length ? this.local.concat(local).sort(byPos) : this.local, children || this.children) } // :: ([Decoration]) → DecorationSet // Create a new set that contains the decorations in this set, minus // the ones in the given array. remove(decorations) { if (decorations.length == 0 || this == empty) return this return this.removeInner(decorations, 0) } removeInner(decorations, offset) { let children = this.children, local = this.local for (let i = 0; i < children.length; i += 3) { let found, from = children[i] + offset, to = children[i + 1] + offset for (let j = 0, span; j < decorations.length; j++) if (span = decorations[j]) { if (span.from > from && span.to < to) { decorations[j] = null ;(found || (found = [])).push(span) } } if (!found) continue if (children == this.children) children = this.children.slice() let removed = children[i + 2].removeInner(found, from + 1) if (removed != empty) { children[i + 2] = removed } else { children.splice(i, 3) i -= 3 } } if (local.length) for (let i = 0, span; i < decorations.length; i++) if (span = decorations[i]) { for (let j = 0; j < local.length; j++) if (local[j].eq(span, offset)) { if (local == this.local) local = this.local.slice() local.splice(j--, 1) } } if (children == this.children && local == this.local) return this return local.length || children.length ? new DecorationSet(local, children) : empty } forChild(offset, node) { if (this == empty) return this if (node.isLeaf) return DecorationSet.empty let child, local for (let i = 0; i < this.children.length; i += 3) if (this.children[i] >= offset) { if (this.children[i] == offset) child = this.children[i + 2] break } let start = offset + 1, end = start + node.content.size for (let i = 0; i < this.local.length; i++) { let dec = this.local[i] if (dec.from < end && dec.to > start && (dec.type instanceof InlineType)) { let from = Math.max(start, dec.from) - start, to = Math.min(end, dec.to) - start if (from < to) (local || (local = [])).push(dec.copy(from, to)) } } if (local) { let localSet = new DecorationSet(local.sort(byPos)) return child ? new DecorationGroup([localSet, child]) : localSet } return child || empty } eq(other) { if (this == other) return true if (!(other instanceof DecorationSet) || this.local.length != other.local.length || this.children.length != other.children.length) return false for (let i = 0; i < this.local.length; i++) if (!this.local[i].eq(other.local[i])) return false for (let i = 0; i < this.children.length; i += 3) if (this.children[i] != other.children[i] || this.children[i + 1] != other.children[i + 1] || !this.children[i + 2].eq(other.children[i + 2])) return false return true } locals(node) { return removeOverlap(this.localsInner(node)) } localsInner(node) { if (this == empty) return none if (node.inlineContent || !this.local.some(InlineType.is)) return this.local let result = [] for (let i = 0; i < this.local.length; i++) { if (!(this.local[i].type instanceof InlineType)) result.push(this.local[i]) } return result } } // DecorationSource:: interface // An object that can [provide](#view.EditorProps.decorations) // decorations. Implemented by [`DecorationSet`](#view.DecorationSet), // and passed to [node views](#view.EditorProps.nodeViews). // // map:: (Mapping, Node) → DecorationSource // Map the set of decorations in response to a change in the // document. const empty = new DecorationSet() // :: DecorationSet // The empty set of decorations. DecorationSet.empty = empty DecorationSet.removeOverlap = removeOverlap // :- An abstraction that allows the code dealing with decorations to // treat multiple DecorationSet objects as if it were a single object // with (a subset of) the same interface. class DecorationGroup { constructor(members) { this.members = members } map(mapping, doc) { const mappedDecos = this.members.map( member => member.map(mapping, doc, noSpec) ) return DecorationGroup.from(mappedDecos) } forChild(offset, child) { if (child.isLeaf) return DecorationSet.empty let found = [] for (let i = 0; i < this.members.length; i++) { let result = this.members[i].forChild(offset, child) if (result == empty) continue if (result instanceof DecorationGroup) found = found.concat(result.members) else found.push(result) } return DecorationGroup.from(found) } eq(other) { if (!(other instanceof DecorationGroup) || other.members.length != this.members.length) return false for (let i = 0; i < this.members.length; i++) if (!this.members[i].eq(other.members[i])) return false return true } locals(node) { let result, sorted = true for (let i = 0; i < this.members.length; i++) { let locals = this.members[i].localsInner(node) if (!locals.length) continue if (!result) { result = locals } else { if (sorted) { result = result.slice() sorted = false } for (let j = 0; j < locals.length; j++) result.push(locals[j]) } } return result ? removeOverlap(sorted ? result : result.sort(byPos)) : none } // : ([DecorationSet]) → union // Create a group for the given array of decoration sets, or return // a single set when possible. static from(members) { switch (members.length) { case 0: return empty case 1: return members[0] default: return new DecorationGroup(members) } } } function mapChildren(oldChildren, newLocal, mapping, node, offset, oldOffset, options) { let children = oldChildren.slice() // Mark the children that are directly touched by changes, and // move those that are after the changes. let shift = (oldStart, oldEnd, newStart, newEnd) => { for (let i = 0; i < children.length; i += 3) { let end = children[i + 1], dSize if (end < 0 || oldStart > end + oldOffset) continue let start = children[i] + oldOffset if (oldEnd >= start) { children[i + 1] = oldStart <= start ? -2 : -1 } else if (newStart >= offset && (dSize = (newEnd - newStart) - (oldEnd - oldStart))) { children[i] += dSize children[i + 1] += dSize } } } for (let i = 0; i < mapping.maps.length; i++) mapping.maps[i].forEach(shift) // Find the child nodes that still correspond to a single node, // recursively call mapInner on them and update their positions. let mustRebuild = false for (let i = 0; i < children.length; i += 3) if (children[i + 1] < 0) { // Touched nodes if (children[i + 1] == -2) { mustRebuild = true children[i + 1] = -1 continue } let from = mapping.map(oldChildren[i] + oldOffset), fromLocal = from - offset if (fromLocal < 0 || fromLocal >= node.content.size) { mustRebuild = true continue } // Must read oldChildren because children was tagged with -1 let to = mapping.map(oldChildren[i + 1] + oldOffset, -1), toLocal = to - offset let {index, offset: childOffset} = node.content.findIndex(fromLocal) let childNode = node.maybeChild(index) if (childNode && childOffset == fromLocal && childOffset + childNode.nodeSize == toLocal) { let mapped = children[i + 2].mapInner(mapping, childNode, from + 1, oldChildren[i] + oldOffset + 1, options) if (mapped != empty) { children[i] = fromLocal children[i + 1] = toLocal children[i + 2] = mapped } else { children[i + 1] = -2 mustRebuild = true } } else { mustRebuild = true } } // Remaining children must be collected and rebuilt into the appropriate structure if (mustRebuild) { let decorations = mapAndGatherRemainingDecorations(children, oldChildren, newLocal || [], mapping, offset, oldOffset, options) let built = buildTree(decorations, node, 0, options) newLocal = built.local for (let i = 0; i < children.length; i += 3) if (children[i + 1] < 0) { children.splice(i, 3) i -= 3 } for (let i = 0, j = 0; i < built.children.length; i += 3) { let from = built.children[i] while (j < children.length && children[j] < from) j += 3 children.splice(j, 0, built.children[i], built.children[i + 1], built.children[i + 2]) } } return new DecorationSet(newLocal && newLocal.sort(byPos), children) } function moveSpans(spans, offset) { if (!offset || !spans.length) return spans let result = [] for (let i = 0; i < spans.length; i++) { let span = spans[i] result.push(new Decoration(span.from + offset, span.to + offset, span.type)) } return result } function mapAndGatherRemainingDecorations(children, oldChildren, decorations, mapping, offset, oldOffset, options) { // Gather all decorations from the remaining marked children function gather(set, oldOffset) { for (let i = 0; i < set.local.length; i++) { let mapped = set.local[i].map(mapping, offset, oldOffset) if (mapped) decorations.push(mapped) else if (options.onRemove) options.onRemove(set.local[i].spec) } for (let i = 0; i < set.children.length; i += 3) gather(set.children[i + 2], set.children[i] + oldOffset + 1) } for (let i = 0; i < children.length; i += 3) if (children[i + 1] == -1) gather(children[i + 2], oldChildren[i] + oldOffset + 1) return decorations } function takeSpansForNode(spans, node, offset) { if (node.isLeaf) return null let end = offset + node.nodeSize, found = null for (let i = 0, span; i < spans.length; i++) { if ((span = spans[i]) && span.from > offset && span.to < end) { ;(found || (found = [])).push(span) spans[i] = null } } return found } function withoutNulls(array) { let result = [] for (let i = 0; i < array.length; i++) if (array[i] != null) result.push(array[i]) return result } // : ([Decoration], Node, number) → DecorationSet // Build up a tree that corresponds to a set of decorations. `offset` // is a base offset that should be subtracted from the `from` and `to` // positions in the spans (so that we don't have to allocate new spans // for recursive calls). function buildTree(spans, node, offset, options) { let children = [], hasNulls = false node.forEach((childNode, localStart) => { let found = takeSpansForNode(spans, childNode, localStart + offset) if (found) { hasNulls = true let subtree = buildTree(found, childNode, offset + localStart + 1, options) if (subtree != empty) children.push(localStart, localStart + childNode.nodeSize, subtree) } }) let locals = moveSpans(hasNulls ? withoutNulls(spans) : spans, -offset).sort(byPos) for (let i = 0; i < locals.length; i++) if (!locals[i].type.valid(node, locals[i])) { if (options.onRemove) options.onRemove(locals[i].spec) locals.splice(i--, 1) } return locals.length || children.length ? new DecorationSet(locals, children) : empty } // : (Decoration, Decoration) → number // Used to sort decorations so that ones with a low start position // come first, and within a set with the same start position, those // with an smaller end position come first. function byPos(a, b) { return a.from - b.from || a.to - b.to } // : ([Decoration]) → [Decoration] // Scan a sorted array of decorations for partially overlapping spans, // and split those so that only fully overlapping spans are left (to // make subsequent rendering easier). Will return the input array if // no partially overlapping spans are found (the common case). function removeOverlap(spans) { let working = spans for (let i = 0; i < working.length - 1; i++) { let span = working[i] if (span.from != span.to) for (let j = i + 1; j < working.length; j++) { let next = working[j] if (next.from == span.from) { if (next.to != span.to) { if (working == spans) working = spans.slice() // Followed by a partially overlapping larger span. Split that // span. working[j] = next.copy(next.from, span.to) insertAhead(working, j + 1, next.copy(span.to, next.to)) } continue } else { if (next.from < span.to) { if (working == spans) working = spans.slice() // The end of this one overlaps with a subsequent span. Split // this one. working[i] = span.copy(span.from, next.from) insertAhead(working, j, span.copy(next.from, span.to)) } break } } } return working } function insertAhead(array, i, deco) { while (i < array.length && byPos(deco, array[i]) > 0) i++ array.splice(i, 0, deco) } // : (EditorView) → union // Get the decorations associated with the current props of a view. export function viewDecorations(view) { let found = [] view.someProp("decorations", f => { let result = f(view.state) if (result && result != empty) found.push(result) }) if (view.cursorWrapper) found.push(DecorationSet.create(view.state.doc, [view.cursorWrapper.deco])) return DecorationGroup.from(found) } prosemirror-view-1.23.13/src/dom.js000066400000000000000000000061361423202571000171220ustar00rootroot00000000000000import browser from "./browser" export const domIndex = function(node) { for (var index = 0;; index++) { node = node.previousSibling if (!node) return index } } export const parentNode = function(node) { let parent = node.assignedSlot || node.parentNode return parent && parent.nodeType == 11 ? parent.host : parent } let reusedRange = null // Note that this will always return the same range, because DOM range // objects are every expensive, and keep slowing down subsequent DOM // updates, for some reason. export const textRange = function(node, from, to) { let range = reusedRange || (reusedRange = document.createRange()) range.setEnd(node, to == null ? node.nodeValue.length : to) range.setStart(node, from || 0) return range } // Scans forward and backward through DOM positions equivalent to the // given one to see if the two are in the same place (i.e. after a // text node vs at the end of that text node) export const isEquivalentPosition = function(node, off, targetNode, targetOff) { return targetNode && (scanFor(node, off, targetNode, targetOff, -1) || scanFor(node, off, targetNode, targetOff, 1)) } const atomElements = /^(img|br|input|textarea|hr)$/i function scanFor(node, off, targetNode, targetOff, dir) { for (;;) { if (node == targetNode && off == targetOff) return true if (off == (dir < 0 ? 0 : nodeSize(node))) { let parent = node.parentNode if (!parent || parent.nodeType != 1 || hasBlockDesc(node) || atomElements.test(node.nodeName) || node.contentEditable == "false") return false off = domIndex(node) + (dir < 0 ? 0 : 1) node = parent } else if (node.nodeType == 1) { node = node.childNodes[off + (dir < 0 ? -1 : 0)] if (node.contentEditable == "false") return false off = dir < 0 ? nodeSize(node) : 0 } else { return false } } } export function nodeSize(node) { return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length } export function isOnEdge(node, offset, parent) { for (let atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;) { if (node == parent) return true let index = domIndex(node) node = node.parentNode if (!node) return false atStart = atStart && index == 0 atEnd = atEnd && index == nodeSize(node) } } function hasBlockDesc(dom) { let desc for (let cur = dom; cur; cur = cur.parentNode) if (desc = cur.pmViewDesc) break return desc && desc.node && desc.node.isBlock && (desc.dom == dom || desc.contentDOM == dom) } // Work around Chrome issue https://bugs.chromium.org/p/chromium/issues/detail?id=447523 // (isCollapsed inappropriately returns true in shadow dom) export const selectionCollapsed = function(domSel) { let collapsed = domSel.isCollapsed if (collapsed && browser.chrome && domSel.rangeCount && !domSel.getRangeAt(0).collapsed) collapsed = false return collapsed } export function keyEvent(keyCode, key) { let event = document.createEvent("Event") event.initEvent("keydown", true, true) event.keyCode = keyCode event.key = event.code = key return event } prosemirror-view-1.23.13/src/domchange.js000066400000000000000000000354471423202571000202770ustar00rootroot00000000000000import {Fragment, DOMParser} from "prosemirror-model" import {Selection, TextSelection} from "prosemirror-state" import {selectionBetween, selectionFromDOM, selectionToDOM} from "./selection" import {selectionCollapsed, keyEvent} from "./dom" import browser from "./browser" // Note that all referencing and parsing is done with the // start-of-operation selection and document, since that's the one // that the DOM represents. If any changes came in in the meantime, // the modification is mapped over those before it is applied, in // readDOMChange. function parseBetween(view, from_, to_) { let {node: parent, fromOffset, toOffset, from, to} = view.docView.parseRange(from_, to_) let domSel = view.root.getSelection(), find = null, anchor = domSel.anchorNode if (anchor && view.dom.contains(anchor.nodeType == 1 ? anchor : anchor.parentNode)) { find = [{node: anchor, offset: domSel.anchorOffset}] if (!selectionCollapsed(domSel)) find.push({node: domSel.focusNode, offset: domSel.focusOffset}) } // Work around issue in Chrome where backspacing sometimes replaces // the deleted content with a random BR node (issues #799, #831) if (browser.chrome && view.lastKeyCode === 8) { for (let off = toOffset; off > fromOffset; off--) { let node = parent.childNodes[off - 1], desc = node.pmViewDesc if (node.nodeName == "BR" && !desc) { toOffset = off; break } if (!desc || desc.size) break } } let startDoc = view.state.doc let parser = view.someProp("domParser") || DOMParser.fromSchema(view.state.schema) let $from = startDoc.resolve(from) let sel = null, doc = parser.parse(parent, { topNode: $from.parent, topMatch: $from.parent.contentMatchAt($from.index()), topOpen: true, from: fromOffset, to: toOffset, preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true, editableContent: true, findPositions: find, ruleFromNode, context: $from }) if (find && find[0].pos != null) { let anchor = find[0].pos, head = find[1] && find[1].pos if (head == null) head = anchor sel = {anchor: anchor + from, head: head + from} } return {doc, sel, from, to} } function ruleFromNode(dom) { let desc = dom.pmViewDesc if (desc) { return desc.parseRule() } else if (dom.nodeName == "BR" && dom.parentNode) { // Safari replaces the list item or table cell with a BR // directly in the list node (?!) if you delete the last // character in a list item or table cell (#708, #862) if (browser.safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) { let skip = document.createElement("div") skip.appendChild(document.createElement("li")) return {skip} } else if (dom.parentNode.lastChild == dom || browser.safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) { return {ignore: true} } } else if (dom.nodeName == "IMG" && dom.getAttribute("mark-placeholder")) { return {ignore: true} } } export function readDOMChange(view, from, to, typeOver, addedNodes) { if (from < 0) { let origin = view.lastSelectionTime > Date.now() - 50 ? view.lastSelectionOrigin : null let newSel = selectionFromDOM(view, origin) if (newSel && !view.state.selection.eq(newSel)) { let tr = view.state.tr.setSelection(newSel) if (origin == "pointer") tr.setMeta("pointer", true) else if (origin == "key") tr.scrollIntoView() view.dispatch(tr) } return } let $before = view.state.doc.resolve(from) let shared = $before.sharedDepth(to) from = $before.before(shared + 1) to = view.state.doc.resolve(to).after(shared + 1) let sel = view.state.selection let parse = parseBetween(view, from, to) // Chrome sometimes leaves the cursor before the inserted text when // composing after a cursor wrapper. This moves it forward. if (browser.chrome && view.cursorWrapper && parse.sel && parse.sel.anchor == view.cursorWrapper.deco.from) { let text = view.cursorWrapper.deco.type.toDOM.nextSibling let size = text && text.nodeValue ? text.nodeValue.length : 1 parse.sel = {anchor: parse.sel.anchor + size, head: parse.sel.anchor + size} } let doc = view.state.doc, compare = doc.slice(parse.from, parse.to) let preferredPos, preferredSide // Prefer anchoring to end when Backspace is pressed if (view.lastKeyCode === 8 && Date.now() - 100 < view.lastKeyCodeTime) { preferredPos = view.state.selection.to preferredSide = "end" } else { preferredPos = view.state.selection.from preferredSide = "start" } view.lastKeyCode = null let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide) if ((browser.ios && view.lastIOSEnter > Date.now() - 225 || browser.android) && addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P") && (!change || change.endA >= change.endB) && view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) { view.lastIOSEnter = 0 return } if (!change) { if (typeOver && sel instanceof TextSelection && !sel.empty && sel.$head.sameParent(sel.$anchor) && !view.composing && !(parse.sel && parse.sel.anchor != parse.sel.head)) { change = {start: sel.from, endA: sel.to, endB: sel.to} } else { if (parse.sel) { let sel = resolveSelection(view, view.state.doc, parse.sel) if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel)) } return } } view.domChangeCount++ // Handle the case where overwriting a selection by typing matches // the start or end of the selected content, creating a change // that's smaller than what was actually overwritten. if (view.state.selection.from < view.state.selection.to && change.start == change.endB && view.state.selection instanceof TextSelection) { if (change.start > view.state.selection.from && change.start <= view.state.selection.from + 2 && view.state.selection.from >= parse.from) { change.start = view.state.selection.from } else if (change.endA < view.state.selection.to && change.endA >= view.state.selection.to - 2 && view.state.selection.to <= parse.to) { change.endB += (view.state.selection.to - change.endA) change.endA = view.state.selection.to } } // IE11 will insert a non-breaking space _ahead_ of the space after // the cursor space when adding a space before another space. When // that happened, adjust the change to cover the space instead. if (browser.ie && browser.ie_version <= 11 && change.endB == change.start + 1 && change.endA == change.start && change.start > parse.from && parse.doc.textBetween(change.start - parse.from - 1, change.start - parse.from + 1) == " \u00a0") { change.start-- change.endA-- change.endB-- } let $from = parse.doc.resolveNoCache(change.start - parse.from) let $to = parse.doc.resolveNoCache(change.endB - parse.from) let inlineChange = $from.sameParent($to) && $from.parent.inlineContent let nextSel // If this looks like the effect of pressing Enter (or was recorded // as being an iOS enter press), just dispatch an Enter key instead. if (((browser.ios && view.lastIOSEnter > Date.now() - 225 && (!inlineChange || addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P"))) || (!inlineChange && $from.pos < parse.doc.content.size && (nextSel = Selection.findFrom(parse.doc.resolve($from.pos + 1), 1, true)) && nextSel.head == $to.pos)) && view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) { view.lastIOSEnter = 0 return } // Same for backspace if (view.state.selection.anchor > change.start && looksLikeJoin(doc, change.start, change.endA, $from, $to) && view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) { if (browser.android && browser.chrome) view.domObserver.suppressSelectionUpdates() // #820 return } // Chrome Android will occasionally, during composition, delete the // entire composition and then immediately insert it again. This is // used to detect that situation. if (browser.chrome && browser.android && change.toB == change.from) view.lastAndroidDelete = Date.now() // This tries to detect Android virtual keyboard // enter-and-pick-suggestion action. That sometimes (see issue // #1059) first fires a DOM mutation, before moving the selection to // the newly created block. And then, because ProseMirror cleans up // the DOM selection, it gives up moving the selection entirely, // leaving the cursor in the wrong place. When that happens, we drop // the new paragraph from the initial change, and fire a simulated // enter key afterwards. if (browser.android && !inlineChange && $from.start() != $to.start() && $to.parentOffset == 0 && $from.depth == $to.depth && parse.sel && parse.sel.anchor == parse.sel.head && parse.sel.head == change.endA) { change.endB -= 2 $to = parse.doc.resolveNoCache(change.endB - parse.from) setTimeout(() => { view.someProp("handleKeyDown", function (f) { return f(view, keyEvent(13, "Enter")); }) }, 20) } let chFrom = change.start, chTo = change.endA let tr, storedMarks, markChange, $from1 if (inlineChange) { if ($from.pos == $to.pos) { // Deletion // IE11 sometimes weirdly moves the DOM selection around after // backspacing out the first element in a textblock if (browser.ie && browser.ie_version <= 11 && $from.parentOffset == 0) { view.domObserver.suppressSelectionUpdates() setTimeout(() => selectionToDOM(view), 20) } tr = view.state.tr.delete(chFrom, chTo) storedMarks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA)) } else if ( // Adding or removing a mark change.endA == change.endB && ($from1 = doc.resolve(change.start)) && (markChange = isMarkChange($from.parent.content.cut($from.parentOffset, $to.parentOffset), $from1.parent.content.cut($from1.parentOffset, change.endA - $from1.start()))) ) { tr = view.state.tr if (markChange.type == "add") tr.addMark(chFrom, chTo, markChange.mark) else tr.removeMark(chFrom, chTo, markChange.mark) } else if ($from.parent.child($from.index()).isText && $from.index() == $to.index() - ($to.textOffset ? 0 : 1)) { // Both positions in the same text node -- simply insert text let text = $from.parent.textBetween($from.parentOffset, $to.parentOffset) if (view.someProp("handleTextInput", f => f(view, chFrom, chTo, text))) return tr = view.state.tr.insertText(text, chFrom, chTo) } } if (!tr) tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from)) if (parse.sel) { let sel = resolveSelection(view, tr.doc, parse.sel) // Chrome Android will sometimes, during composition, report the // selection in the wrong place. If it looks like that is // happening, don't update the selection. // Edge just doesn't move the cursor forward when you start typing // in an empty block or between br nodes. if (sel && !(browser.chrome && browser.android && view.composing && sel.empty && (change.start != change.endB || view.lastAndroidDelete < Date.now() - 100) && (sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) || browser.ie && sel.empty && sel.head == chFrom)) tr.setSelection(sel) } if (storedMarks) tr.ensureMarks(storedMarks) view.dispatch(tr.scrollIntoView()) } function resolveSelection(view, doc, parsedSel) { if (Math.max(parsedSel.anchor, parsedSel.head) > doc.content.size) return null return selectionBetween(view, doc.resolve(parsedSel.anchor), doc.resolve(parsedSel.head)) } // : (Fragment, Fragment) → ?{mark: Mark, type: string} // Given two same-length, non-empty fragments of inline content, // determine whether the first could be created from the second by // removing or adding a single mark type. function isMarkChange(cur, prev) { let curMarks = cur.firstChild.marks, prevMarks = prev.firstChild.marks let added = curMarks, removed = prevMarks, type, mark, update for (let i = 0; i < prevMarks.length; i++) added = prevMarks[i].removeFromSet(added) for (let i = 0; i < curMarks.length; i++) removed = curMarks[i].removeFromSet(removed) if (added.length == 1 && removed.length == 0) { mark = added[0] type = "add" update = node => node.mark(mark.addToSet(node.marks)) } else if (added.length == 0 && removed.length == 1) { mark = removed[0] type = "remove" update = node => node.mark(mark.removeFromSet(node.marks)) } else { return null } let updated = [] for (let i = 0; i < prev.childCount; i++) updated.push(update(prev.child(i))) if (Fragment.from(updated).eq(cur)) return {mark, type} } function looksLikeJoin(old, start, end, $newStart, $newEnd) { if (!$newStart.parent.isTextblock || // The content must have shrunk end - start <= $newEnd.pos - $newStart.pos || // newEnd must point directly at or after the end of the block that newStart points into skipClosingAndOpening($newStart, true, false) < $newEnd.pos) return false let $start = old.resolve(start) // Start must be at the end of a block if ($start.parentOffset < $start.parent.content.size || !$start.parent.isTextblock) return false let $next = old.resolve(skipClosingAndOpening($start, true, true)) // The next textblock must start before end and end near it if (!$next.parent.isTextblock || $next.pos > end || skipClosingAndOpening($next, true, false) < end) return false // The fragments after the join point must match return $newStart.parent.content.cut($newStart.parentOffset).eq($next.parent.content) } function skipClosingAndOpening($pos, fromEnd, mayOpen) { let depth = $pos.depth, end = fromEnd ? $pos.end() : $pos.pos while (depth > 0 && (fromEnd || $pos.indexAfter(depth) == $pos.node(depth).childCount)) { depth-- end++ fromEnd = false } if (mayOpen) { let next = $pos.node(depth).maybeChild($pos.indexAfter(depth)) while (next && !next.isLeaf) { next = next.firstChild end++ } } return end } function findDiff(a, b, pos, preferredPos, preferredSide) { let start = a.findDiffStart(b, pos) if (start == null) return null let {a: endA, b: endB} = a.findDiffEnd(b, pos + a.size, pos + b.size) if (preferredSide == "end") { let adjust = Math.max(0, start - Math.min(endA, endB)) preferredPos -= endA + adjust - start } if (endA < start && a.size < b.size) { let move = preferredPos <= start && preferredPos >= endA ? start - preferredPos : 0 start -= move endB = start + (endB - endA) endA = start } else if (endB < start) { let move = preferredPos <= start && preferredPos >= endB ? start - preferredPos : 0 start -= move endA = start + (endA - endB) endB = start } return {start, endA, endB} } prosemirror-view-1.23.13/src/domcoords.js000066400000000000000000000452671423202571000203440ustar00rootroot00000000000000import {nodeSize, textRange, parentNode} from "./dom" import browser from "./browser" function windowRect(doc) { return {left: 0, right: doc.documentElement.clientWidth, top: 0, bottom: doc.documentElement.clientHeight} } function getSide(value, side) { return typeof value == "number" ? value : value[side] } function clientRect(node) { let rect = node.getBoundingClientRect() // Adjust for elements with style "transform: scale()" let scaleX = (rect.width / node.offsetWidth) || 1 let scaleY = (rect.height / node.offsetHeight) || 1 // Make sure scrollbar width isn't included in the rectangle return {left: rect.left, right: rect.left + node.clientWidth * scaleX, top: rect.top, bottom: rect.top + node.clientHeight * scaleY} } export function scrollRectIntoView(view, rect, startDOM) { let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5 let doc = view.dom.ownerDocument for (let parent = startDOM || view.dom;; parent = parentNode(parent)) { if (!parent) break if (parent.nodeType != 1) continue let atTop = parent == doc.body || parent.nodeType != 1 let bounding = atTop ? windowRect(doc) : clientRect(parent) let moveX = 0, moveY = 0 if (rect.top < bounding.top + getSide(scrollThreshold, "top")) moveY = -(bounding.top - rect.top + getSide(scrollMargin, "top")) else if (rect.bottom > bounding.bottom - getSide(scrollThreshold, "bottom")) moveY = rect.bottom - bounding.bottom + getSide(scrollMargin, "bottom") if (rect.left < bounding.left + getSide(scrollThreshold, "left")) moveX = -(bounding.left - rect.left + getSide(scrollMargin, "left")) else if (rect.right > bounding.right - getSide(scrollThreshold, "right")) moveX = rect.right - bounding.right + getSide(scrollMargin, "right") if (moveX || moveY) { if (atTop) { doc.defaultView.scrollBy(moveX, moveY) } else { let startX = parent.scrollLeft, startY = parent.scrollTop if (moveY) parent.scrollTop += moveY if (moveX) parent.scrollLeft += moveX let dX = parent.scrollLeft - startX, dY = parent.scrollTop - startY rect = {left: rect.left - dX, top: rect.top - dY, right: rect.right - dX, bottom: rect.bottom - dY} } } if (atTop) break } } // Store the scroll position of the editor's parent nodes, along with // the top position of an element near the top of the editor, which // will be used to make sure the visible viewport remains stable even // when the size of the content above changes. export function storeScrollPos(view) { let rect = view.dom.getBoundingClientRect(), startY = Math.max(0, rect.top) let refDOM, refTop for (let x = (rect.left + rect.right) / 2, y = startY + 1; y < Math.min(innerHeight, rect.bottom); y += 5) { let dom = view.root.elementFromPoint(x, y) if (dom == view.dom || !view.dom.contains(dom)) continue let localRect = dom.getBoundingClientRect() if (localRect.top >= startY - 20) { refDOM = dom refTop = localRect.top break } } return {refDOM, refTop, stack: scrollStack(view.dom)} } function scrollStack(dom) { let stack = [], doc = dom.ownerDocument for (; dom; dom = parentNode(dom)) { stack.push({dom, top: dom.scrollTop, left: dom.scrollLeft}) if (dom == doc) break } return stack } // Reset the scroll position of the editor's parent nodes to that what // it was before, when storeScrollPos was called. export function resetScrollPos({refDOM, refTop, stack}) { let newRefTop = refDOM ? refDOM.getBoundingClientRect().top : 0 restoreScrollStack(stack, newRefTop == 0 ? 0 : newRefTop - refTop) } function restoreScrollStack(stack, dTop) { for (let i = 0; i < stack.length; i++) { let {dom, top, left} = stack[i] if (dom.scrollTop != top + dTop) dom.scrollTop = top + dTop if (dom.scrollLeft != left) dom.scrollLeft = left } } let preventScrollSupported = null // Feature-detects support for .focus({preventScroll: true}), and uses // a fallback kludge when not supported. export function focusPreventScroll(dom) { if (dom.setActive) return dom.setActive() // in IE if (preventScrollSupported) return dom.focus(preventScrollSupported) let stored = scrollStack(dom) dom.focus(preventScrollSupported == null ? { get preventScroll() { preventScrollSupported = {preventScroll: true} return true } } : undefined) if (!preventScrollSupported) { preventScrollSupported = false restoreScrollStack(stored, 0) } } function findOffsetInNode(node, coords) { let closest, dxClosest = 2e8, coordsClosest, offset = 0 let rowBot = coords.top, rowTop = coords.top for (let child = node.firstChild, childIndex = 0; child; child = child.nextSibling, childIndex++) { let rects if (child.nodeType == 1) rects = child.getClientRects() else if (child.nodeType == 3) rects = textRange(child).getClientRects() else continue for (let i = 0; i < rects.length; i++) { let rect = rects[i] if (rect.top <= rowBot && rect.bottom >= rowTop) { rowBot = Math.max(rect.bottom, rowBot) rowTop = Math.min(rect.top, rowTop) let dx = rect.left > coords.left ? rect.left - coords.left : rect.right < coords.left ? coords.left - rect.right : 0 if (dx < dxClosest) { closest = child dxClosest = dx coordsClosest = dx && closest.nodeType == 3 ? {left: rect.right < coords.left ? rect.right : rect.left, top: coords.top} : coords if (child.nodeType == 1 && dx) offset = childIndex + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0) continue } } if (!closest && (coords.left >= rect.right && coords.top >= rect.top || coords.left >= rect.left && coords.top >= rect.bottom)) offset = childIndex + 1 } } if (closest && closest.nodeType == 3) return findOffsetInText(closest, coordsClosest) if (!closest || (dxClosest && closest.nodeType == 1)) return {node, offset} return findOffsetInNode(closest, coordsClosest) } function findOffsetInText(node, coords) { let len = node.nodeValue.length let range = document.createRange() for (let i = 0; i < len; i++) { range.setEnd(node, i + 1) range.setStart(node, i) let rect = singleRect(range, 1) if (rect.top == rect.bottom) continue if (inRect(coords, rect)) return {node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)} } return {node, offset: 0} } function inRect(coords, rect) { return coords.left >= rect.left - 1 && coords.left <= rect.right + 1&& coords.top >= rect.top - 1 && coords.top <= rect.bottom + 1 } function targetKludge(dom, coords) { let parent = dom.parentNode if (parent && /^li$/i.test(parent.nodeName) && coords.left < dom.getBoundingClientRect().left) return parent return dom } function posFromElement(view, elt, coords) { let {node, offset} = findOffsetInNode(elt, coords), bias = -1 if (node.nodeType == 1 && !node.firstChild) { let rect = node.getBoundingClientRect() bias = rect.left != rect.right && coords.left > (rect.left + rect.right) / 2 ? 1 : -1 } return view.docView.posFromDOM(node, offset, bias) } function posFromCaret(view, node, offset, coords) { // Browser (in caretPosition/RangeFromPoint) will agressively // normalize towards nearby inline nodes. Since we are interested in // positions between block nodes too, we first walk up the hierarchy // of nodes to see if there are block nodes that the coordinates // fall outside of. If so, we take the position before/after that // block. If not, we call `posFromDOM` on the raw node/offset. let outside = -1 for (let cur = node;;) { if (cur == view.dom) break let desc = view.docView.nearestDesc(cur, true) if (!desc) return null if (desc.node.isBlock && desc.parent) { let rect = desc.dom.getBoundingClientRect() if (rect.left > coords.left || rect.top > coords.top) outside = desc.posBefore else if (rect.right < coords.left || rect.bottom < coords.top) outside = desc.posAfter else break } cur = desc.dom.parentNode } return outside > -1 ? outside : view.docView.posFromDOM(node, offset) } function elementFromPoint(element, coords, box) { let len = element.childNodes.length if (len && box.top < box.bottom) { for (let startI = Math.max(0, Math.min(len - 1, Math.floor(len * (coords.top - box.top) / (box.bottom - box.top)) - 2)), i = startI;;) { let child = element.childNodes[i] if (child.nodeType == 1) { let rects = child.getClientRects() for (let j = 0; j < rects.length; j++) { let rect = rects[j] if (inRect(coords, rect)) return elementFromPoint(child, coords, rect) } } if ((i = (i + 1) % len) == startI) break } } return element } // Given an x,y position on the editor, get the position in the document. export function posAtCoords(view, coords) { let doc = view.dom.ownerDocument, node, offset if (doc.caretPositionFromPoint) { try { // Firefox throws for this call in hard-to-predict circumstances (#994) let pos = doc.caretPositionFromPoint(coords.left, coords.top) if (pos) ({offsetNode: node, offset} = pos) } catch (_) {} } if (!node && doc.caretRangeFromPoint) { let range = doc.caretRangeFromPoint(coords.left, coords.top) if (range) ({startContainer: node, startOffset: offset} = range) } let elt = (view.root.elementFromPoint ? view.root : doc).elementFromPoint(coords.left, coords.top + 1), pos if (!elt || !view.dom.contains(elt.nodeType != 1 ? elt.parentNode : elt)) { let box = view.dom.getBoundingClientRect() if (!inRect(coords, box)) return null elt = elementFromPoint(view.dom, coords, box) if (!elt) return null } // Safari's caretRangeFromPoint returns nonsense when on a draggable element if (browser.safari) { for (let p = elt; node && p; p = parentNode(p)) if (p.draggable) node = offset = null } elt = targetKludge(elt, coords) if (node) { if (browser.gecko && node.nodeType == 1) { // Firefox will sometimes return offsets into nodes, which // have no actual children, from caretPositionFromPoint (#953) offset = Math.min(offset, node.childNodes.length) // It'll also move the returned position before image nodes, // even if those are behind it. if (offset < node.childNodes.length) { let next = node.childNodes[offset], box if (next.nodeName == "IMG" && (box = next.getBoundingClientRect()).right <= coords.left && box.bottom > coords.top) offset++ } } // Suspiciously specific kludge to work around caret*FromPoint // never returning a position at the end of the document if (node == view.dom && offset == node.childNodes.length - 1 && node.lastChild.nodeType == 1 && coords.top > node.lastChild.getBoundingClientRect().bottom) pos = view.state.doc.content.size // Ignore positions directly after a BR, since caret*FromPoint // 'round up' positions that would be more accurately placed // before the BR node. else if (offset == 0 || node.nodeType != 1 || node.childNodes[offset - 1].nodeName != "BR") pos = posFromCaret(view, node, offset, coords) } if (pos == null) pos = posFromElement(view, elt, coords) let desc = view.docView.nearestDesc(elt, true) return {pos, inside: desc ? desc.posAtStart - desc.border : -1} } function singleRect(object, bias) { let rects = object.getClientRects() return !rects.length ? object.getBoundingClientRect() : rects[bias < 0 ? 0 : rects.length - 1] } const BIDI = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/ // : (EditorView, number, number) → {left: number, top: number, right: number, bottom: number} // Given a position in the document model, get a bounding box of the // character at that position, relative to the window. export function coordsAtPos(view, pos, side) { let {node, offset} = view.docView.domFromPos(pos, side < 0 ? -1 : 1) let supportEmptyRange = browser.webkit || browser.gecko if (node.nodeType == 3) { // These browsers support querying empty text ranges. Prefer that in // bidi context or when at the end of a node. if (supportEmptyRange && (BIDI.test(node.nodeValue) || (side < 0 ? !offset : offset == node.nodeValue.length))) { let rect = singleRect(textRange(node, offset, offset), side) // Firefox returns bad results (the position before the space) // when querying a position directly after line-broken // whitespace. Detect this situation and and kludge around it if (browser.gecko && offset && /\s/.test(node.nodeValue[offset - 1]) && offset < node.nodeValue.length) { let rectBefore = singleRect(textRange(node, offset - 1, offset - 1), -1) if (rectBefore.top == rect.top) { let rectAfter = singleRect(textRange(node, offset, offset + 1), -1) if (rectAfter.top != rect.top) return flattenV(rectAfter, rectAfter.left < rectBefore.left) } } return rect } else { let from = offset, to = offset, takeSide = side < 0 ? 1 : -1 if (side < 0 && !offset) { to++; takeSide = -1 } else if (side >= 0 && offset == node.nodeValue.length) { from--; takeSide = 1 } else if (side < 0) { from-- } else { to ++ } return flattenV(singleRect(textRange(node, from, to), takeSide), takeSide < 0) } } // Return a horizontal line in block context if (!view.state.doc.resolve(pos).parent.inlineContent) { if (offset && (side < 0 || offset == nodeSize(node))) { let before = node.childNodes[offset - 1] if (before.nodeType == 1) return flattenH(before.getBoundingClientRect(), false) } if (offset < nodeSize(node)) { let after = node.childNodes[offset] if (after.nodeType == 1) return flattenH(after.getBoundingClientRect(), true) } return flattenH(node.getBoundingClientRect(), side >= 0) } // Inline, not in text node (this is not Bidi-safe) if (offset && (side < 0 || offset == nodeSize(node))) { let before = node.childNodes[offset - 1] let target = before.nodeType == 3 ? textRange(before, nodeSize(before) - (supportEmptyRange ? 0 : 1)) // BR nodes tend to only return the rectangle before them. // Only use them if they are the last element in their parent : before.nodeType == 1 && (before.nodeName != "BR" || !before.nextSibling) ? before : null if (target) return flattenV(singleRect(target, 1), false) } if (offset < nodeSize(node)) { let after = node.childNodes[offset] while (after.pmViewDesc && after.pmViewDesc.ignoreForCoords) after = after.nextSibling let target = !after ? null : after.nodeType == 3 ? textRange(after, 0, (supportEmptyRange ? 0 : 1)) : after.nodeType == 1 ? after : null if (target) return flattenV(singleRect(target, -1), true) } // All else failed, just try to get a rectangle for the target node return flattenV(singleRect(node.nodeType == 3 ? textRange(node) : node, -side), side >= 0) } function flattenV(rect, left) { if (rect.width == 0) return rect let x = left ? rect.left : rect.right return {top: rect.top, bottom: rect.bottom, left: x, right: x} } function flattenH(rect, top) { if (rect.height == 0) return rect let y = top ? rect.top : rect.bottom return {top: y, bottom: y, left: rect.left, right: rect.right} } function withFlushedState(view, state, f) { let viewState = view.state, active = view.root.activeElement if (viewState != state) view.updateState(state) if (active != view.dom) view.focus() try { return f() } finally { if (viewState != state) view.updateState(viewState) if (active != view.dom && active) active.focus() } } // : (EditorView, number, number) // Whether vertical position motion in a given direction // from a position would leave a text block. function endOfTextblockVertical(view, state, dir) { let sel = state.selection let $pos = dir == "up" ? sel.$from : sel.$to return withFlushedState(view, state, () => { let {node: dom} = view.docView.domFromPos($pos.pos, dir == "up" ? -1 : 1) for (;;) { let nearest = view.docView.nearestDesc(dom, true) if (!nearest) break if (nearest.node.isBlock) { dom = nearest.dom; break } dom = nearest.dom.parentNode } let coords = coordsAtPos(view, $pos.pos, 1) for (let child = dom.firstChild; child; child = child.nextSibling) { let boxes if (child.nodeType == 1) boxes = child.getClientRects() else if (child.nodeType == 3) boxes = textRange(child, 0, child.nodeValue.length).getClientRects() else continue for (let i = 0; i < boxes.length; i++) { let box = boxes[i] if (box.bottom > box.top + 1 && (dir == "up" ? coords.top - box.top > (box.bottom - coords.top) * 2 : box.bottom - coords.bottom > (coords.bottom - box.top) * 2)) return false } } return true }) } const maybeRTL = /[\u0590-\u08ac]/ function endOfTextblockHorizontal(view, state, dir) { let {$head} = state.selection if (!$head.parent.isTextblock) return false let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size let sel = view.root.getSelection() // If the textblock is all LTR, or the browser doesn't support // Selection.modify (Edge), fall back to a primitive approach if (!maybeRTL.test($head.parent.textContent) || !sel.modify) return dir == "left" || dir == "backward" ? atStart : atEnd return withFlushedState(view, state, () => { // This is a huge hack, but appears to be the best we can // currently do: use `Selection.modify` to move the selection by // one character, and see if that moves the cursor out of the // textblock (or doesn't move it at all, when at the start/end of // the document). let oldRange = sel.getRangeAt(0), oldNode = sel.focusNode, oldOff = sel.focusOffset let oldBidiLevel = sel.caretBidiLevel // Only for Firefox sel.modify("move", dir, "character") let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom let result = !parentDOM.contains(sel.focusNode.nodeType == 1 ? sel.focusNode : sel.focusNode.parentNode) || (oldNode == sel.focusNode && oldOff == sel.focusOffset) // Restore the previous selection sel.removeAllRanges() sel.addRange(oldRange) if (oldBidiLevel != null) sel.caretBidiLevel = oldBidiLevel return result }) } let cachedState = null, cachedDir = null, cachedResult = false export function endOfTextblock(view, state, dir) { if (cachedState == state && cachedDir == dir) return cachedResult cachedState = state; cachedDir = dir return cachedResult = dir == "up" || dir == "down" ? endOfTextblockVertical(view, state, dir) : endOfTextblockHorizontal(view, state, dir) } prosemirror-view-1.23.13/src/domobserver.js000066400000000000000000000214011423202571000206620ustar00rootroot00000000000000import browser from "./browser" import {domIndex, isEquivalentPosition} from "./dom" import {hasFocusAndSelection, selectionToDOM} from "./selection" const observeOptions = { childList: true, characterData: true, characterDataOldValue: true, attributes: true, attributeOldValue: true, subtree: true } // IE11 has very broken mutation observers, so we also listen to DOMCharacterDataModified const useCharData = browser.ie && browser.ie_version <= 11 class SelectionState { constructor() { this.anchorNode = this.anchorOffset = this.focusNode = this.focusOffset = null } set(sel) { this.anchorNode = sel.anchorNode; this.anchorOffset = sel.anchorOffset this.focusNode = sel.focusNode; this.focusOffset = sel.focusOffset } eq(sel) { return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset && sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset } } export class DOMObserver { constructor(view, handleDOMChange) { this.view = view this.handleDOMChange = handleDOMChange this.queue = [] this.flushingSoon = -1 this.observer = window.MutationObserver && new window.MutationObserver(mutations => { for (let i = 0; i < mutations.length; i++) this.queue.push(mutations[i]) // IE11 will sometimes (on backspacing out a single character // text node after a BR node) call the observer callback // before actually updating the DOM, which will cause // ProseMirror to miss the change (see #930) if (browser.ie && browser.ie_version <= 11 && mutations.some( m => m.type == "childList" && m.removedNodes.length || m.type == "characterData" && m.oldValue.length > m.target.nodeValue.length)) this.flushSoon() else this.flush() }) this.currentSelection = new SelectionState if (useCharData) { this.onCharData = e => { this.queue.push({target: e.target, type: "characterData", oldValue: e.prevValue}) this.flushSoon() } } this.onSelectionChange = this.onSelectionChange.bind(this) this.suppressingSelectionUpdates = false } flushSoon() { if (this.flushingSoon < 0) this.flushingSoon = window.setTimeout(() => { this.flushingSoon = -1; this.flush() }, 20) } forceFlush() { if (this.flushingSoon > -1) { window.clearTimeout(this.flushingSoon) this.flushingSoon = -1 this.flush() } } start() { if (this.observer) this.observer.observe(this.view.dom, observeOptions) if (useCharData) this.view.dom.addEventListener("DOMCharacterDataModified", this.onCharData) this.connectSelection() } stop() { if (this.observer) { let take = this.observer.takeRecords() if (take.length) { for (let i = 0; i < take.length; i++) this.queue.push(take[i]) window.setTimeout(() => this.flush(), 20) } this.observer.disconnect() } if (useCharData) this.view.dom.removeEventListener("DOMCharacterDataModified", this.onCharData) this.disconnectSelection() } connectSelection() { this.view.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange) } disconnectSelection() { this.view.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange) } suppressSelectionUpdates() { this.suppressingSelectionUpdates = true setTimeout(() => this.suppressingSelectionUpdates = false, 50) } onSelectionChange() { if (!hasFocusAndSelection(this.view)) return if (this.suppressingSelectionUpdates) return selectionToDOM(this.view) // Deletions on IE11 fire their events in the wrong order, giving // us a selection change event before the DOM changes are // reported. if (browser.ie && browser.ie_version <= 11 && !this.view.state.selection.empty) { let sel = this.view.root.getSelection() // Selection.isCollapsed isn't reliable on IE if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset)) return this.flushSoon() } this.flush() } setCurSelection() { this.currentSelection.set(this.view.root.getSelection()) } ignoreSelectionChange(sel) { if (sel.rangeCount == 0) return true let container = sel.getRangeAt(0).commonAncestorContainer let desc = this.view.docView.nearestDesc(container) if (desc && desc.ignoreMutation({type: "selection", target: container.nodeType == 3 ? container.parentNode : container})) { this.setCurSelection() return true } } flush() { if (!this.view.docView || this.flushingSoon > -1) return let mutations = this.observer ? this.observer.takeRecords() : [] if (this.queue.length) { mutations = this.queue.concat(mutations) this.queue.length = 0 } let sel = this.view.root.getSelection() let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel) let from = -1, to = -1, typeOver = false, added = [] if (this.view.editable) { for (let i = 0; i < mutations.length; i++) { let result = this.registerMutation(mutations[i], added) if (result) { from = from < 0 ? result.from : Math.min(result.from, from) to = to < 0 ? result.to : Math.max(result.to, to) if (result.typeOver) typeOver = true } } } if (browser.gecko && added.length > 1) { let brs = added.filter(n => n.nodeName == "BR") if (brs.length == 2) { let [a, b] = brs if (a.parentNode && a.parentNode.parentNode == b.parentNode) b.remove() else a.remove() } } if (from > -1 || newSel) { if (from > -1) { this.view.docView.markDirty(from, to) checkCSS(this.view) } this.handleDOMChange(from, to, typeOver, added) if (this.view.docView && this.view.docView.dirty) this.view.updateState(this.view.state) else if (!this.currentSelection.eq(sel)) selectionToDOM(this.view) this.currentSelection.set(sel) } } registerMutation(mut, added) { // Ignore mutations inside nodes that were already noted as inserted if (added.indexOf(mut.target) > -1) return null let desc = this.view.docView.nearestDesc(mut.target) if (mut.type == "attributes" && (desc == this.view.docView || mut.attributeName == "contenteditable" || // Firefox sometimes fires spurious events for null/empty styles (mut.attributeName == "style" && !mut.oldValue && !mut.target.getAttribute("style")))) return null if (!desc || desc.ignoreMutation(mut)) return null if (mut.type == "childList") { for (let i = 0; i < mut.addedNodes.length; i++) added.push(mut.addedNodes[i]) if (desc.contentDOM && desc.contentDOM != desc.dom && !desc.contentDOM.contains(mut.target)) return {from: desc.posBefore, to: desc.posAfter} let prev = mut.previousSibling, next = mut.nextSibling if (browser.ie && browser.ie_version <= 11 && mut.addedNodes.length) { // IE11 gives us incorrect next/prev siblings for some // insertions, so if there are added nodes, recompute those for (let i = 0; i < mut.addedNodes.length; i++) { let {previousSibling, nextSibling} = mut.addedNodes[i] if (!previousSibling || Array.prototype.indexOf.call(mut.addedNodes, previousSibling) < 0) prev = previousSibling if (!nextSibling || Array.prototype.indexOf.call(mut.addedNodes, nextSibling) < 0) next = nextSibling } } let fromOffset = prev && prev.parentNode == mut.target ? domIndex(prev) + 1 : 0 let from = desc.localPosFromDOM(mut.target, fromOffset, -1) let toOffset = next && next.parentNode == mut.target ? domIndex(next) : mut.target.childNodes.length let to = desc.localPosFromDOM(mut.target, toOffset, 1) return {from, to} } else if (mut.type == "attributes") { return {from: desc.posAtStart - desc.border, to: desc.posAtEnd + desc.border} } else { // "characterData" return { from: desc.posAtStart, to: desc.posAtEnd, // An event was generated for a text change that didn't change // any text. Mark the dom change to fall back to assuming the // selection was typed over with an identical value if it can't // find another change. typeOver: mut.target.nodeValue == mut.oldValue } } } } let cssChecked = false function checkCSS(view) { if (cssChecked) return cssChecked = true if (getComputedStyle(view.dom).whiteSpace == "normal") console["warn"]("ProseMirror expects the CSS white-space property to be set, preferably to 'pre-wrap'. It is recommended to load style/prosemirror.css from the prosemirror-view package.") } prosemirror-view-1.23.13/src/index.js000066400000000000000000000665531423202571000174630ustar00rootroot00000000000000import {NodeSelection} from "prosemirror-state" import {scrollRectIntoView, posAtCoords, coordsAtPos, endOfTextblock, storeScrollPos, resetScrollPos, focusPreventScroll} from "./domcoords" import {docViewDesc} from "./viewdesc" import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition} from "./input" import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection" import {Decoration, viewDecorations} from "./decoration" import browser from "./browser" export {Decoration, DecorationSet} from "./decoration" // Exported for testing export {serializeForClipboard as __serializeForClipboard, parseFromClipboard as __parseFromClipboard} from "./clipboard" export {endComposition as __endComposition} from "./input" // ::- An editor view manages the DOM structure that represents an // editable document. Its state and behavior are determined by its // [props](#view.DirectEditorProps). export class EditorView { // :: (?union, DirectEditorProps) // Create a view. `place` may be a DOM node that the editor should // be appended to, a function that will place it into the document, // or an object whose `mount` property holds the node to use as the // document container. If it is `null`, the editor will not be added // to the document. constructor(place, props) { this._props = props // :: EditorState // The view's current [state](#state.EditorState). this.state = props.state this.directPlugins = props.plugins || [] this.directPlugins.forEach(checkStateComponent) this.dispatch = this.dispatch.bind(this) this._root = null this.focused = false // Kludge used to work around a Chrome bug this.trackWrites = null // :: dom.Element // An editable DOM node containing the document. (You probably // should not directly interfere with its content.) this.dom = (place && place.mount) || document.createElement("div") if (place) { if (place.appendChild) place.appendChild(this.dom) else if (place.apply) place(this.dom) else if (place.mount) this.mounted = true } // :: bool // Indicates whether the editor is currently [editable](#view.EditorProps.editable). this.editable = getEditable(this) this.markCursor = null this.cursorWrapper = null updateCursorWrapper(this) this.nodeViews = buildNodeViews(this) this.docView = docViewDesc(this.state.doc, computeDocDeco(this), viewDecorations(this), this.dom, this) this.lastSelectedViewDesc = null // :: ?{slice: Slice, move: bool} // When editor content is being dragged, this object contains // information about the dragged slice and whether it is being // copied or moved. At any other time, it is null. this.dragging = null initInput(this) this.prevDirectPlugins = [] this.pluginViews = [] this.updatePluginViews() } // composing:: boolean // Holds `true` when a // [composition](https://w3c.github.io/uievents/#events-compositionevents) // is active. // :: DirectEditorProps // The view's current [props](#view.EditorProps). get props() { if (this._props.state != this.state) { let prev = this._props this._props = {} for (let name in prev) this._props[name] = prev[name] this._props.state = this.state } return this._props } // :: (DirectEditorProps) // Update the view's props. Will immediately cause an update to // the DOM. update(props) { if (props.handleDOMEvents != this._props.handleDOMEvents) ensureListeners(this) this._props = props if (props.plugins) { props.plugins.forEach(checkStateComponent) this.directPlugins = props.plugins } this.updateStateInner(props.state, true) } // :: (DirectEditorProps) // Update the view by updating existing props object with the object // given as argument. Equivalent to `view.update(Object.assign({}, // view.props, props))`. setProps(props) { let updated = {} for (let name in this._props) updated[name] = this._props[name] updated.state = this.state for (let name in props) updated[name] = props[name] this.update(updated) } // :: (EditorState) // Update the editor's `state` prop, without touching any of the // other props. updateState(state) { this.updateStateInner(state, this.state.plugins != state.plugins) } updateStateInner(state, reconfigured) { let prev = this.state, redraw = false, updateSel = false // When stored marks are added, stop composition, so that they can // be displayed. if (state.storedMarks && this.composing) { clearComposition(this) updateSel = true } this.state = state if (reconfigured) { let nodeViews = buildNodeViews(this) if (changedNodeViews(nodeViews, this.nodeViews)) { this.nodeViews = nodeViews redraw = true } ensureListeners(this) } this.editable = getEditable(this) updateCursorWrapper(this) let innerDeco = viewDecorations(this), outerDeco = computeDocDeco(this) let scroll = reconfigured ? "reset" : state.scrollToSelection > prev.scrollToSelection ? "to selection" : "preserve" let updateDoc = redraw || !this.docView.matchesNode(state.doc, outerDeco, innerDeco) if (updateDoc || !state.selection.eq(prev.selection)) updateSel = true let oldScrollPos = scroll == "preserve" && updateSel && this.dom.style.overflowAnchor == null && storeScrollPos(this) if (updateSel) { this.domObserver.stop() // Work around an issue in Chrome, IE, and Edge where changing // the DOM around an active selection puts it into a broken // state where the thing the user sees differs from the // selection reported by the Selection object (#710, #973, // #1011, #1013, #1035). let forceSelUpdate = updateDoc && (browser.ie || browser.chrome) && !this.composing && !prev.selection.empty && !state.selection.empty && selectionContextChanged(prev.selection, state.selection) if (updateDoc) { // If the node that the selection points into is written to, // Chrome sometimes starts misreporting the selection, so this // tracks that and forces a selection reset when our update // did write to the node. let chromeKludge = browser.chrome ? (this.trackWrites = this.root.getSelection().focusNode) : null if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) { this.docView.updateOuterDeco([]) this.docView.destroy() this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this) } if (chromeKludge && !this.trackWrites) forceSelUpdate = true } // Work around for an issue where an update arriving right between // a DOM selection change and the "selectionchange" event for it // can cause a spurious DOM selection update, disrupting mouse // drag selection. if (forceSelUpdate || !(this.mouseDown && this.domObserver.currentSelection.eq(this.root.getSelection()) && anchorInRightPlace(this))) { selectionToDOM(this, forceSelUpdate) } else { syncNodeSelection(this, state.selection) this.domObserver.setCurSelection() } this.domObserver.start() } this.updatePluginViews(prev) if (scroll == "reset") { this.dom.scrollTop = 0 } else if (scroll == "to selection") { let startDOM = this.root.getSelection().focusNode if (this.someProp("handleScrollToSelection", f => f(this))) {} // Handled else if (state.selection instanceof NodeSelection) scrollRectIntoView(this, this.docView.domAfterPos(state.selection.from).getBoundingClientRect(), startDOM) else scrollRectIntoView(this, this.coordsAtPos(state.selection.head, 1), startDOM) } else if (oldScrollPos) { resetScrollPos(oldScrollPos) } } destroyPluginViews() { let view while (view = this.pluginViews.pop()) if (view.destroy) view.destroy() } updatePluginViews(prevState) { if (!prevState || prevState.plugins != this.state.plugins || this.directPlugins != this.prevDirectPlugins) { this.prevDirectPlugins = this.directPlugins this.destroyPluginViews() for (let i = 0; i < this.directPlugins.length; i++) { let plugin = this.directPlugins[i] if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this)) } for (let i = 0; i < this.state.plugins.length; i++) { let plugin = this.state.plugins[i] if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this)) } } else { for (let i = 0; i < this.pluginViews.length; i++) { let pluginView = this.pluginViews[i] if (pluginView.update) pluginView.update(this, prevState) } } } // :: (string, ?(prop: *) → *) → * // Goes over the values of a prop, first those provided directly, // then those from plugins given to the view, then from plugins in // the state (in order), and calls `f` every time a non-undefined // value is found. When `f` returns a truthy value, that is // immediately returned. When `f` isn't provided, it is treated as // the identity function (the prop value is returned directly). someProp(propName, f) { let prop = this._props && this._props[propName], value if (prop != null && (value = f ? f(prop) : prop)) return value for (let i = 0; i < this.directPlugins.length; i++) { let prop = this.directPlugins[i].props[propName] if (prop != null && (value = f ? f(prop) : prop)) return value } let plugins = this.state.plugins if (plugins) for (let i = 0; i < plugins.length; i++) { let prop = plugins[i].props[propName] if (prop != null && (value = f ? f(prop) : prop)) return value } } // :: () → bool // Query whether the view has focus. hasFocus() { return this.root.activeElement == this.dom } // :: () // Focus the editor. focus() { this.domObserver.stop() if (this.editable) focusPreventScroll(this.dom) selectionToDOM(this) this.domObserver.start() } // :: union // Get the document root in which the editor exists. This will // usually be the top-level `document`, but might be a [shadow // DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) // root if the editor is inside one. get root() { let cached = this._root if (cached == null) for (let search = this.dom.parentNode; search; search = search.parentNode) { if (search.nodeType == 9 || (search.nodeType == 11 && search.host)) { if (!search.getSelection) Object.getPrototypeOf(search).getSelection = () => document.getSelection() return this._root = search } } return cached || document } // :: ({left: number, top: number}) → ?{pos: number, inside: number} // Given a pair of viewport coordinates, return the document // position that corresponds to them. May return null if the given // coordinates aren't inside of the editor. When an object is // returned, its `pos` property is the position nearest to the // coordinates, and its `inside` property holds the position of the // inner node that the position falls inside of, or -1 if it is at // the top level, not in any node. posAtCoords(coords) { return posAtCoords(this, coords) } // :: (number, number) → {left: number, right: number, top: number, bottom: number} // Returns the viewport rectangle at a given document position. // `left` and `right` will be the same number, as this returns a // flat cursor-ish rectangle. If the position is between two things // that aren't directly adjacent, `side` determines which element is // used. When < 0, the element before the position is used, // otherwise the element after. coordsAtPos(pos, side = 1) { return coordsAtPos(this, pos, side) } // :: (number, number) → {node: dom.Node, offset: number} // Find the DOM position that corresponds to the given document // position. When `side` is negative, find the position as close as // possible to the content before the position. When positive, // prefer positions close to the content after the position. When // zero, prefer as shallow a position as possible. // // Note that you should **not** mutate the editor's internal DOM, // only inspect it (and even that is usually not necessary). domAtPos(pos, side = 0) { return this.docView.domFromPos(pos, side) } // :: (number) → ?dom.Node // Find the DOM node that represents the document node after the // given position. May return `null` when the position doesn't point // in front of a node or if the node is inside an opaque node view. // // This is intended to be able to call things like // `getBoundingClientRect` on that DOM node. Do **not** mutate the // editor DOM directly, or add styling this way, since that will be // immediately overriden by the editor as it redraws the node. nodeDOM(pos) { let desc = this.docView.descAt(pos) return desc ? desc.nodeDOM : null } // :: (dom.Node, number, ?number) → number // Find the document position that corresponds to a given DOM // position. (Whenever possible, it is preferable to inspect the // document structure directly, rather than poking around in the // DOM, but sometimes—for example when interpreting an event // target—you don't have a choice.) // // The `bias` parameter can be used to influence which side of a DOM // node to use when the position is inside a leaf node. posAtDOM(node, offset, bias = -1) { let pos = this.docView.posFromDOM(node, offset, bias) if (pos == null) throw new RangeError("DOM position not inside the editor") return pos } // :: (union<"up", "down", "left", "right", "forward", "backward">, ?EditorState) → bool // Find out whether the selection is at the end of a textblock when // moving in a given direction. When, for example, given `"left"`, // it will return true if moving left from the current cursor // position would leave that position's parent textblock. Will apply // to the view's current state by default, but it is possible to // pass a different state. endOfTextblock(dir, state) { return endOfTextblock(this, state || this.state, dir) } // :: () // Removes the editor from the DOM and destroys all [node // views](#view.NodeView). destroy() { if (!this.docView) return destroyInput(this) this.destroyPluginViews() if (this.mounted) { this.docView.update(this.state.doc, [], viewDecorations(this), this) this.dom.textContent = "" } else if (this.dom.parentNode) { this.dom.parentNode.removeChild(this.dom) } this.docView.destroy() this.docView = null } // :: boolean // This is true when the view has been // [destroyed](#view.EditorView.destroy) (and thus should not be // used anymore). get isDestroyed() { return this.docView == null } // Used for testing. dispatchEvent(event) { return dispatchEvent(this, event) } // :: (Transaction) // Dispatch a transaction. Will call // [`dispatchTransaction`](#view.DirectEditorProps.dispatchTransaction) // when given, and otherwise defaults to applying the transaction to // the current state and calling // [`updateState`](#view.EditorView.updateState) with the result. // This method is bound to the view instance, so that it can be // easily passed around. dispatch(tr) { let dispatchTransaction = this._props.dispatchTransaction if (dispatchTransaction) dispatchTransaction.call(this, tr) else this.updateState(this.state.apply(tr)) } } function computeDocDeco(view) { let attrs = Object.create(null) attrs.class = "ProseMirror" attrs.contenteditable = String(view.editable) attrs.translate = "no" view.someProp("attributes", value => { if (typeof value == "function") value = value(view.state) if (value) for (let attr in value) { if (attr == "class") attrs.class += " " + value[attr] if (attr == "style") { attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr] } else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName") attrs[attr] = String(value[attr]) } }) return [Decoration.node(0, view.state.doc.content.size, attrs)] } function updateCursorWrapper(view) { if (view.markCursor) { let dom = document.createElement("img") dom.className = "ProseMirror-separator" dom.setAttribute("mark-placeholder", "true") dom.setAttribute("alt", "") view.cursorWrapper = {dom, deco: Decoration.widget(view.state.selection.head, dom, {raw: true, marks: view.markCursor})} } else { view.cursorWrapper = null } } function getEditable(view) { return !view.someProp("editable", value => value(view.state) === false) } function selectionContextChanged(sel1, sel2) { let depth = Math.min(sel1.$anchor.sharedDepth(sel1.head), sel2.$anchor.sharedDepth(sel2.head)) return sel1.$anchor.start(depth) != sel2.$anchor.start(depth) } function buildNodeViews(view) { let result = {} view.someProp("nodeViews", obj => { for (let prop in obj) if (!Object.prototype.hasOwnProperty.call(result, prop)) result[prop] = obj[prop] }) return result } function changedNodeViews(a, b) { let nA = 0, nB = 0 for (let prop in a) { if (a[prop] != b[prop]) return true nA++ } for (let _ in b) nB++ return nA != nB } function checkStateComponent(plugin) { if (plugin.spec.state || plugin.spec.filterTransaction || plugin.spec.appendTransaction) throw new RangeError("Plugins passed directly to the view must not have a state component") } // EditorProps:: interface // // Props are configuration values that can be passed to an editor view // or included in a plugin. This interface lists the supported props. // // The various event-handling functions may all return `true` to // indicate that they handled the given event. The view will then take // care to call `preventDefault` on the event, except with // `handleDOMEvents`, where the handler itself is responsible for that. // // How a prop is resolved depends on the prop. Handler functions are // called one at a time, starting with the base props and then // searching through the plugins (in order of appearance) until one of // them returns true. For some props, the first plugin that yields a // value gets precedence. // // handleDOMEvents:: ?Object<(view: EditorView, event: dom.Event) → bool> // Can be an object mapping DOM event type names to functions that // handle them. Such functions will be called before any handling // ProseMirror does of events fired on the editable DOM element. // Contrary to the other event handling props, when returning true // from such a function, you are responsible for calling // `preventDefault` yourself (or not, if you want to allow the // default behavior). // // handleKeyDown:: ?(view: EditorView, event: dom.KeyboardEvent) → bool // Called when the editor receives a `keydown` event. // // handleKeyPress:: ?(view: EditorView, event: dom.KeyboardEvent) → bool // Handler for `keypress` events. // // handleTextInput:: ?(view: EditorView, from: number, to: number, text: string) → bool // Whenever the user directly input text, this handler is called // before the input is applied. If it returns `true`, the default // behavior of actually inserting the text is suppressed. // // handleClickOn:: ?(view: EditorView, pos: number, node: Node, nodePos: number, event: dom.MouseEvent, direct: bool) → bool // Called for each node around a click, from the inside out. The // `direct` flag will be true for the inner node. // // handleClick:: ?(view: EditorView, pos: number, event: dom.MouseEvent) → bool // Called when the editor is clicked, after `handleClickOn` handlers // have been called. // // handleDoubleClickOn:: ?(view: EditorView, pos: number, node: Node, nodePos: number, event: dom.MouseEvent, direct: bool) → bool // Called for each node around a double click. // // handleDoubleClick:: ?(view: EditorView, pos: number, event: dom.MouseEvent) → bool // Called when the editor is double-clicked, after `handleDoubleClickOn`. // // handleTripleClickOn:: ?(view: EditorView, pos: number, node: Node, nodePos: number, event: dom.MouseEvent, direct: bool) → bool // Called for each node around a triple click. // // handleTripleClick:: ?(view: EditorView, pos: number, event: dom.MouseEvent) → bool // Called when the editor is triple-clicked, after `handleTripleClickOn`. // // handlePaste:: ?(view: EditorView, event: dom.ClipboardEvent, slice: Slice) → bool // Can be used to override the behavior of pasting. `slice` is the // pasted content parsed by the editor, but you can directly access // the event to get at the raw content. // // handleDrop:: ?(view: EditorView, event: dom.Event, slice: Slice, moved: bool) → bool // Called when something is dropped on the editor. `moved` will be // true if this drop moves from the current selection (which should // thus be deleted). // // handleScrollToSelection:: ?(view: EditorView) → bool // Called when the view, after updating its state, tries to scroll // the selection into view. A handler function may return false to // indicate that it did not handle the scrolling and further // handlers or the default behavior should be tried. // // createSelectionBetween:: ?(view: EditorView, anchor: ResolvedPos, head: ResolvedPos) → ?Selection // Can be used to override the way a selection is created when // reading a DOM selection between the given anchor and head. // // domParser:: ?DOMParser // The [parser](#model.DOMParser) to use when reading editor changes // from the DOM. Defaults to calling // [`DOMParser.fromSchema`](#model.DOMParser^fromSchema) on the // editor's schema. // // transformPastedHTML:: ?(html: string) → string // Can be used to transform pasted HTML text, _before_ it is parsed, // for example to clean it up. // // clipboardParser:: ?DOMParser // The [parser](#model.DOMParser) to use when reading content from // the clipboard. When not given, the value of the // [`domParser`](#view.EditorProps.domParser) prop is used. // // transformPastedText:: ?(text: string, plain: bool) → string // Transform pasted plain text. The `plain` flag will be true when // the text is pasted as plain text. // // clipboardTextParser:: ?(text: string, $context: ResolvedPos, plain: bool) → Slice // A function to parse text from the clipboard into a document // slice. Called after // [`transformPastedText`](#view.EditorProps.transformPastedText). // The default behavior is to split the text into lines, wrap them // in `

    ` tags, and call // [`clipboardParser`](#view.EditorProps.clipboardParser) on it. // The `plain` flag will be true when the text is pasted as plain text. // // transformPasted:: ?(Slice) → Slice // Can be used to transform pasted content before it is applied to // the document. // // nodeViews:: ?Object<(node: Node, view: EditorView, getPos: () → number, decorations: [Decoration], innerDecorations: DecorationSource) → NodeView> // Allows you to pass custom rendering and behavior logic for nodes // and marks. Should map node and mark names to constructor // functions that produce a [`NodeView`](#view.NodeView) object // implementing the node's display behavior. For nodes, the third // argument `getPos` is a function that can be called to get the // node's current position, which can be useful when creating // transactions to update it. For marks, the third argument is a // boolean that indicates whether the mark's content is inline. // // `decorations` is an array of node or inline decorations that are // active around the node. They are automatically drawn in the // normal way, and you will usually just want to ignore this, but // they can also be used as a way to provide context information to // the node view without adding it to the document itself. // // `innerDecorations` holds the decorations for the node's content. // You can safely ignore this if your view has no content or a // `contentDOM` property, since the editor will draw the decorations // on the content. But if you, for example, want to create a nested // editor with the content, it may make sense to provide it with the // inner decorations. // // clipboardSerializer:: ?DOMSerializer // The DOM serializer to use when putting content onto the // clipboard. If not given, the result of // [`DOMSerializer.fromSchema`](#model.DOMSerializer^fromSchema) // will be used. This object will only have its // [`serializeFragment`](#model.DOMSerializer.serializeFragment) // method called, and you may provide an alternative object type // implementing a compatible method. // // clipboardTextSerializer:: ?(Slice) → string // A function that will be called to get the text for the current // selection when copying text to the clipboard. By default, the // editor will use [`textBetween`](#model.Node.textBetween) on the // selected range. // // decorations:: ?(state: EditorState) → ?DecorationSource // A set of [document decorations](#view.Decoration) to show in the // view. // // editable:: ?(state: EditorState) → bool // When this returns false, the content of the view is not directly // editable. // // attributes:: ?union, (EditorState) → ?Object> // Control the DOM attributes of the editable element. May be either // an object or a function going from an editor state to an object. // By default, the element will get a class `"ProseMirror"`, and // will have its `contentEditable` attribute determined by the // [`editable` prop](#view.EditorProps.editable). Additional classes // provided here will be added to the class. For other attributes, // the value provided first (as in // [`someProp`](#view.EditorView.someProp)) will be used. // // scrollThreshold:: ?union // Determines the distance (in pixels) between the cursor and the // end of the visible viewport at which point, when scrolling the // cursor into view, scrolling takes place. Defaults to 0. // // scrollMargin:: ?union // Determines the extra space (in pixels) that is left above or // below the cursor when it is scrolled into view. Defaults to 5. // DirectEditorProps:: interface extends EditorProps // // The props object given directly to the editor view supports some // fields that can't be used in plugins: // // state:: EditorState // The current state of the editor. // // plugins:: [Plugin] // A set of plugins to use in the view, applying their [plugin // view](#state.PluginSpec.view) and // [props](#state.PluginSpec.props). Passing plugins with a state // component (a [state field](#state.PluginSpec.state) field or a // [transaction](#state.PluginSpec.filterTransaction) filter or // appender) will result in an error, since such plugins must be // present in the state to work. // // dispatchTransaction:: ?(tr: Transaction) // The callback over which to send transactions (state updates) // produced by the view. If you specify this, you probably want to // make sure this ends up calling the view's // [`updateState`](#view.EditorView.updateState) method with a new // state that has the transaction // [applied](#state.EditorState.apply). The callback will be bound to have // the view instance as its `this` binding. prosemirror-view-1.23.13/src/input.js000066400000000000000000000653271423202571000175110ustar00rootroot00000000000000import {Selection, NodeSelection, TextSelection} from "prosemirror-state" import {dropPoint} from "prosemirror-transform" import {Slice} from "prosemirror-model" import browser from "./browser" import {captureKeyDown} from "./capturekeys" import {readDOMChange} from "./domchange" import {parseFromClipboard, serializeForClipboard} from "./clipboard" import {DOMObserver} from "./domobserver" import {selectionBetween, selectionToDOM, selectionFromDOM} from "./selection" import {keyEvent} from "./dom" // A collection of DOM events that occur within the editor, and callback functions // to invoke when the event fires. const handlers = {}, editHandlers = {} export function initInput(view) { view.shiftKey = false view.mouseDown = null view.lastKeyCode = null view.lastKeyCodeTime = 0 view.lastClick = {time: 0, x: 0, y: 0, type: ""} view.lastSelectionOrigin = null view.lastSelectionTime = 0 view.lastIOSEnter = 0 view.lastIOSEnterFallbackTimeout = null view.lastAndroidDelete = 0 view.composing = false view.composingTimeout = null view.compositionNodes = [] view.compositionEndedAt = -2e8 view.domObserver = new DOMObserver(view, (from, to, typeOver, added) => readDOMChange(view, from, to, typeOver, added)) view.domObserver.start() // Used by hacks like the beforeinput handler to check whether anything happened in the DOM view.domChangeCount = 0 view.eventHandlers = Object.create(null) for (let event in handlers) { let handler = handlers[event] view.dom.addEventListener(event, view.eventHandlers[event] = event => { if (eventBelongsToView(view, event) && !runCustomHandler(view, event) && (view.editable || !(event.type in editHandlers))) handler(view, event) }) } // On Safari, for reasons beyond my understanding, adding an input // event handler makes an issue where the composition vanishes when // you press enter go away. if (browser.safari) view.dom.addEventListener("input", () => null) ensureListeners(view) } function setSelectionOrigin(view, origin) { view.lastSelectionOrigin = origin view.lastSelectionTime = Date.now() } export function destroyInput(view) { view.domObserver.stop() for (let type in view.eventHandlers) view.dom.removeEventListener(type, view.eventHandlers[type]) clearTimeout(view.composingTimeout) clearTimeout(view.lastIOSEnterFallbackTimeout) } export function ensureListeners(view) { view.someProp("handleDOMEvents", currentHandlers => { for (let type in currentHandlers) if (!view.eventHandlers[type]) view.dom.addEventListener(type, view.eventHandlers[type] = event => runCustomHandler(view, event)) }) } function runCustomHandler(view, event) { return view.someProp("handleDOMEvents", handlers => { let handler = handlers[event.type] return handler ? handler(view, event) || event.defaultPrevented : false }) } function eventBelongsToView(view, event) { if (!event.bubbles) return true if (event.defaultPrevented) return false for (let node = event.target; node != view.dom; node = node.parentNode) if (!node || node.nodeType == 11 || (node.pmViewDesc && node.pmViewDesc.stopEvent(event))) return false return true } export function dispatchEvent(view, event) { if (!runCustomHandler(view, event) && handlers[event.type] && (view.editable || !(event.type in editHandlers))) handlers[event.type](view, event) } editHandlers.keydown = (view, event) => { view.shiftKey = event.keyCode == 16 || event.shiftKey if (inOrNearComposition(view, event)) return view.lastKeyCode = event.keyCode view.lastKeyCodeTime = Date.now() // Suppress enter key events on Chrome Android, because those tend // to be part of a confused sequence of composition events fired, // and handling them eagerly tends to corrupt the input. if (browser.android && browser.chrome && event.keyCode == 13) return if (event.keyCode != 229) view.domObserver.forceFlush() // On iOS, if we preventDefault enter key presses, the virtual // keyboard gets confused. So the hack here is to set a flag that // makes the DOM change code recognize that what just happens should // be replaced by whatever the Enter key handlers do. if (browser.ios && event.keyCode == 13 && !event.ctrlKey && !event.altKey && !event.metaKey) { let now = Date.now() view.lastIOSEnter = now view.lastIOSEnterFallbackTimeout = setTimeout(() => { if (view.lastIOSEnter == now) { view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter"))) view.lastIOSEnter = 0 } }, 200) } else if (view.someProp("handleKeyDown", f => f(view, event)) || captureKeyDown(view, event)) { event.preventDefault() } else { setSelectionOrigin(view, "key") } } editHandlers.keyup = (view, e) => { if (e.keyCode == 16) view.shiftKey = false } editHandlers.keypress = (view, event) => { if (inOrNearComposition(view, event) || !event.charCode || event.ctrlKey && !event.altKey || browser.mac && event.metaKey) return if (view.someProp("handleKeyPress", f => f(view, event))) { event.preventDefault() return } let sel = view.state.selection if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to)) { let text = String.fromCharCode(event.charCode) if (!view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text))) view.dispatch(view.state.tr.insertText(text).scrollIntoView()) event.preventDefault() } } function eventCoords(event) { return {left: event.clientX, top: event.clientY} } function isNear(event, click) { let dx = click.x - event.clientX, dy = click.y - event.clientY return dx * dx + dy * dy < 100 } function runHandlerOnContext(view, propName, pos, inside, event) { if (inside == -1) return false let $pos = view.state.doc.resolve(inside) for (let i = $pos.depth + 1; i > 0; i--) { if (view.someProp(propName, f => i > $pos.depth ? f(view, pos, $pos.nodeAfter, $pos.before(i), event, true) : f(view, pos, $pos.node(i), $pos.before(i), event, false))) return true } return false } function updateSelection(view, selection, origin) { if (!view.focused) view.focus() let tr = view.state.tr.setSelection(selection) if (origin == "pointer") tr.setMeta("pointer", true) view.dispatch(tr) } function selectClickedLeaf(view, inside) { if (inside == -1) return false let $pos = view.state.doc.resolve(inside), node = $pos.nodeAfter if (node && node.isAtom && NodeSelection.isSelectable(node)) { updateSelection(view, new NodeSelection($pos), "pointer") return true } return false } function selectClickedNode(view, inside) { if (inside == -1) return false let sel = view.state.selection, selectedNode, selectAt if (sel instanceof NodeSelection) selectedNode = sel.node let $pos = view.state.doc.resolve(inside) for (let i = $pos.depth + 1; i > 0; i--) { let node = i > $pos.depth ? $pos.nodeAfter : $pos.node(i) if (NodeSelection.isSelectable(node)) { if (selectedNode && sel.$from.depth > 0 && i >= sel.$from.depth && $pos.before(sel.$from.depth + 1) == sel.$from.pos) selectAt = $pos.before(sel.$from.depth) else selectAt = $pos.before(i) break } } if (selectAt != null) { updateSelection(view, NodeSelection.create(view.state.doc, selectAt), "pointer") return true } else { return false } } function handleSingleClick(view, pos, inside, event, selectNode) { return runHandlerOnContext(view, "handleClickOn", pos, inside, event) || view.someProp("handleClick", f => f(view, pos, event)) || (selectNode ? selectClickedNode(view, inside) : selectClickedLeaf(view, inside)) } function handleDoubleClick(view, pos, inside, event) { return runHandlerOnContext(view, "handleDoubleClickOn", pos, inside, event) || view.someProp("handleDoubleClick", f => f(view, pos, event)) } function handleTripleClick(view, pos, inside, event) { return runHandlerOnContext(view, "handleTripleClickOn", pos, inside, event) || view.someProp("handleTripleClick", f => f(view, pos, event)) || defaultTripleClick(view, inside, event) } function defaultTripleClick(view, inside, event) { if (event.button != 0) return false let doc = view.state.doc if (inside == -1) { if (doc.inlineContent) { updateSelection(view, TextSelection.create(doc, 0, doc.content.size), "pointer") return true } return false } let $pos = doc.resolve(inside) for (let i = $pos.depth + 1; i > 0; i--) { let node = i > $pos.depth ? $pos.nodeAfter : $pos.node(i) let nodePos = $pos.before(i) if (node.inlineContent) updateSelection(view, TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size), "pointer") else if (NodeSelection.isSelectable(node)) updateSelection(view, NodeSelection.create(doc, nodePos), "pointer") else continue return true } } function forceDOMFlush(view) { return endComposition(view) } const selectNodeModifier = browser.mac ? "metaKey" : "ctrlKey" handlers.mousedown = (view, event) => { view.shiftKey = event.shiftKey let flushed = forceDOMFlush(view) let now = Date.now(), type = "singleClick" if (now - view.lastClick.time < 500 && isNear(event, view.lastClick) && !event[selectNodeModifier]) { if (view.lastClick.type == "singleClick") type = "doubleClick" else if (view.lastClick.type == "doubleClick") type = "tripleClick" } view.lastClick = {time: now, x: event.clientX, y: event.clientY, type} let pos = view.posAtCoords(eventCoords(event)) if (!pos) return if (type == "singleClick") { if (view.mouseDown) view.mouseDown.done() view.mouseDown = new MouseDown(view, pos, event, flushed) } else if ((type == "doubleClick" ? handleDoubleClick : handleTripleClick)(view, pos.pos, pos.inside, event)) { event.preventDefault() } else { setSelectionOrigin(view, "pointer") } } class MouseDown { constructor(view, pos, event, flushed) { this.view = view this.startDoc = view.state.doc this.pos = pos this.event = event this.flushed = flushed this.selectNode = event[selectNodeModifier] this.allowDefault = event.shiftKey this.delayedSelectionSync = false let targetNode, targetPos if (pos.inside > -1) { targetNode = view.state.doc.nodeAt(pos.inside) targetPos = pos.inside } else { let $pos = view.state.doc.resolve(pos.pos) targetNode = $pos.parent targetPos = $pos.depth ? $pos.before() : 0 } this.mightDrag = null const target = flushed ? null : event.target const targetDesc = target ? view.docView.nearestDesc(target, true) : null this.target = targetDesc ? targetDesc.dom : null let {selection} = view.state if (event.button == 0 && targetNode.type.spec.draggable && targetNode.type.spec.selectable !== false || selection instanceof NodeSelection && selection.from <= targetPos && selection.to > targetPos) this.mightDrag = {node: targetNode, pos: targetPos, addAttr: this.target && !this.target.draggable, setUneditable: this.target && browser.gecko && !this.target.hasAttribute("contentEditable")} if (this.target && this.mightDrag && (this.mightDrag.addAttr || this.mightDrag.setUneditable)) { this.view.domObserver.stop() if (this.mightDrag.addAttr) this.target.draggable = true if (this.mightDrag.setUneditable) setTimeout(() => { if (this.view.mouseDown == this) this.target.setAttribute("contentEditable", "false") }, 20) this.view.domObserver.start() } view.root.addEventListener("mouseup", this.up = this.up.bind(this)) view.root.addEventListener("mousemove", this.move = this.move.bind(this)) setSelectionOrigin(view, "pointer") } done() { this.view.root.removeEventListener("mouseup", this.up) this.view.root.removeEventListener("mousemove", this.move) if (this.mightDrag && this.target) { this.view.domObserver.stop() if (this.mightDrag.addAttr) this.target.removeAttribute("draggable") if (this.mightDrag.setUneditable) this.target.removeAttribute("contentEditable") this.view.domObserver.start() } if (this.delayedSelectionSync) setTimeout(() => selectionToDOM(this.view)) this.view.mouseDown = null } up(event) { this.done() if (!this.view.dom.contains(event.target.nodeType == 3 ? event.target.parentNode : event.target)) return let pos = this.pos if (this.view.state.doc != this.startDoc) pos = this.view.posAtCoords(eventCoords(event)) if (this.allowDefault || !pos) { setSelectionOrigin(this.view, "pointer") } else if (handleSingleClick(this.view, pos.pos, pos.inside, event, this.selectNode)) { event.preventDefault() } else if (event.button == 0 && (this.flushed || // Safari ignores clicks on draggable elements (browser.safari && this.mightDrag && !this.mightDrag.node.isAtom) || // Chrome will sometimes treat a node selection as a // cursor, but still report that the node is selected // when asked through getSelection. You'll then get a // situation where clicking at the point where that // (hidden) cursor is doesn't change the selection, and // thus doesn't get a reaction from ProseMirror. This // works around that. (browser.chrome && !(this.view.state.selection instanceof TextSelection) && Math.min(Math.abs(pos.pos - this.view.state.selection.from), Math.abs(pos.pos - this.view.state.selection.to)) <= 2))) { updateSelection(this.view, Selection.near(this.view.state.doc.resolve(pos.pos)), "pointer") event.preventDefault() } else { setSelectionOrigin(this.view, "pointer") } } move(event) { if (!this.allowDefault && (Math.abs(this.event.x - event.clientX) > 4 || Math.abs(this.event.y - event.clientY) > 4)) this.allowDefault = true setSelectionOrigin(this.view, "pointer") if (event.buttons == 0) this.done() } } handlers.touchdown = view => { forceDOMFlush(view) setSelectionOrigin(view, "pointer") } handlers.contextmenu = view => forceDOMFlush(view) function inOrNearComposition(view, event) { if (view.composing) return true // See https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/. // On Japanese input method editors (IMEs), the Enter key is used to confirm character // selection. On Safari, when Enter is pressed, compositionend and keydown events are // emitted. The keydown event triggers newline insertion, which we don't want. // This method returns true if the keydown event should be ignored. // We only ignore it once, as pressing Enter a second time *should* insert a newline. // Furthermore, the keydown event timestamp must be close to the compositionEndedAt timestamp. // This guards against the case where compositionend is triggered without the keyboard // (e.g. character confirmation may be done with the mouse), and keydown is triggered // afterwards- we wouldn't want to ignore the keydown event in this case. if (browser.safari && Math.abs(event.timeStamp - view.compositionEndedAt) < 500) { view.compositionEndedAt = -2e8 return true } return false } // Drop active composition after 5 seconds of inactivity on Android const timeoutComposition = browser.android ? 5000 : -1 editHandlers.compositionstart = editHandlers.compositionupdate = view => { if (!view.composing) { view.domObserver.flush() let {state} = view, $pos = state.selection.$from if (state.selection.empty && (state.storedMarks || (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore.marks.some(m => m.type.spec.inclusive === false)))) { // Need to wrap the cursor in mark nodes different from the ones in the DOM context view.markCursor = view.state.storedMarks || $pos.marks() endComposition(view, true) view.markCursor = null } else { endComposition(view) // In firefox, if the cursor is after but outside a marked node, // the inserted text won't inherit the marks. So this moves it // inside if necessary. if (browser.gecko && state.selection.empty && $pos.parentOffset && !$pos.textOffset && $pos.nodeBefore.marks.length) { let sel = view.root.getSelection() for (let node = sel.focusNode, offset = sel.focusOffset; node && node.nodeType == 1 && offset != 0;) { let before = offset < 0 ? node.lastChild : node.childNodes[offset - 1] if (!before) break if (before.nodeType == 3) { sel.collapse(before, before.nodeValue.length) break } else { node = before offset = -1 } } } } view.composing = true } scheduleComposeEnd(view, timeoutComposition) } editHandlers.compositionend = (view, event) => { if (view.composing) { view.composing = false view.compositionEndedAt = event.timeStamp scheduleComposeEnd(view, 20) } } function scheduleComposeEnd(view, delay) { clearTimeout(view.composingTimeout) if (delay > -1) view.composingTimeout = setTimeout(() => endComposition(view), delay) } export function clearComposition(view) { if (view.composing) { view.composing = false view.compositionEndedAt = timestampFromCustomEvent() } while (view.compositionNodes.length > 0) view.compositionNodes.pop().markParentsDirty() } function timestampFromCustomEvent() { let event = document.createEvent("Event") event.initEvent("event", true, true) return event.timeStamp } export function endComposition(view, forceUpdate) { if (browser.android && view.domObserver.flushingSoon >= 0) return view.domObserver.forceFlush() clearComposition(view) if (forceUpdate || view.docView && view.docView.dirty) { let sel = selectionFromDOM(view) if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel)) else view.updateState(view.state) return true } return false } function captureCopy(view, dom) { // The extra wrapper is somehow necessary on IE/Edge to prevent the // content from being mangled when it is put onto the clipboard if (!view.dom.parentNode) return let wrap = view.dom.parentNode.appendChild(document.createElement("div")) wrap.appendChild(dom) wrap.style.cssText = "position: fixed; left: -10000px; top: 10px" let sel = getSelection(), range = document.createRange() range.selectNodeContents(dom) // Done because IE will fire a selectionchange moving the selection // to its start when removeAllRanges is called and the editor still // has focus (which will mess up the editor's selection state). view.dom.blur() sel.removeAllRanges() sel.addRange(range) setTimeout(() => { if (wrap.parentNode) wrap.parentNode.removeChild(wrap) view.focus() }, 50) } // This is very crude, but unfortunately both these browsers _pretend_ // that they have a clipboard API—all the objects and methods are // there, they just don't work, and they are hard to test. const brokenClipboardAPI = (browser.ie && browser.ie_version < 15) || (browser.ios && browser.webkit_version < 604) handlers.copy = editHandlers.cut = (view, e) => { let sel = view.state.selection, cut = e.type == "cut" if (sel.empty) return // IE and Edge's clipboard interface is completely broken let data = brokenClipboardAPI ? null : e.clipboardData let slice = sel.content(), {dom, text} = serializeForClipboard(view, slice) if (data) { e.preventDefault() data.clearData() data.setData("text/html", dom.innerHTML) data.setData("text/plain", text) } else { captureCopy(view, dom) } if (cut) view.dispatch(view.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent", "cut")) } function sliceSingleNode(slice) { return slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1 ? slice.content.firstChild : null } function capturePaste(view, e) { if (!view.dom.parentNode) return let plainText = view.shiftKey || view.state.selection.$from.parent.type.spec.code let target = view.dom.parentNode.appendChild(document.createElement(plainText ? "textarea" : "div")) if (!plainText) target.contentEditable = "true" target.style.cssText = "position: fixed; left: -10000px; top: 10px" target.focus() setTimeout(() => { view.focus() if (target.parentNode) target.parentNode.removeChild(target) if (plainText) doPaste(view, target.value, null, e) else doPaste(view, target.textContent, target.innerHTML, e) }, 50) } function doPaste(view, text, html, e) { let slice = parseFromClipboard(view, text, html, view.shiftKey, view.state.selection.$from) if (view.someProp("handlePaste", f => f(view, e, slice || Slice.empty))) return true if (!slice) return false let singleNode = sliceSingleNode(slice) let tr = singleNode ? view.state.tr.replaceSelectionWith(singleNode, view.shiftKey) : view.state.tr.replaceSelection(slice) view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")) return true } editHandlers.paste = (view, e) => { // Handling paste from JavaScript during composition is very poorly // handled by browsers, so as a dodgy but preferable kludge, we just // let the browser do its native thing there, except on Android, // where the editor is almost always composing. if (view.composing && !browser.android) return let data = brokenClipboardAPI ? null : e.clipboardData if (data && doPaste(view, data.getData("text/plain"), data.getData("text/html"), e)) e.preventDefault() else capturePaste(view, e) } class Dragging { constructor(slice, move) { this.slice = slice this.move = move } } const dragCopyModifier = browser.mac ? "altKey" : "ctrlKey" handlers.dragstart = (view, e) => { let mouseDown = view.mouseDown if (mouseDown) mouseDown.done() if (!e.dataTransfer) return let sel = view.state.selection let pos = sel.empty ? null : view.posAtCoords(eventCoords(e)) if (pos && pos.pos >= sel.from && pos.pos <= (sel instanceof NodeSelection ? sel.to - 1: sel.to)) { // In selection } else if (mouseDown && mouseDown.mightDrag) { view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, mouseDown.mightDrag.pos))) } else if (e.target && e.target.nodeType == 1) { let desc = view.docView.nearestDesc(e.target, true) if (desc && desc.node.type.spec.draggable && desc != view.docView) view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, desc.posBefore))) } let slice = view.state.selection.content(), {dom, text} = serializeForClipboard(view, slice) e.dataTransfer.clearData() e.dataTransfer.setData(brokenClipboardAPI ? "Text" : "text/html", dom.innerHTML) // See https://github.com/ProseMirror/prosemirror/issues/1156 e.dataTransfer.effectAllowed = "copyMove" if (!brokenClipboardAPI) e.dataTransfer.setData("text/plain", text) view.dragging = new Dragging(slice, !e[dragCopyModifier]) } handlers.dragend = view => { let dragging = view.dragging window.setTimeout(() => { if (view.dragging == dragging) view.dragging = null }, 50) } editHandlers.dragover = editHandlers.dragenter = (_, e) => e.preventDefault() editHandlers.drop = (view, e) => { let dragging = view.dragging view.dragging = null if (!e.dataTransfer) return let eventPos = view.posAtCoords(eventCoords(e)) if (!eventPos) return let $mouse = view.state.doc.resolve(eventPos.pos) if (!$mouse) return let slice = dragging && dragging.slice if (slice) { view.someProp("transformPasted", f => { slice = f(slice) }) } else { slice = parseFromClipboard(view, e.dataTransfer.getData(brokenClipboardAPI ? "Text" : "text/plain"), brokenClipboardAPI ? null : e.dataTransfer.getData("text/html"), false, $mouse) } let move = dragging && !e[dragCopyModifier] if (view.someProp("handleDrop", f => f(view, e, slice || Slice.empty, move))) { e.preventDefault() return } if (!slice) return e.preventDefault() let insertPos = slice ? dropPoint(view.state.doc, $mouse.pos, slice) : $mouse.pos if (insertPos == null) insertPos = $mouse.pos let tr = view.state.tr if (move) tr.deleteSelection() let pos = tr.mapping.map(insertPos) let isNode = slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1 let beforeInsert = tr.doc if (isNode) tr.replaceRangeWith(pos, pos, slice.content.firstChild) else tr.replaceRange(pos, pos, slice) if (tr.doc.eq(beforeInsert)) return let $pos = tr.doc.resolve(pos) if (isNode && NodeSelection.isSelectable(slice.content.firstChild) && $pos.nodeAfter && $pos.nodeAfter.sameMarkup(slice.content.firstChild)) { tr.setSelection(new NodeSelection($pos)) } else { let end = tr.mapping.map(insertPos) tr.mapping.maps[tr.mapping.maps.length - 1].forEach((_from, _to, _newFrom, newTo) => end = newTo) tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end))) } view.focus() view.dispatch(tr.setMeta("uiEvent", "drop")) } handlers.focus = view => { if (!view.focused) { view.domObserver.stop() view.dom.classList.add("ProseMirror-focused") view.domObserver.start() view.focused = true setTimeout(() => { if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.root.getSelection())) selectionToDOM(view) }, 20) } } handlers.blur = (view, e) => { if (view.focused) { view.domObserver.stop() view.dom.classList.remove("ProseMirror-focused") view.domObserver.start() if (e.relatedTarget && view.dom.contains(e.relatedTarget)) view.domObserver.currentSelection.set({}) view.focused = false } } handlers.beforeinput = (view, event) => { // We should probably do more with beforeinput events, but support // is so spotty that I'm still waiting to see where they are going. // Very specific hack to deal with backspace sometimes failing on // Chrome Android when after an uneditable node. if (browser.chrome && browser.android && event.inputType == "deleteContentBackward") { view.domObserver.flushSoon() let {domChangeCount} = view setTimeout(() => { if (view.domChangeCount != domChangeCount) return // Event already had some effect // This bug tends to close the virtual keyboard, so we refocus view.dom.blur() view.focus() if (view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) return let {$cursor} = view.state.selection // Crude approximation of backspace behavior when no command handled it if ($cursor && $cursor.pos > 0) view.dispatch(view.state.tr.delete($cursor.pos - 1, $cursor.pos).scrollIntoView()) }, 50) } } // Make sure all handlers get registered for (let prop in editHandlers) handlers[prop] = editHandlers[prop] prosemirror-view-1.23.13/src/selection.js000066400000000000000000000171721423202571000203320ustar00rootroot00000000000000import {TextSelection, NodeSelection} from "prosemirror-state" import browser from "./browser" import {selectionCollapsed, isEquivalentPosition, domIndex, isOnEdge} from "./dom" export function selectionFromDOM(view, origin) { let domSel = view.root.getSelection(), doc = view.state.doc if (!domSel.focusNode) return null let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0 let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset) if (head < 0) return null let $head = doc.resolve(head), $anchor, selection if (selectionCollapsed(domSel)) { $anchor = $head while (nearestDesc && !nearestDesc.node) nearestDesc = nearestDesc.parent if (nearestDesc && nearestDesc.node.isAtom && NodeSelection.isSelectable(nearestDesc.node) && nearestDesc.parent && !(nearestDesc.node.isInline && isOnEdge(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) { let pos = nearestDesc.posBefore selection = new NodeSelection(head == pos ? $head : doc.resolve(pos)) } } else { let anchor = view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset) if (anchor < 0) return null $anchor = doc.resolve(anchor) } if (!selection) { let bias = origin == "pointer" || (view.state.selection.head < $head.pos && !inWidget) ? 1 : -1 selection = selectionBetween(view, $anchor, $head, bias) } return selection } function editorOwnsSelection(view) { return view.editable ? view.hasFocus() : hasSelection(view) && document.activeElement && document.activeElement.contains(view.dom) } export function selectionToDOM(view, force) { let sel = view.state.selection syncNodeSelection(view, sel) if (!editorOwnsSelection(view)) return // The delayed drag selection causes issues with Cell Selections // in Safari. And the drag selection delay is to workarond issues // which only present in Chrome. if (!force && view.mouseDown && view.mouseDown.allowDefault && browser.chrome) { let domSel = view.root.getSelection(), curSel = view.domObserver.currentSelection if (domSel.anchorNode && isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset)) { view.mouseDown.delayedSelectionSync = true view.domObserver.setCurSelection() return } } view.domObserver.disconnectSelection() if (view.cursorWrapper) { selectCursorWrapper(view) } else { let {anchor, head} = sel, resetEditableFrom, resetEditableTo if (brokenSelectBetweenUneditable && !(sel instanceof TextSelection)) { if (!sel.$from.parent.inlineContent) resetEditableFrom = temporarilyEditableNear(view, sel.from) if (!sel.empty && !sel.$from.parent.inlineContent) resetEditableTo = temporarilyEditableNear(view, sel.to) } view.docView.setSelection(anchor, head, view.root, force) if (brokenSelectBetweenUneditable) { if (resetEditableFrom) resetEditable(resetEditableFrom) if (resetEditableTo) resetEditable(resetEditableTo) } if (sel.visible) { view.dom.classList.remove("ProseMirror-hideselection") } else { view.dom.classList.add("ProseMirror-hideselection") if ("onselectionchange" in document) removeClassOnSelectionChange(view) } } view.domObserver.setCurSelection() view.domObserver.connectSelection() } // Kludge to work around Webkit not allowing a selection to start/end // between non-editable block nodes. We briefly make something // editable, set the selection, then set it uneditable again. const brokenSelectBetweenUneditable = browser.safari || browser.chrome && browser.chrome_version < 63 function temporarilyEditableNear(view, pos) { let {node, offset} = view.docView.domFromPos(pos, 0) let after = offset < node.childNodes.length ? node.childNodes[offset] : null let before = offset ? node.childNodes[offset - 1] : null if (browser.safari && after && after.contentEditable == "false") return setEditable(after) if ((!after || after.contentEditable == "false") && (!before || before.contentEditable == "false")) { if (after) return setEditable(after) else if (before) return setEditable(before) } } function setEditable(element) { element.contentEditable = "true" if (browser.safari && element.draggable) { element.draggable = false; element.wasDraggable = true } return element } function resetEditable(element) { element.contentEditable = "false" if (element.wasDraggable) { element.draggable = true; element.wasDraggable = null } } function removeClassOnSelectionChange(view) { let doc = view.dom.ownerDocument doc.removeEventListener("selectionchange", view.hideSelectionGuard) let domSel = view.root.getSelection() let node = domSel.anchorNode, offset = domSel.anchorOffset doc.addEventListener("selectionchange", view.hideSelectionGuard = () => { if (domSel.anchorNode != node || domSel.anchorOffset != offset) { doc.removeEventListener("selectionchange", view.hideSelectionGuard) setTimeout(() => { if (!editorOwnsSelection(view) || view.state.selection.visible) view.dom.classList.remove("ProseMirror-hideselection") }, 20) } }) } function selectCursorWrapper(view) { let domSel = view.root.getSelection(), range = document.createRange() let node = view.cursorWrapper.dom, img = node.nodeName == "IMG" if (img) range.setEnd(node.parentNode, domIndex(node) + 1) else range.setEnd(node, 0) range.collapse(false) domSel.removeAllRanges() domSel.addRange(range) // Kludge to kill 'control selection' in IE11 when selecting an // invisible cursor wrapper, since that would result in those weird // resize handles and a selection that considers the absolutely // positioned wrapper, rather than the root editable node, the // focused element. if (!img && !view.state.selection.visible && browser.ie && browser.ie_version <= 11) { node.disabled = true node.disabled = false } } export function syncNodeSelection(view, sel) { if (sel instanceof NodeSelection) { let desc = view.docView.descAt(sel.from) if (desc != view.lastSelectedViewDesc) { clearNodeSelection(view) if (desc) desc.selectNode() view.lastSelectedViewDesc = desc } } else { clearNodeSelection(view) } } // Clear all DOM statefulness of the last node selection. function clearNodeSelection(view) { if (view.lastSelectedViewDesc) { if (view.lastSelectedViewDesc.parent) view.lastSelectedViewDesc.deselectNode() view.lastSelectedViewDesc = null } } export function selectionBetween(view, $anchor, $head, bias) { return view.someProp("createSelectionBetween", f => f(view, $anchor, $head)) || TextSelection.between($anchor, $head, bias) } export function hasFocusAndSelection(view) { if (view.editable && view.root.activeElement != view.dom) return false return hasSelection(view) } export function hasSelection(view) { let sel = view.root.getSelection() if (!sel.anchorNode) return false try { // Firefox will raise 'permission denied' errors when accessing // properties of `sel.anchorNode` when it's in a generated CSS // element. return view.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) && (view.editable || view.dom.contains(sel.focusNode.nodeType == 3 ? sel.focusNode.parentNode : sel.focusNode)) } catch(_) { return false } } export function anchorInRightPlace(view) { let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0) let domSel = view.root.getSelection() return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) } prosemirror-view-1.23.13/src/viewdesc.js000066400000000000000000001556011423202571000201560ustar00rootroot00000000000000import {DOMSerializer, Fragment, Mark} from "prosemirror-model" import {TextSelection} from "prosemirror-state" import {domIndex, isEquivalentPosition, nodeSize} from "./dom" import browser from "./browser" // NodeView:: interface // // By default, document nodes are rendered using the result of the // [`toDOM`](#model.NodeSpec.toDOM) method of their spec, and managed // entirely by the editor. For some use cases, such as embedded // node-specific editing interfaces, you want more control over // the behavior of a node's in-editor representation, and need to // [define](#view.EditorProps.nodeViews) a custom node view. // // Mark views only support `dom` and `contentDOM`, and don't support // any of the node view methods. // // Objects returned as node views must conform to this interface. // // dom:: ?dom.Node // The outer DOM node that represents the document node. When not // given, the default strategy is used to create a DOM node. // // contentDOM:: ?dom.Node // The DOM node that should hold the node's content. Only meaningful // if the node view also defines a `dom` property and if its node // type is not a leaf node type. When this is present, ProseMirror // will take care of rendering the node's children into it. When it // is not present, the node view itself is responsible for rendering // (or deciding not to render) its child nodes. // // update:: ?(node: Node, decorations: [Decoration], innerDecorations: DecorationSource) → bool // When given, this will be called when the view is updating itself. // It will be given a node (possibly of a different type), an array // of active decorations around the node (which are automatically // drawn, and the node view may ignore if it isn't interested in // them), and a [decoration source](#view.DecorationSource) that // represents any decorations that apply to the content of the node // (which again may be ignored). It should return true if it was // able to update to that node, and false otherwise. If the node // view has a `contentDOM` property (or no `dom` property), updating // its child nodes will be handled by ProseMirror. // // selectNode:: ?() // Can be used to override the way the node's selected status (as a // node selection) is displayed. // // deselectNode:: ?() // When defining a `selectNode` method, you should also provide a // `deselectNode` method to remove the effect again. // // setSelection:: ?(anchor: number, head: number, root: dom.Document) // This will be called to handle setting the selection inside the // node. The `anchor` and `head` positions are relative to the start // of the node. By default, a DOM selection will be created between // the DOM positions corresponding to those positions, but if you // override it you can do something else. // // stopEvent:: ?(event: dom.Event) → bool // Can be used to prevent the editor view from trying to handle some // or all DOM events that bubble up from the node view. Events for // which this returns true are not handled by the editor. // // ignoreMutation:: ?(dom.MutationRecord) → bool // Called when a DOM // [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) // or a selection change happens within the view. When the change is // a selection change, the record will have a `type` property of // `"selection"` (which doesn't occur for native mutation records). // Return false if the editor should re-read the selection or // re-parse the range around the mutation, true if it can safely be // ignored. // // destroy:: ?() // Called when the node view is removed from the editor or the whole // editor is destroyed. (Not available for marks.) // View descriptions are data structures that describe the DOM that is // used to represent the editor's content. They are used for: // // - Incremental redrawing when the document changes // // - Figuring out what part of the document a given DOM position // corresponds to // // - Wiring in custom implementations of the editing interface for a // given node // // They form a doubly-linked mutable tree, starting at `view.docView`. const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3 // Superclass for the various kinds of descriptions. Defines their // basic structure and shared methods. class ViewDesc { // : (?ViewDesc, [ViewDesc], dom.Node, ?dom.Node) constructor(parent, children, dom, contentDOM) { this.parent = parent this.children = children this.dom = dom // An expando property on the DOM node provides a link back to its // description. dom.pmViewDesc = this // This is the node that holds the child views. It may be null for // descs that don't have children. this.contentDOM = contentDOM this.dirty = NOT_DIRTY } // Used to check whether a given description corresponds to a // widget/mark/node. matchesWidget() { return false } matchesMark() { return false } matchesNode() { return false } matchesHack(_nodeName) { return false } // : () → ?ParseRule // When parsing in-editor content (in domchange.js), we allow // descriptions to determine the parse rules that should be used to // parse them. parseRule() { return null } // : (dom.Event) → bool // Used by the editor's event handler to ignore events that come // from certain descs. stopEvent() { return false } // The size of the content represented by this desc. get size() { let size = 0 for (let i = 0; i < this.children.length; i++) size += this.children[i].size return size } // For block nodes, this represents the space taken up by their // start/end tokens. get border() { return 0 } destroy() { this.parent = null if (this.dom.pmViewDesc == this) this.dom.pmViewDesc = null for (let i = 0; i < this.children.length; i++) this.children[i].destroy() } posBeforeChild(child) { for (let i = 0, pos = this.posAtStart; i < this.children.length; i++) { let cur = this.children[i] if (cur == child) return pos pos += cur.size } } get posBefore() { return this.parent.posBeforeChild(this) } get posAtStart() { return this.parent ? this.parent.posBeforeChild(this) + this.border : 0 } get posAfter() { return this.posBefore + this.size } get posAtEnd() { return this.posAtStart + this.size - 2 * this.border } // : (dom.Node, number, ?number) → number localPosFromDOM(dom, offset, bias) { // If the DOM position is in the content, use the child desc after // it to figure out a position. if (this.contentDOM && this.contentDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode)) { if (bias < 0) { let domBefore, desc if (dom == this.contentDOM) { domBefore = dom.childNodes[offset - 1] } else { while (dom.parentNode != this.contentDOM) dom = dom.parentNode domBefore = dom.previousSibling } while (domBefore && !((desc = domBefore.pmViewDesc) && desc.parent == this)) domBefore = domBefore.previousSibling return domBefore ? this.posBeforeChild(desc) + desc.size : this.posAtStart } else { let domAfter, desc if (dom == this.contentDOM) { domAfter = dom.childNodes[offset] } else { while (dom.parentNode != this.contentDOM) dom = dom.parentNode domAfter = dom.nextSibling } while (domAfter && !((desc = domAfter.pmViewDesc) && desc.parent == this)) domAfter = domAfter.nextSibling return domAfter ? this.posBeforeChild(desc) : this.posAtEnd } } // Otherwise, use various heuristics, falling back on the bias // parameter, to determine whether to return the position at the // start or at the end of this view desc. let atEnd if (dom == this.dom && this.contentDOM) { atEnd = offset > domIndex(this.contentDOM) } else if (this.contentDOM && this.contentDOM != this.dom && this.dom.contains(this.contentDOM)) { atEnd = dom.compareDocumentPosition(this.contentDOM) & 2 } else if (this.dom.firstChild) { if (offset == 0) for (let search = dom;; search = search.parentNode) { if (search == this.dom) { atEnd = false; break } if (search.parentNode.firstChild != search) break } if (atEnd == null && offset == dom.childNodes.length) for (let search = dom;; search = search.parentNode) { if (search == this.dom) { atEnd = true; break } if (search.parentNode.lastChild != search) break } } return (atEnd == null ? bias > 0 : atEnd) ? this.posAtEnd : this.posAtStart } // Scan up the dom finding the first desc that is a descendant of // this one. nearestDesc(dom, onlyNodes) { for (let first = true, cur = dom; cur; cur = cur.parentNode) { let desc = this.getDesc(cur) if (desc && (!onlyNodes || desc.node)) { // If dom is outside of this desc's nodeDOM, don't count it. if (first && desc.nodeDOM && !(desc.nodeDOM.nodeType == 1 ? desc.nodeDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode) : desc.nodeDOM == dom)) first = false else return desc } } } getDesc(dom) { let desc = dom.pmViewDesc for (let cur = desc; cur; cur = cur.parent) if (cur == this) return desc } posFromDOM(dom, offset, bias) { for (let scan = dom; scan; scan = scan.parentNode) { let desc = this.getDesc(scan) if (desc) return desc.localPosFromDOM(dom, offset, bias) } return -1 } // : (number) → ?NodeViewDesc // Find the desc for the node after the given pos, if any. (When a // parent node overrode rendering, there might not be one.) descAt(pos) { for (let i = 0, offset = 0; i < this.children.length; i++) { let child = this.children[i], end = offset + child.size if (offset == pos && end != offset) { while (!child.border && child.children.length) child = child.children[0] return child } if (pos < end) return child.descAt(pos - offset - child.border) offset = end } } // : (number, number) → {node: dom.Node, offset: number} domFromPos(pos, side) { if (!this.contentDOM) return {node: this.dom, offset: 0} // First find the position in the child array let i = 0, offset = 0 for (let curPos = 0; i < this.children.length; i++) { let child = this.children[i], end = curPos + child.size if (end > pos || child instanceof TrailingHackViewDesc) { offset = pos - curPos; break } curPos = end } // If this points into the middle of a child, call through if (offset) return this.children[i].domFromPos(offset - this.children[i].border, side) // Go back if there were any zero-length widgets with side >= 0 before this point for (let prev; i && !(prev = this.children[i - 1]).size && prev instanceof WidgetViewDesc && prev.widget.type.side >= 0; i--) {} // Scan towards the first useable node if (side <= 0) { let prev, enter = true for (;; i--, enter = false) { prev = i ? this.children[i - 1] : null if (!prev || prev.dom.parentNode == this.contentDOM) break } if (prev && side && enter && !prev.border && !prev.domAtom) return prev.domFromPos(prev.size, side) return {node: this.contentDOM, offset: prev ? domIndex(prev.dom) + 1 : 0} } else { let next, enter = true for (;; i++, enter = false) { next = i < this.children.length ? this.children[i] : null if (!next || next.dom.parentNode == this.contentDOM) break } if (next && enter && !next.border && !next.domAtom) return next.domFromPos(0, side) return {node: this.contentDOM, offset: next ? domIndex(next.dom) : this.contentDOM.childNodes.length} } } // Used to find a DOM range in a single parent for a given changed // range. parseRange(from, to, base = 0) { if (this.children.length == 0) return {node: this.contentDOM, from, to, fromOffset: 0, toOffset: this.contentDOM.childNodes.length} let fromOffset = -1, toOffset = -1 for (let offset = base, i = 0;; i++) { let child = this.children[i], end = offset + child.size if (fromOffset == -1 && from <= end) { let childBase = offset + child.border // FIXME maybe descend mark views to parse a narrower range? if (from >= childBase && to <= end - child.border && child.node && child.contentDOM && this.contentDOM.contains(child.contentDOM)) return child.parseRange(from, to, childBase) from = offset for (let j = i; j > 0; j--) { let prev = this.children[j - 1] if (prev.size && prev.dom.parentNode == this.contentDOM && !prev.emptyChildAt(1)) { fromOffset = domIndex(prev.dom) + 1 break } from -= prev.size } if (fromOffset == -1) fromOffset = 0 } if (fromOffset > -1 && (end > to || i == this.children.length - 1)) { to = end for (let j = i + 1; j < this.children.length; j++) { let next = this.children[j] if (next.size && next.dom.parentNode == this.contentDOM && !next.emptyChildAt(-1)) { toOffset = domIndex(next.dom) break } to += next.size } if (toOffset == -1) toOffset = this.contentDOM.childNodes.length break } offset = end } return {node: this.contentDOM, from, to, fromOffset, toOffset} } emptyChildAt(side) { if (this.border || !this.contentDOM || !this.children.length) return false let child = this.children[side < 0 ? 0 : this.children.length - 1] return child.size == 0 || child.emptyChildAt(side) } // : (number) → dom.Node domAfterPos(pos) { let {node, offset} = this.domFromPos(pos, 0) if (node.nodeType != 1 || offset == node.childNodes.length) throw new RangeError("No node after pos " + pos) return node.childNodes[offset] } // : (number, number, dom.Document) // View descs are responsible for setting any selection that falls // entirely inside of them, so that custom implementations can do // custom things with the selection. Note that this falls apart when // a selection starts in such a node and ends in another, in which // case we just use whatever domFromPos produces as a best effort. setSelection(anchor, head, root, force) { // If the selection falls entirely in a child, give it to that child let from = Math.min(anchor, head), to = Math.max(anchor, head) for (let i = 0, offset = 0; i < this.children.length; i++) { let child = this.children[i], end = offset + child.size if (from > offset && to < end) return child.setSelection(anchor - offset - child.border, head - offset - child.border, root, force) offset = end } let anchorDOM = this.domFromPos(anchor, anchor ? -1 : 1) let headDOM = head == anchor ? anchorDOM : this.domFromPos(head, head ? -1 : 1) let domSel = root.getSelection() let brKludge = false // On Firefox, using Selection.collapse to put the cursor after a // BR node for some reason doesn't always work (#1073). On Safari, // the cursor sometimes inexplicable visually lags behind its // reported position in such situations (#1092). if ((browser.gecko || browser.safari) && anchor == head) { let {node, offset} = anchorDOM if (node.nodeType == 3) { brKludge = offset && node.nodeValue[offset - 1] == "\n" // Issue #1128 if (brKludge && offset == node.nodeValue.length) { for (let scan = node, after; scan; scan = scan.parentNode) { if (after = scan.nextSibling) { if (after.nodeName == "BR") anchorDOM = headDOM = {node: after.parentNode, offset: domIndex(after) + 1} break } let desc = scan.pmViewDesc if (desc && desc.node && desc.node.isBlock) break } } } else { let prev = node.childNodes[offset - 1] brKludge = prev && (prev.nodeName == "BR" || prev.contentEditable == "false") } } // Firefox can act strangely when the selection is in front of an // uneditable node. See #1163 and https://bugzilla.mozilla.org/show_bug.cgi?id=1709536 if (browser.gecko && domSel.focusNode && domSel.focusNode != headDOM.node && domSel.focusNode.nodeType == 1) { let after = domSel.focusNode.childNodes[domSel.focusOffset] if (after && after.contentEditable == "false") force = true } if (!(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode, domSel.focusOffset)) return // Selection.extend can be used to create an 'inverted' selection // (one where the focus is before the anchor), but not all // browsers support it yet. let domSelExtended = false if ((domSel.extend || anchor == head) && !brKludge) { domSel.collapse(anchorDOM.node, anchorDOM.offset) try { if (anchor != head) domSel.extend(headDOM.node, headDOM.offset) domSelExtended = true } catch (err) { // In some cases with Chrome the selection is empty after calling // collapse, even when it should be valid. This appears to be a bug, but // it is difficult to isolate. If this happens fallback to the old path // without using extend. if (!(err instanceof DOMException)) throw err // declare global: DOMException } } if (!domSelExtended) { if (anchor > head) { let tmp = anchorDOM; anchorDOM = headDOM; headDOM = tmp } let range = document.createRange() range.setEnd(headDOM.node, headDOM.offset) range.setStart(anchorDOM.node, anchorDOM.offset) domSel.removeAllRanges() domSel.addRange(range) } } // : (dom.MutationRecord) → bool ignoreMutation(mutation) { return !this.contentDOM && mutation.type != "selection" } get contentLost() { return this.contentDOM && this.contentDOM != this.dom && !this.dom.contains(this.contentDOM) } // Remove a subtree of the element tree that has been touched // by a DOM change, so that the next update will redraw it. markDirty(from, to) { for (let offset = 0, i = 0; i < this.children.length; i++) { let child = this.children[i], end = offset + child.size if (offset == end ? from <= end && to >= offset : from < end && to > offset) { let startInside = offset + child.border, endInside = end - child.border if (from >= startInside && to <= endInside) { this.dirty = from == offset || to == end ? CONTENT_DIRTY : CHILD_DIRTY if (from == startInside && to == endInside && (child.contentLost || child.dom.parentNode != this.contentDOM)) child.dirty = NODE_DIRTY else child.markDirty(from - startInside, to - startInside) return } else { child.dirty = child.dom == child.contentDOM && child.dom.parentNode == this.contentDOM && !child.children.length ? CONTENT_DIRTY : NODE_DIRTY } } offset = end } this.dirty = CONTENT_DIRTY } markParentsDirty() { let level = 1 for (let node = this.parent; node; node = node.parent, level++) { let dirty = level == 1 ? CONTENT_DIRTY : CHILD_DIRTY if (node.dirty < dirty) node.dirty = dirty } } get domAtom() { return false } get ignoreForCoords() { return false } } // Reused array to avoid allocating fresh arrays for things that will // stay empty anyway. const nothing = [] // A widget desc represents a widget decoration, which is a DOM node // drawn between the document nodes. class WidgetViewDesc extends ViewDesc { // : (ViewDesc, Decoration) constructor(parent, widget, view, pos) { let self, dom = widget.type.toDOM if (typeof dom == "function") dom = dom(view, () => { if (!self) return pos if (self.parent) return self.parent.posBeforeChild(self) }) if (!widget.type.spec.raw) { if (dom.nodeType != 1) { let wrap = document.createElement("span") wrap.appendChild(dom) dom = wrap } dom.contentEditable = false dom.classList.add("ProseMirror-widget") } super(parent, nothing, dom, null) this.widget = widget self = this } matchesWidget(widget) { return this.dirty == NOT_DIRTY && widget.type.eq(this.widget.type) } parseRule() { return {ignore: true} } stopEvent(event) { let stop = this.widget.spec.stopEvent return stop ? stop(event) : false } ignoreMutation(mutation) { return mutation.type != "selection" || this.widget.spec.ignoreSelection } destroy() { this.widget.type.destroy(this.dom) super.destroy() } get domAtom() { return true } } class CompositionViewDesc extends ViewDesc { constructor(parent, dom, textDOM, text) { super(parent, nothing, dom, null) this.textDOM = textDOM this.text = text } get size() { return this.text.length } localPosFromDOM(dom, offset) { if (dom != this.textDOM) return this.posAtStart + (offset ? this.size : 0) return this.posAtStart + offset } domFromPos(pos) { return {node: this.textDOM, offset: pos} } ignoreMutation(mut) { return mut.type === 'characterData' && mut.target.nodeValue == mut.oldValue } } // A mark desc represents a mark. May have multiple children, // depending on how the mark is split. Note that marks are drawn using // a fixed nesting order, for simplicity and predictability, so in // some cases they will be split more often than would appear // necessary. class MarkViewDesc extends ViewDesc { // : (ViewDesc, Mark, dom.Node) constructor(parent, mark, dom, contentDOM) { super(parent, [], dom, contentDOM) this.mark = mark } static create(parent, mark, inline, view) { let custom = view.nodeViews[mark.type.name] let spec = custom && custom(mark, view, inline) if (!spec || !spec.dom) spec = DOMSerializer.renderSpec(document, mark.type.spec.toDOM(mark, inline)) return new MarkViewDesc(parent, mark, spec.dom, spec.contentDOM || spec.dom) } parseRule() { if ((this.dirty & NODE_DIRTY) || this.mark.type.spec.reparseInView) return null return {mark: this.mark.type.name, attrs: this.mark.attrs, contentElement: this.contentDOM} } matchesMark(mark) { return this.dirty != NODE_DIRTY && this.mark.eq(mark) } markDirty(from, to) { super.markDirty(from, to) // Move dirty info to nearest node view if (this.dirty != NOT_DIRTY) { let parent = this.parent while (!parent.node) parent = parent.parent if (parent.dirty < this.dirty) parent.dirty = this.dirty this.dirty = NOT_DIRTY } } slice(from, to, view) { let copy = MarkViewDesc.create(this.parent, this.mark, true, view) let nodes = this.children, size = this.size if (to < size) nodes = replaceNodes(nodes, to, size, view) if (from > 0) nodes = replaceNodes(nodes, 0, from, view) for (let i = 0; i < nodes.length; i++) nodes[i].parent = copy copy.children = nodes return copy } } // Node view descs are the main, most common type of view desc, and // correspond to an actual node in the document. Unlike mark descs, // they populate their child array themselves. class NodeViewDesc extends ViewDesc { // : (?ViewDesc, Node, [Decoration], DecorationSource, dom.Node, ?dom.Node, EditorView) constructor(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos) { super(parent, node.isLeaf ? nothing : [], dom, contentDOM) this.nodeDOM = nodeDOM this.node = node this.outerDeco = outerDeco this.innerDeco = innerDeco if (contentDOM) this.updateChildren(view, pos) } // By default, a node is rendered using the `toDOM` method from the // node type spec. But client code can use the `nodeViews` spec to // supply a custom node view, which can influence various aspects of // the way the node works. // // (Using subclassing for this was intentionally decided against, // since it'd require exposing a whole slew of finicky // implementation details to the user code that they probably will // never need.) static create(parent, node, outerDeco, innerDeco, view, pos) { let custom = view.nodeViews[node.type.name], descObj let spec = custom && custom(node, view, () => { // (This is a function that allows the custom view to find its // own position) if (!descObj) return pos if (descObj.parent) return descObj.parent.posBeforeChild(descObj) }, outerDeco, innerDeco) let dom = spec && spec.dom, contentDOM = spec && spec.contentDOM if (node.isText) { if (!dom) dom = document.createTextNode(node.text) else if (dom.nodeType != 3) throw new RangeError("Text must be rendered as a DOM text node") } else if (!dom) { ;({dom, contentDOM} = DOMSerializer.renderSpec(document, node.type.spec.toDOM(node))) } if (!contentDOM && !node.isText && dom.nodeName != "BR") { // Chrome gets confused by
    if (!dom.hasAttribute("contenteditable")) dom.contentEditable = false if (node.type.spec.draggable) dom.draggable = true } let nodeDOM = dom dom = applyOuterDeco(dom, outerDeco, node) if (spec) return descObj = new CustomNodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, spec, view, pos + 1) else if (node.isText) return new TextViewDesc(parent, node, outerDeco, innerDeco, dom, nodeDOM, view) else return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos + 1) } parseRule() { // Experimental kludge to allow opt-in re-parsing of nodes if (this.node.type.spec.reparseInView) return null // FIXME the assumption that this can always return the current // attrs means that if the user somehow manages to change the // attrs in the dom, that won't be picked up. Not entirely sure // whether this is a problem let rule = {node: this.node.type.name, attrs: this.node.attrs} if (this.node.type.whitespace == "pre") rule.preserveWhitespace = "full" if (!this.contentDOM) { rule.getContent = () => this.node.content } else if (!this.contentLost) { rule.contentElement = this.contentDOM } else { // Chrome likes to randomly recreate parent nodes when // backspacing things. When that happens, this tries to find the // new parent. for (let i = this.children.length - 1; i >= 0; i--) { let child = this.children[i] if (this.dom.contains(child.dom.parentNode)) { rule.contentElement = child.dom.parentNode break } } if (!rule.contentElement) rule.getContent = () => Fragment.empty } return rule } matchesNode(node, outerDeco, innerDeco) { return this.dirty == NOT_DIRTY && node.eq(this.node) && sameOuterDeco(outerDeco, this.outerDeco) && innerDeco.eq(this.innerDeco) } get size() { return this.node.nodeSize } get border() { return this.node.isLeaf ? 0 : 1 } // Syncs `this.children` to match `this.node.content` and the local // decorations, possibly introducing nesting for marks. Then, in a // separate step, syncs the DOM inside `this.contentDOM` to // `this.children`. updateChildren(view, pos) { let inline = this.node.inlineContent, off = pos let composition = view.composing && this.localCompositionInfo(view, pos) let localComposition = composition && composition.pos > -1 ? composition : null let compositionInChild = composition && composition.pos < 0 let updater = new ViewTreeUpdater(this, localComposition && localComposition.node) iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => { if (widget.spec.marks) updater.syncToMarks(widget.spec.marks, inline, view) else if (widget.type.side >= 0 && !insideNode) updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view) // If the next node is a desc matching this widget, reuse it, // otherwise insert the widget as a new view desc. updater.placeWidget(widget, view, off) }, (child, outerDeco, innerDeco, i) => { // Make sure the wrapping mark descs match the node's marks. updater.syncToMarks(child.marks, inline, view) // Try several strategies for drawing this node let compIndex if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) { // Found precise match with existing node view } else if (compositionInChild && view.state.selection.from > off && view.state.selection.to < off + child.nodeSize && (compIndex = updater.findIndexWithChild(composition.node)) > -1 && updater.updateNodeAt(child, outerDeco, innerDeco, compIndex, view)) { // Updated the specific node that holds the composition } else if (updater.updateNextNode(child, outerDeco, innerDeco, view, i)) { // Could update an existing node to reflect this node } else { // Add it as a new view updater.addNode(child, outerDeco, innerDeco, view, off) } off += child.nodeSize }) // Drop all remaining descs after the current position. updater.syncToMarks(nothing, inline, view) if (this.node.isTextblock) updater.addTextblockHacks() updater.destroyRest() // Sync the DOM if anything changed if (updater.changed || this.dirty == CONTENT_DIRTY) { // May have to protect focused DOM from being changed if a composition is active if (localComposition) this.protectLocalComposition(view, localComposition) renderDescs(this.contentDOM, this.children, view) if (browser.ios) iosHacks(this.dom) } } localCompositionInfo(view, pos) { // Only do something if both the selection and a focused text node // are inside of this node let {from, to} = view.state.selection if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size) return let sel = view.root.getSelection() let textNode = nearbyTextNode(sel.focusNode, sel.focusOffset) if (!textNode || !this.dom.contains(textNode.parentNode)) return if (this.node.inlineContent) { // Find the text in the focused node in the node, stop if it's not // there (may have been modified through other means, in which // case it should overwritten) let text = textNode.nodeValue let textPos = findTextInFragment(this.node.content, text, from - pos, to - pos) return textPos < 0 ? null : {node: textNode, pos: textPos, text} } else { return {node: textNode, pos: -1} } } protectLocalComposition(view, {node, pos, text}) { // The node is already part of a local view desc, leave it there if (this.getDesc(node)) return // Create a composition view for the orphaned nodes let topNode = node for (;; topNode = topNode.parentNode) { if (topNode.parentNode == this.contentDOM) break while (topNode.previousSibling) topNode.parentNode.removeChild(topNode.previousSibling) while (topNode.nextSibling) topNode.parentNode.removeChild(topNode.nextSibling) if (topNode.pmViewDesc) topNode.pmViewDesc = null } let desc = new CompositionViewDesc(this, topNode, node, text) view.compositionNodes.push(desc) // Patch up this.children to contain the composition view this.children = replaceNodes(this.children, pos, pos + text.length, view, desc) } // : (Node, [Decoration], DecorationSource, EditorView) → bool // If this desc be updated to match the given node decoration, // do so and return true. update(node, outerDeco, innerDeco, view) { if (this.dirty == NODE_DIRTY || !node.sameMarkup(this.node)) return false this.updateInner(node, outerDeco, innerDeco, view) return true } updateInner(node, outerDeco, innerDeco, view) { this.updateOuterDeco(outerDeco) this.node = node this.innerDeco = innerDeco if (this.contentDOM) this.updateChildren(view, this.posAtStart) this.dirty = NOT_DIRTY } updateOuterDeco(outerDeco) { if (sameOuterDeco(outerDeco, this.outerDeco)) return let needsWrap = this.nodeDOM.nodeType != 1 let oldDOM = this.dom this.dom = patchOuterDeco(this.dom, this.nodeDOM, computeOuterDeco(this.outerDeco, this.node, needsWrap), computeOuterDeco(outerDeco, this.node, needsWrap)) if (this.dom != oldDOM) { oldDOM.pmViewDesc = null this.dom.pmViewDesc = this } this.outerDeco = outerDeco } // Mark this node as being the selected node. selectNode() { this.nodeDOM.classList.add("ProseMirror-selectednode") if (this.contentDOM || !this.node.type.spec.draggable) this.dom.draggable = true } // Remove selected node marking from this node. deselectNode() { this.nodeDOM.classList.remove("ProseMirror-selectednode") if (this.contentDOM || !this.node.type.spec.draggable) this.dom.removeAttribute("draggable") } get domAtom() { return this.node.isAtom } } // Create a view desc for the top-level document node, to be exported // and used by the view class. export function docViewDesc(doc, outerDeco, innerDeco, dom, view) { applyOuterDeco(dom, outerDeco, doc) return new NodeViewDesc(null, doc, outerDeco, innerDeco, dom, dom, dom, view, 0) } class TextViewDesc extends NodeViewDesc { constructor(parent, node, outerDeco, innerDeco, dom, nodeDOM, view) { super(parent, node, outerDeco, innerDeco, dom, null, nodeDOM, view) } parseRule() { let skip = this.nodeDOM.parentNode while (skip && skip != this.dom && !skip.pmIsDeco) skip = skip.parentNode return {skip: skip || true} } update(node, outerDeco, _, view) { if (this.dirty == NODE_DIRTY || (this.dirty != NOT_DIRTY && !this.inParent()) || !node.sameMarkup(this.node)) return false this.updateOuterDeco(outerDeco) if ((this.dirty != NOT_DIRTY || node.text != this.node.text) && node.text != this.nodeDOM.nodeValue) { this.nodeDOM.nodeValue = node.text if (view.trackWrites == this.nodeDOM) view.trackWrites = null } this.node = node this.dirty = NOT_DIRTY return true } inParent() { let parentDOM = this.parent.contentDOM for (let n = this.nodeDOM; n; n = n.parentNode) if (n == parentDOM) return true return false } domFromPos(pos) { return {node: this.nodeDOM, offset: pos} } localPosFromDOM(dom, offset, bias) { if (dom == this.nodeDOM) return this.posAtStart + Math.min(offset, this.node.text.length) return super.localPosFromDOM(dom, offset, bias) } ignoreMutation(mutation) { return mutation.type != "characterData" && mutation.type != "selection" } slice(from, to, view) { let node = this.node.cut(from, to), dom = document.createTextNode(node.text) return new TextViewDesc(this.parent, node, this.outerDeco, this.innerDeco, dom, dom, view) } markDirty(from, to) { super.markDirty(from, to) if (this.dom != this.nodeDOM && (from == 0 || to == this.nodeDOM.nodeValue.length)) this.dirty = NODE_DIRTY } get domAtom() { return false } } // A dummy desc used to tag trailing BR or IMG nodes created to work // around contentEditable terribleness. class TrailingHackViewDesc extends ViewDesc { parseRule() { return {ignore: true} } matchesHack(nodeName) { return this.dirty == NOT_DIRTY && this.dom.nodeName == nodeName } get domAtom() { return true } get ignoreForCoords() { return this.dom.nodeName == "IMG" } } // A separate subclass is used for customized node views, so that the // extra checks only have to be made for nodes that are actually // customized. class CustomNodeViewDesc extends NodeViewDesc { // : (?ViewDesc, Node, [Decoration], DecorationSource, dom.Node, ?dom.Node, NodeView, EditorView) constructor(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, spec, view, pos) { super(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos) this.spec = spec } // A custom `update` method gets to decide whether the update goes // through. If it does, and there's a `contentDOM` node, our logic // updates the children. update(node, outerDeco, innerDeco, view) { if (this.dirty == NODE_DIRTY) return false if (this.spec.update) { let result = this.spec.update(node, outerDeco, innerDeco) if (result) this.updateInner(node, outerDeco, innerDeco, view) return result } else if (!this.contentDOM && !node.isLeaf) { return false } else { return super.update(node, outerDeco, innerDeco, view) } } selectNode() { this.spec.selectNode ? this.spec.selectNode() : super.selectNode() } deselectNode() { this.spec.deselectNode ? this.spec.deselectNode() : super.deselectNode() } setSelection(anchor, head, root, force) { this.spec.setSelection ? this.spec.setSelection(anchor, head, root) : super.setSelection(anchor, head, root, force) } destroy() { if (this.spec.destroy) this.spec.destroy() super.destroy() } stopEvent(event) { return this.spec.stopEvent ? this.spec.stopEvent(event) : false } ignoreMutation(mutation) { return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation) } } // : (dom.Node, [ViewDesc]) // Sync the content of the given DOM node with the nodes associated // with the given array of view descs, recursing into mark descs // because this should sync the subtree for a whole node at a time. function renderDescs(parentDOM, descs, view) { let dom = parentDOM.firstChild, written = false for (let i = 0; i < descs.length; i++) { let desc = descs[i], childDOM = desc.dom if (childDOM.parentNode == parentDOM) { while (childDOM != dom) { dom = rm(dom); written = true } dom = dom.nextSibling } else { written = true parentDOM.insertBefore(childDOM, dom) } if (desc instanceof MarkViewDesc) { let pos = dom ? dom.previousSibling : parentDOM.lastChild renderDescs(desc.contentDOM, desc.children, view) dom = pos ? pos.nextSibling : parentDOM.firstChild } } while (dom) { dom = rm(dom); written = true } if (written && view.trackWrites == parentDOM) view.trackWrites = null } function OuterDecoLevel(nodeName) { if (nodeName) this.nodeName = nodeName } OuterDecoLevel.prototype = Object.create(null) const noDeco = [new OuterDecoLevel] function computeOuterDeco(outerDeco, node, needsWrap) { if (outerDeco.length == 0) return noDeco let top = needsWrap ? noDeco[0] : new OuterDecoLevel, result = [top] for (let i = 0; i < outerDeco.length; i++) { let attrs = outerDeco[i].type.attrs if (!attrs) continue if (attrs.nodeName) result.push(top = new OuterDecoLevel(attrs.nodeName)) for (let name in attrs) { let val = attrs[name] if (val == null) continue if (needsWrap && result.length == 1) result.push(top = new OuterDecoLevel(node.isInline ? "span" : "div")) if (name == "class") top.class = (top.class ? top.class + " " : "") + val else if (name == "style") top.style = (top.style ? top.style + ";" : "") + val else if (name != "nodeName") top[name] = val } } return result } function patchOuterDeco(outerDOM, nodeDOM, prevComputed, curComputed) { // Shortcut for trivial case if (prevComputed == noDeco && curComputed == noDeco) return nodeDOM let curDOM = nodeDOM for (let i = 0; i < curComputed.length; i++) { let deco = curComputed[i], prev = prevComputed[i] if (i) { let parent if (prev && prev.nodeName == deco.nodeName && curDOM != outerDOM && (parent = curDOM.parentNode) && parent.tagName.toLowerCase() == deco.nodeName) { curDOM = parent } else { parent = document.createElement(deco.nodeName) parent.pmIsDeco = true parent.appendChild(curDOM) prev = noDeco[0] curDOM = parent } } patchAttributes(curDOM, prev || noDeco[0], deco) } return curDOM } function patchAttributes(dom, prev, cur) { for (let name in prev) if (name != "class" && name != "style" && name != "nodeName" && !(name in cur)) dom.removeAttribute(name) for (let name in cur) if (name != "class" && name != "style" && name != "nodeName" && cur[name] != prev[name]) dom.setAttribute(name, cur[name]) if (prev.class != cur.class) { let prevList = prev.class ? prev.class.split(" ").filter(Boolean) : nothing let curList = cur.class ? cur.class.split(" ").filter(Boolean) : nothing for (let i = 0; i < prevList.length; i++) if (curList.indexOf(prevList[i]) == -1) dom.classList.remove(prevList[i]) for (let i = 0; i < curList.length; i++) if (prevList.indexOf(curList[i]) == -1) dom.classList.add(curList[i]) if (dom.classList.length == 0) dom.removeAttribute("class") } if (prev.style != cur.style) { if (prev.style) { let prop = /\s*([\w\-\xa1-\uffff]+)\s*:(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\(.*?\)|[^;])*/g, m while (m = prop.exec(prev.style)) dom.style.removeProperty(m[1]) } if (cur.style) dom.style.cssText += cur.style } } function applyOuterDeco(dom, deco, node) { return patchOuterDeco(dom, dom, noDeco, computeOuterDeco(deco, node, dom.nodeType != 1)) } // : ([Decoration], [Decoration]) → bool function sameOuterDeco(a, b) { if (a.length != b.length) return false for (let i = 0; i < a.length; i++) if (!a[i].type.eq(b[i].type)) return false return true } // Remove a DOM node and return its next sibling. function rm(dom) { let next = dom.nextSibling dom.parentNode.removeChild(dom) return next } // Helper class for incrementally updating a tree of mark descs and // the widget and node descs inside of them. class ViewTreeUpdater { // : (NodeViewDesc) constructor(top, lockedNode) { this.top = top this.lock = lockedNode // Index into `this.top`'s child array, represents the current // update position. this.index = 0 // When entering a mark, the current top and index are pushed // onto this. this.stack = [] // Tracks whether anything was changed this.changed = false this.preMatch = preMatch(top.node.content, top) } // Destroy and remove the children between the given indices in // `this.top`. destroyBetween(start, end) { if (start == end) return for (let i = start; i < end; i++) this.top.children[i].destroy() this.top.children.splice(start, end - start) this.changed = true } // Destroy all remaining children in `this.top`. destroyRest() { this.destroyBetween(this.index, this.top.children.length) } // : ([Mark], EditorView) // Sync the current stack of mark descs with the given array of // marks, reusing existing mark descs when possible. syncToMarks(marks, inline, view) { let keep = 0, depth = this.stack.length >> 1 let maxKeep = Math.min(depth, marks.length) while (keep < maxKeep && (keep == depth - 1 ? this.top : this.stack[(keep + 1) << 1]).matchesMark(marks[keep]) && marks[keep].type.spec.spanning !== false) keep++ while (keep < depth) { this.destroyRest() this.top.dirty = NOT_DIRTY this.index = this.stack.pop() this.top = this.stack.pop() depth-- } while (depth < marks.length) { this.stack.push(this.top, this.index + 1) let found = -1 for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) { if (this.top.children[i].matchesMark(marks[depth])) { found = i; break } } if (found > -1) { if (found > this.index) { this.changed = true this.destroyBetween(this.index, found) } this.top = this.top.children[this.index] } else { let markDesc = MarkViewDesc.create(this.top, marks[depth], inline, view) this.top.children.splice(this.index, 0, markDesc) this.top = markDesc this.changed = true } this.index = 0 depth++ } } // : (Node, [Decoration], DecorationSource) → bool // Try to find a node desc matching the given data. Skip over it and // return true when successful. findNodeMatch(node, outerDeco, innerDeco, index) { let found = -1, targetDesc if (index >= this.preMatch.index && (targetDesc = this.preMatch.matches[index - this.preMatch.index]).parent == this.top && targetDesc.matchesNode(node, outerDeco, innerDeco)) { found = this.top.children.indexOf(targetDesc, this.index) } else { for (let i = this.index, e = Math.min(this.top.children.length, i + 5); i < e; i++) { let child = this.top.children[i] if (child.matchesNode(node, outerDeco, innerDeco) && !this.preMatch.matched.has(child)) { found = i break } } } if (found < 0) return false this.destroyBetween(this.index, found) this.index++ return true } updateNodeAt(node, outerDeco, innerDeco, index, view) { let child = this.top.children[index] if (!child.update(node, outerDeco, innerDeco, view)) return false this.destroyBetween(this.index, index) this.index = index + 1 return true } findIndexWithChild(domNode) { for (;;) { let parent = domNode.parentNode if (!parent) return -1 if (parent == this.top.contentDOM) { let desc = domNode.pmViewDesc if (desc) for (let i = this.index; i < this.top.children.length; i++) { if (this.top.children[i] == desc) return i } return -1 } domNode = parent } } // : (Node, [Decoration], DecorationSource, EditorView, Fragment, number) → bool // Try to update the next node, if any, to the given data. Checks // pre-matches to avoid overwriting nodes that could still be used. updateNextNode(node, outerDeco, innerDeco, view, index) { for (let i = this.index; i < this.top.children.length; i++) { let next = this.top.children[i] if (next instanceof NodeViewDesc) { let preMatch = this.preMatch.matched.get(next) if (preMatch != null && preMatch != index) return false let nextDOM = next.dom // Can't update if nextDOM is or contains this.lock, except if // it's a text node whose content already matches the new text // and whose decorations match the new ones. let locked = this.lock && (nextDOM == this.lock || nextDOM.nodeType == 1 && nextDOM.contains(this.lock.parentNode)) && !(node.isText && next.node && next.node.isText && next.nodeDOM.nodeValue == node.text && next.dirty != NODE_DIRTY && sameOuterDeco(outerDeco, next.outerDeco)) if (!locked && next.update(node, outerDeco, innerDeco, view)) { this.destroyBetween(this.index, i) if (next.dom != nextDOM) this.changed = true this.index++ return true } break } } return false } // : (Node, [Decoration], DecorationSource, EditorView) // Insert the node as a newly created node desc. addNode(node, outerDeco, innerDeco, view, pos) { this.top.children.splice(this.index++, 0, NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos)) this.changed = true } placeWidget(widget, view, pos) { let next = this.index < this.top.children.length ? this.top.children[this.index] : null if (next && next.matchesWidget(widget) && (widget == next.widget || !next.widget.type.toDOM.parentNode)) { this.index++ } else { let desc = new WidgetViewDesc(this.top, widget, view, pos) this.top.children.splice(this.index++, 0, desc) this.changed = true } } // Make sure a textblock looks and behaves correctly in // contentEditable. addTextblockHacks() { let lastChild = this.top.children[this.index - 1], parent = this.top while (lastChild instanceof MarkViewDesc) { parent = lastChild lastChild = parent.children[parent.children.length - 1] } if (!lastChild || // Empty textblock !(lastChild instanceof TextViewDesc) || /\n$/.test(lastChild.node.text)) { // Avoid bugs in Safari's cursor drawing (#1165) and Chrome's mouse selection (#1152) if ((browser.safari || browser.chrome) && lastChild && lastChild.dom.contentEditable == "false") this.addHackNode("IMG", parent) this.addHackNode("BR", this.top) } } addHackNode(nodeName, parent) { if (parent == this.top && this.index < parent.children.length && parent.children[this.index].matchesHack(nodeName)) { this.index++ } else { let dom = document.createElement(nodeName) if (nodeName == "IMG") { dom.className = "ProseMirror-separator" dom.alt = "" } if (nodeName == "BR") dom.className = "ProseMirror-trailingBreak" let hack = new TrailingHackViewDesc(this.top, nothing, dom, null) if (parent != this.top) parent.children.push(hack) else parent.children.splice(this.index++, 0, hack) this.changed = true } } } // : (Fragment, [ViewDesc]) → {index: number, matched: Map, matches: ViewDesc[]} // Iterate from the end of the fragment and array of descs to find // directly matching ones, in order to avoid overeagerly reusing those // for other nodes. Returns the fragment index of the first node that // is part of the sequence of matched nodes at the end of the // fragment. function preMatch(frag, parentDesc) { let curDesc = parentDesc, descI = curDesc.children.length let fI = frag.childCount, matched = new Map, matches = [] outer: while (fI > 0) { let desc for (;;) { if (descI) { let next = curDesc.children[descI - 1] if (next instanceof MarkViewDesc) { curDesc = next descI = next.children.length } else { desc = next descI-- break } } else if (curDesc == parentDesc) { break outer } else { // FIXME descI = curDesc.parent.children.indexOf(curDesc) curDesc = curDesc.parent } } let node = desc.node if (!node) continue if (node != frag.child(fI - 1)) break --fI matched.set(desc, fI) matches.push(desc) } return {index: fI, matched, matches: matches.reverse()} } function compareSide(a, b) { return a.type.side - b.type.side } // : (ViewDesc, DecorationSource, (Decoration, number), (Node, [Decoration], DecorationSource, number)) // This function abstracts iterating over the nodes and decorations in // a fragment. Calls `onNode` for each node, with its local and child // decorations. Splits text nodes when there is a decoration starting // or ending inside of them. Calls `onWidget` for each widget. function iterDeco(parent, deco, onWidget, onNode) { let locals = deco.locals(parent), offset = 0 // Simple, cheap variant for when there are no local decorations if (locals.length == 0) { for (let i = 0; i < parent.childCount; i++) { let child = parent.child(i) onNode(child, locals, deco.forChild(offset, child), i) offset += child.nodeSize } return } let decoIndex = 0, active = [], restNode = null for (let parentIndex = 0;;) { if (decoIndex < locals.length && locals[decoIndex].to == offset) { let widget = locals[decoIndex++], widgets while (decoIndex < locals.length && locals[decoIndex].to == offset) (widgets || (widgets = [widget])).push(locals[decoIndex++]) if (widgets) { widgets.sort(compareSide) for (let i = 0; i < widgets.length; i++) onWidget(widgets[i], parentIndex, !!restNode) } else { onWidget(widget, parentIndex, !!restNode) } } let child, index if (restNode) { index = -1 child = restNode restNode = null } else if (parentIndex < parent.childCount) { index = parentIndex child = parent.child(parentIndex++) } else { break } for (let i = 0; i < active.length; i++) if (active[i].to <= offset) active.splice(i--, 1) while (decoIndex < locals.length && locals[decoIndex].from <= offset && locals[decoIndex].to > offset) active.push(locals[decoIndex++]) let end = offset + child.nodeSize if (child.isText) { let cutAt = end if (decoIndex < locals.length && locals[decoIndex].from < cutAt) cutAt = locals[decoIndex].from for (let i = 0; i < active.length; i++) if (active[i].to < cutAt) cutAt = active[i].to if (cutAt < end) { restNode = child.cut(cutAt - offset) child = child.cut(0, cutAt - offset) end = cutAt index = -1 } } let outerDeco = !active.length ? nothing : child.isInline && !child.isLeaf ? active.filter(d => !d.inline) : active.slice() onNode(child, outerDeco, deco.forChild(offset, child), index) offset = end } } // List markers in Mobile Safari will mysteriously disappear // sometimes. This works around that. function iosHacks(dom) { if (dom.nodeName == "UL" || dom.nodeName == "OL") { let oldCSS = dom.style.cssText dom.style.cssText = oldCSS + "; list-style: square !important" window.getComputedStyle(dom).listStyle dom.style.cssText = oldCSS } } function nearbyTextNode(node, offset) { for (;;) { if (node.nodeType == 3) return node if (node.nodeType == 1 && offset > 0) { if (node.childNodes.length > offset && node.childNodes[offset].nodeType == 3) return node.childNodes[offset] node = node.childNodes[offset - 1] offset = nodeSize(node) } else if (node.nodeType == 1 && offset < node.childNodes.length) { node = node.childNodes[offset] offset = 0 } else { return null } } } // Find a piece of text in an inline fragment, overlapping from-to function findTextInFragment(frag, text, from, to) { for (let i = 0, pos = 0; i < frag.childCount && pos <= to;) { let child = frag.child(i++), childStart = pos pos += child.nodeSize if (!child.isText) continue let str = child.text while (i < frag.childCount) { let next = frag.child(i++) pos += next.nodeSize if (!next.isText) break str += next.text } if (pos >= from) { let found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1 if (found >= 0 && found + text.length + childStart >= from) return childStart + found if (from == to && str.length >= (to + text.length) - childStart && str.slice(to - childStart, to - childStart + text.length) == text) return to } } return -1 } // Replace range from-to in an array of view descs with replacement // (may be null to just delete). This goes very much against the grain // of the rest of this code, which tends to create nodes with the // right shape in one go, rather than messing with them after // creation, but is necessary in the composition hack. function replaceNodes(nodes, from, to, view, replacement) { let result = [] for (let i = 0, off = 0; i < nodes.length; i++) { let child = nodes[i], start = off, end = off += child.size if (start >= to || end <= from) { result.push(child) } else { if (start < from) result.push(child.slice(0, from - start, view)) if (replacement) { result.push(replacement) replacement = null } if (end > to) result.push(child.slice(to - start, child.size, view)) } } return result } prosemirror-view-1.23.13/style/000077500000000000000000000000001423202571000163505ustar00rootroot00000000000000prosemirror-view-1.23.13/style/prosemirror.css000066400000000000000000000020601423202571000214430ustar00rootroot00000000000000.ProseMirror { position: relative; } .ProseMirror { word-wrap: break-word; white-space: pre-wrap; white-space: break-spaces; -webkit-font-variant-ligatures: none; font-variant-ligatures: none; font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ } .ProseMirror pre { white-space: pre-wrap; } .ProseMirror li { position: relative; } .ProseMirror-hideselection *::selection { background: transparent; } .ProseMirror-hideselection *::-moz-selection { background: transparent; } .ProseMirror-hideselection { caret-color: transparent; } .ProseMirror-selectednode { outline: 2px solid #8cf; } /* Make sure li selections wrap around markers */ li.ProseMirror-selectednode { outline: none; } li.ProseMirror-selectednode:after { content: ""; position: absolute; left: -32px; right: -2px; top: -2px; bottom: -2px; border: 2px solid #8cf; pointer-events: none; } /* Protect against generic img rules */ img.ProseMirror-separator { display: inline !important; border: none !important; margin: 0 !important; } prosemirror-view-1.23.13/test/000077500000000000000000000000001423202571000161675ustar00rootroot00000000000000prosemirror-view-1.23.13/test/index.html000066400000000000000000000006121423202571000201630ustar00rootroot00000000000000 ProseMirror-view tests

    ProseMirror Tests

    prosemirror-view-1.23.13/test/link-mocha-css.js000066400000000000000000000005241423202571000213360ustar00rootroot00000000000000let path = require("path"), child = require("child_process") let source = path.resolve(path.dirname(require.resolve("mocha/mocha")), "mocha.css") // declare global: __dirname let dest = __dirname + "/mocha.css" child.execFileSync("rm", ["-rf", dest], {stdio: "inherit"}) child.execFileSync("ln", ["-s", source, dest], {stdio: "inherit"}) prosemirror-view-1.23.13/test/prosemirror.css000077700000000000000000000000001423202571000257272../style/prosemirror.cssustar00rootroot00000000000000prosemirror-view-1.23.13/test/test-clipboard.js000066400000000000000000000107141423202571000214440ustar00rootroot00000000000000const ist = require("ist") const {eq, doc, p, strong, blockquote, ul, ol, li, hr, br} = require("prosemirror-test-builder") const {NodeSelection, TextSelection} = require("prosemirror-state") const {Slice, Fragment} = require("prosemirror-model") const {tempEditor} = require("./view") const {__serializeForClipboard: serializeForClipboard, __parseFromClipboard: parseFromClipboard} = require("..") describe("Clipboard interface", () => { it("copies only the node for a node selection", () => { let d = doc(blockquote(p("a"), "
    ", hr), p("b")) let view = tempEditor({doc: d}) let {dom} = serializeForClipboard(view, NodeSelection.create(d, d.tag.a).content()) ist(dom.innerHTML, '
    ') ist(parseFromClipboard(view, "", dom.innerHTML, false, d.resolve(1)), d.slice(d.tag.a, d.tag.a + 1), eq) }) it("includes context for text selections", () => { let d = doc(blockquote(ul(li(p("fo
    o"), p("bar"))))) let view = tempEditor({doc: d}) let slice = TextSelection.create(d, d.tag.a, d.tag.b).content(), {dom, text} = serializeForClipboard(view, slice) ist(dom.innerHTML, '
  • o

    b

  • ') ist(parseFromClipboard(view, text, dom.innerHTML, false, d.resolve(1)), d.slice(d.tag.a, d.tag.b, true), eq) ist(parseFromClipboard(view, text, dom.innerHTML, true, d.resolve(1)), new Slice(doc(p("o"), p("b")).content, 1, 1), eq) }) it("preserves open nodes", () => { let d = doc(blockquote(blockquote(p("foo")))) let view = tempEditor({doc: d}) let slice = new Slice(Fragment.from(d.firstChild), 1, 1) let html = serializeForClipboard(view, slice).dom.innerHTML let parsed = parseFromClipboard(view, "-", html, false, d.resolve(1)) ist(parsed, slice, eq) }) it("uses clipboardTextSerializer when given", () => { let view = tempEditor({doc: doc(p("hello")), clipboardTextSerializer(_) { return "OK" }}) let {text} = serializeForClipboard(view, view.state.doc.slice(1, 6)) ist(text, "OK") }) it("can read external HTML", () => { let view = tempEditor(), $p = view.state.doc.resolve(1) ist(parseFromClipboard(view, "", "

    hello


    ", false, $p), new Slice(doc(p("hello"), hr).content, 1, 0), eq) ist(parseFromClipboard(view, "", "

    hello

    bar", false, $p), new Slice(doc(p("hello"), p("bar")).content, 1, 1), eq) }) it("will sanely clean up top-level nodes in HTML", () => { let view = tempEditor(), $p = view.state.doc.resolve(1) ist(parseFromClipboard(view, "", "
    • foo
    bar
    baz", false, $p), new Slice(doc(ul(li(p("foo"))), p("bar", br, "baz")).content, 3, 1), eq) ist(parseFromClipboard(view, "", "
    • foo
    bar

    x

    ", false, $p), new Slice(doc(ul(li(p("foo"))), p("bar", br), p("x")).content, 3, 1), eq) ist(parseFromClipboard(view, "", "
  • foo
  • bar
  • x

    ", false, $p), new Slice(doc(ol(li(p("foo")), li(p("bar"))), p("x")).content, 3, 1), eq) }) it("only drops trailing br nodes in block parents", () => { let view = tempEditor() ist(parseFromClipboard(view, "", "

    a
    b

    ", false, view.state.doc.resolve(1)), new Slice(doc(p(strong("a"), strong(br), " b")).content, 1, 1), eq) }) it("will call transformPastedHTML", () => { let view = tempEditor({transformPastedHTML(_) { return "abc" }}) ist(parseFromClipboard(view, "", "def", false, view.state.doc.resolve(1)), new Slice(p("abc").content, 0, 0), eq) }) it("will call transformPastedText", () => { let view = tempEditor({transformPastedText(_) { return "abc" }}) ist(parseFromClipboard(view, "def", null, false, view.state.doc.resolve(1)), new Slice(doc(p("abc")).content, 1, 1), eq) }) it("allows text parsing to be overridden with clipboardTextParser", () => { let view = tempEditor({clipboardTextParser(text) { return doc(p(text.toUpperCase())).slice(1, text.length + 1) }}) ist(parseFromClipboard(view, "abc", null, false, view.state.doc.resolve(1)), new Slice(p("ABC").content, 0, 0), eq) }) it("preserves attributes", () => { let d = doc(ol({order: 3}, li(p("f
    oo")))) let view = tempEditor({doc: d}) let {dom, text} = serializeForClipboard(view, TextSelection.create(d, d.tag.a, d.tag.b).content()) ist(parseFromClipboard(view, text, dom.innerHTML, false, d.resolve(1)), d.slice(d.tag.a, d.tag.b, true), eq) }) }) prosemirror-view-1.23.13/test/test-composition.js000066400000000000000000000257731423202571000220630ustar00rootroot00000000000000const {schema, eq, doc, p, em, code, strong} = require("prosemirror-test-builder") const ist = require("ist") const {tempEditor, requireFocus, findTextNode} = require("./view") const {Decoration, DecorationSet, __endComposition} = require("..") const {Plugin} = require("prosemirror-state") // declare global: CompositionEvent function event(pm, type) { pm.dom.dispatchEvent(new CompositionEvent(type)) } function edit(node, text = "", from = node.nodeValue.length, to = from) { let val = node.nodeValue node.nodeValue = val.slice(0, from) + text + val.slice(to) document.getSelection().collapse(node, from + text.length) return node } function hasCompositionNode(_pm) { let {focusNode} = document.getSelection() while (focusNode && !focusNode.pmViewDesc) focusNode = focusNode.parentNode return focusNode && focusNode.pmViewDesc.constructor.name == "CompositionViewDesc" } function compose(pm, start, update, options = {}) { event(pm, "compositionstart") ist(pm.composing) let node, sel = document.getSelection() for (let i = -1; i < update.length; i++) { if (i < 0) node = start() else update[i](node) let {focusNode, focusOffset} = sel pm.domObserver.flush() if (options.cancel && i == update.length - 1) { ist(!hasCompositionNode(pm)) } else { ist(node.parentNode && pm.dom.contains(node.parentNode)) ist(sel.focusNode, focusNode) ist(sel.focusOffset, focusOffset) if (options.node) ist(hasCompositionNode(pm)) } } event(pm, "compositionend") if (options.end) { options.end(node) pm.domObserver.flush() } __endComposition(pm) ist(!pm.composing) ist(!hasCompositionNode(pm)) } function wordDeco(state) { let re = /\w+/g, deco = [] state.doc.descendants((node, pos) => { if (node.isText) for (let m; m = re.exec(node.text);) deco.push(Decoration.inline(pos + m.index, pos + m.index + m[0].length, {class: "word"})) }) return DecorationSet.create(state.doc, deco) } const wordHighlighter = new Plugin({ props: {decorations: wordDeco} }) function widgets(positions, sides) { return new Plugin({ state: { init(state) { let deco = positions.map((p, i) => Decoration.widget(p, () => { let s = document.createElement("var") s.textContent = "×" return s }, {side: sides[i]})) return DecorationSet.create(state.doc, deco) }, apply(tr, deco) { return deco.map(tr.mapping, tr.doc) } }, props: { decorations(state) { return this.getState(state) } } }) } describe("EditorView composition", () => { it("supports composition in an empty block", () => { let pm = requireFocus(tempEditor({doc: doc(p(""))})) compose(pm, () => edit(pm.dom.firstChild.appendChild(document.createTextNode("a"))), [ n => edit(n, "b"), n => edit(n, "c") ], {node: true}) ist(pm.state.doc, doc(p("abc")), eq) }) it("supports composition at end of block", () => { let pm = requireFocus(tempEditor({doc: doc(p("foo"))})) compose(pm, () => edit(findTextNode(pm.dom, "foo")), [ n => edit(n, "!"), n => edit(n, "?") ]) ist(pm.state.doc, doc(p("foo!?")), eq) }) it("supports composition at end of block in a new node", () => { let pm = requireFocus(tempEditor({doc: doc(p("foo"))})) compose(pm, () => edit(pm.dom.firstChild.appendChild(document.createTextNode("!"))), [ n => edit(n, "?") ], {node: true}) ist(pm.state.doc, doc(p("foo!?")), eq) }) it("supports composition at start of block in a new node", () => { let pm = requireFocus(tempEditor({doc: doc(p("foo"))})) compose(pm, () => { let p = pm.dom.firstChild return edit(p.insertBefore(document.createTextNode("!"), p.firstChild)) }, [ n => edit(n, "?") ], {node: true}) ist(pm.state.doc, doc(p("!?foo")), eq) }) it("supports composition inside existing text", () => { let pm = requireFocus(tempEditor({doc: doc(p("foo"))})) compose(pm, () => edit(findTextNode(pm.dom, "foo")), [ n => edit(n, "x", 1), n => edit(n, "y", 2), n => edit(n, "z", 3) ]) ist(pm.state.doc, doc(p("fxyzoo")), eq) }) it("can deal with Android-style newline-after-composition", () => { let pm = requireFocus(tempEditor({doc: doc(p("abcdef"))})) compose(pm, () => edit(findTextNode(pm.dom, "abcdef")), [ n => edit(n, "x", 3), n => edit(n, "y", 4) ], {end: n => { let line = pm.dom.appendChild(document.createElement("div")) line.textContent = "def" n.nodeValue = "abcxy" document.getSelection().collapse(line, 0) }}) ist(pm.state.doc, doc(p("abcxy"), p("def")), eq) }) it("handles replacement of existing words", () => { let pm = requireFocus(tempEditor({doc: doc(p("one two three"))})) compose(pm, () => edit(findTextNode(pm.dom, "one two three"), "five", 4, 7), [ n => edit(n, "seven", 4, 8), n => edit(n, "zero", 4, 9) ]) ist(pm.state.doc, doc(p("one zero three")), eq) }) it("handles composition inside marks", () => { let pm = requireFocus(tempEditor({doc: doc(p("one ", em("two")))})) compose(pm, () => edit(findTextNode(pm.dom, "two"), "o"), [ n => edit(n, "o"), n => edit(n, "w") ]) ist(pm.state.doc, doc(p("one ", em("twooow"))), eq) }) it("handles composition in a mark that has multiple children", () => { let pm = requireFocus(tempEditor({doc: doc(p("one ", em("two", strong(" three"))))})) compose(pm, () => edit(findTextNode(pm.dom, "two"), "o"), [ n => edit(n, "o"), n => edit(n, "w") ]) ist(pm.state.doc, doc(p("one ", em("twooow", strong(" three")))), eq) }) it("supports composition in a cursor wrapper", () => { let pm = requireFocus(tempEditor({doc: doc(p(""))})) pm.dispatch(pm.state.tr.addStoredMark(schema.marks.em.create())) compose(pm, () => edit(pm.dom.firstChild.appendChild(document.createTextNode("")), "a"), [ n => edit(n, "b"), n => edit(n, "c") ], {node: true}) ist(pm.state.doc, doc(p(em("abc"))), eq) }) it("handles composition in a multi-child mark with a cursor wrapper", () => { let pm = requireFocus(tempEditor({doc: doc(p("one ", em("two", strong(" three"))))})) pm.dispatch(pm.state.tr.addStoredMark(schema.marks.code.create())) let emNode = pm.dom.querySelector("em") compose(pm, () => edit(emNode.insertBefore(document.createTextNode(""), emNode.querySelector("strong")), "o"), [ n => edit(n, "o"), n => edit(n, "w") ], {node: true}) ist(pm.state.doc, doc(p("one ", em("two", code("oow"), strong(" three")))), eq) }) it("doesn't get interrupted by changes in decorations", () => { let pm = requireFocus(tempEditor({doc: doc(p("foo ...")), plugins: [wordHighlighter]})) compose(pm, () => edit(findTextNode(pm.dom, " ...")), [ n => edit(n, "hi", 1, 4) ]) ist(pm.state.doc, doc(p("foo hi")), eq) }) it("works inside highlighted text", () => { let pm = requireFocus(tempEditor({doc: doc(p("one two")), plugins: [wordHighlighter]})) compose(pm, () => edit(findTextNode(pm.dom, "one"), "x"), [ n => edit(n, "y"), n => edit(n, ".") ]) ist(pm.state.doc, doc(p("onexy. two")), eq) }) it("can handle compositions spanning multiple nodes", () => { let pm = requireFocus(tempEditor({doc: doc(p("one two")), plugins: [wordHighlighter]})) compose(pm, () => edit(findTextNode(pm.dom, "two"), "a"), [ n => edit(n, "b"), n => edit(n, "c") ], {end: n => { n.parentNode.previousSibling.remove() n.parentNode.previousSibling.remove() return edit(n, "xyzone ", 0) }}) ist(pm.state.doc, doc(p("xyzone twoabc")), eq) }) it("doesn't overwrite widgets next to the composition", () => { let pm = requireFocus(tempEditor({doc: doc(p("")), plugins: [widgets([1, 1], [-1, 1])]})) compose(pm, () => { let p = pm.dom.firstChild return edit(p.insertBefore(document.createTextNode("a"), p.lastChild)) }, [n => edit(n, "b", 0, 1)], {end: () => { ist(pm.dom.querySelectorAll("var").length, 2) }}) ist(pm.state.doc, doc(p("b")), eq) }) it("cancels composition when a change fully overlaps with it", () => { let pm = requireFocus(tempEditor({doc: doc(p("one"), p("two"), p("three"))})) compose(pm, () => edit(findTextNode(pm.dom, "two"), "x"), [ () => pm.dispatch(pm.state.tr.insertText("---", 3, 13)) ], {cancel: true}) ist(pm.state.doc, doc(p("on---hree")), eq) }) it("cancels composition when a change partially overlaps with it", () => { let pm = requireFocus(tempEditor({doc: doc(p("one"), p("two"), p("three"))})) compose(pm, () => edit(findTextNode(pm.dom, "two"), "x", 0), [ () => pm.dispatch(pm.state.tr.insertText("---", 7, 15)) ], {cancel: true}) ist(pm.state.doc, doc(p("one"), p("x---ee")), eq) }) it("cancels composition when a change happens inside of it", () => { let pm = requireFocus(tempEditor({doc: doc(p("one"), p("two"), p("three"))})) compose(pm, () => edit(findTextNode(pm.dom, "two"), "x", 0), [ () => pm.dispatch(pm.state.tr.insertText("!", 7, 8)) ], {cancel: true}) ist(pm.state.doc, doc(p("one"), p("x!wo"), p("three")), eq) }) it("doesn't cancel composition when a change happens elsewhere", () => { let pm = requireFocus(tempEditor({doc: doc(p("one"), p("two"), p("three"))})) compose(pm, () => edit(findTextNode(pm.dom, "two"), "x", 0), [ n => edit(n, "y", 1), () => pm.dispatch(pm.state.tr.insertText("!", 2, 3)), n => edit(n, "z", 2) ]) ist(pm.state.doc, doc(p("o!e"), p("xyztwo"), p("three")), eq) }) it("handles compositions rapidly following each other", () => { let pm = requireFocus(tempEditor({doc: doc(p("one"), p("two"))})) event(pm, "compositionstart") let one = findTextNode(pm.dom, "one") edit(one, "!") pm.domObserver.flush() event(pm, "compositionend") one.nodeValue = "one!!" let L2 = pm.dom.lastChild event(pm, "compositionstart") let two = findTextNode(pm.dom, "two") ist(pm.dom.lastChild, L2) edit(two, ".") window.two = two pm.domObserver.flush() ist(document.getSelection().focusNode, two) ist(document.getSelection().focusOffset, 4) ist(pm.composing) event(pm, "compositionend") pm.domObserver.flush() ist(pm.state.doc, doc(p("one!!"), p("two.")), eq) }) function crossParagraph(first) { let pm = requireFocus(tempEditor({doc: doc(p("one two"), p("three"), p("four five"))})) compose(pm, () => { for (let i = 0; i < 2; i++) pm.dom.removeChild(first ? pm.dom.lastChild : pm.dom.firstChild) let target = pm.dom.firstChild.firstChild target.nodeValue = "one A five" document.getSelection().collapse(target, 4) return target }, [ n => edit(n, "B", 4, 5), n => edit(n, "C", 4, 5) ]) ist(pm.state.doc, doc(p("one C five")), eq) } it("can handle cross-paragraph compositions", () => crossParagraph(true)) it("can handle cross-paragraph compositions (keeping the last paragraph)", () => crossParagraph(false)) }) prosemirror-view-1.23.13/test/test-decoration.js000066400000000000000000000306361423202571000216410ustar00rootroot00000000000000const ist = require("ist") const {schema, doc, p, h1, li, ul, blockquote} = require("prosemirror-test-builder") const {Transform, ReplaceAroundStep, liftTarget} = require("prosemirror-transform") const {Slice, NodeRange} = require("prosemirror-model") const {Decoration, DecorationSet} = require("..") let widget = document.createElement("button") function make(d) { if (d.type) return d if (d.pos != null) return Decoration.widget(d.pos, d.widget || widget, d) if (d.node) return Decoration.node(d.from, d.to, d.attrs || {}, d) return Decoration.inline(d.from, d.to, d.attrs || {}, d) } function build(doc, ...decorations) { return DecorationSet.create(doc, decorations.map(make)) } function str(set) { if (!set) return "[]" let s = "[" + set.local.map(d => d.from + "-" + d.to).join(", ") for (let i = 0; i < set.children.length; i += 3) s += (s.length > 1 ? ", " : "") + set.children[i] + ": " + str(set.children[i + 2]) return s + "]" } function arrayStr(arr) { return arr.map(d => d.from + "-" + d.to).join(", ") } function buildMap(doc, ...decorations) { let f = decorations.pop() let oldSet = build(doc, ...decorations) let tr = f(new Transform(doc)) return {set: oldSet.map(tr.mapping, tr.doc), oldSet} } function buildAdd(doc, ...decorations) { let toAdd = decorations.pop() return build(doc, ...decorations).add(doc, Array.isArray(toAdd) ? toAdd.map(make) : [make(toAdd)]) } function buildRem(doc, ...decorations) { let toAdd = decorations.pop() return build(doc, ...decorations).remove(Array.isArray(toAdd) ? toAdd.map(make) : [make(toAdd)]) } describe("DecorationSet", () => { it("builds up a matching tree", () => { let set = build(doc(p("foo"), blockquote(p("bar"))), {from: 2, to: 3}, {from: 8, to: 9}) ist(str(set), "[0: [1-2], 5: [0: [1-2]]]") }) it("does not build nodes when there are no decorations", () => { let set = build(doc(p("foo"), blockquote(p("bar"))), {from: 8, to: 9}) ist(str(set), "[5: [0: [1-2]]]") }) it("puts decorations between children in local", () => { let set = build(doc(p("a"), p("b")), {pos: 3}) ist(str(set), "[3-3]") }) it("puts decorations spanning children in local", () => { let set = build(doc(p("a"), p("b")), {from: 1, to: 5}) ist(str(set), "[1-5]") }) it("puts node decorations in the parent node", () => { let set = build(doc(p("a"), p("b")), {from: 3, to: 6, node: true}) ist(str(set), "[3-6]") }) it("drops empty inline decorations", () => { let set = build(doc(p()), {from: 1, to: 1}) ist(str(set), "[]") }) describe("find", () => { it("finds all when no arguments are given", () => { let set = build(doc(p("a"), p("b")), {from: 1, to: 2}, {pos: 3}) ist(arrayStr(set.find()), "3-3, 1-2") }) it("finds only those within the given range", () => { let set = build(doc(p("a"), p("b")), {from: 1, to: 2}, {pos: 1}, {from: 4, to: 5}) ist(arrayStr(set.find(0, 3)), "1-1, 1-2") }) it("finds decorations at the edge of the range", () => { let set = build(doc(p("a"), p("b")), {from: 1, to: 2}, {pos: 3}, {from: 4, to: 5}) ist(arrayStr(set.find(2, 3)), "3-3, 1-2") }) it("returns the correct offset for deeply nested decorations", () => { let set = build(doc(blockquote(blockquote(p("a")))), {from: 3, to: 4}) ist(arrayStr(set.find()), "3-4") }) it("can filter by predicate", () => { let set = build(doc(blockquote(blockquote(p("a")))), {from: 3, to: 4, name: "X"}, {from: 3, to: 4, name: "Y"}) ist(set.find(undefined, undefined, x => x.name == "Y").map(d => d.spec.name).join(), "Y") }) }) describe("map", () => { it("supports basic mapping", () => { let {oldSet, set} = buildMap(doc(p("foo"), p("bar")), {from: 2, to: 3}, {from: 7, to: 8}, tr => tr.replaceWith(4, 4, schema.text("!!"))) ist(str(oldSet), "[0: [1-2], 5: [1-2]]") ist(str(set), "[0: [1-2], 7: [1-2]]") }) it("drops deleted decorations", () => { let {set} = buildMap(doc(p("foobar")), {from: 2, to: 3}, tr => tr.delete(1, 4)) ist(str(set), "[]") }) it("can map node decorations", () => { let {set} = buildMap(doc(blockquote(p("a"), p("b"))), {from: 4, to: 7, node: true}, tr => tr.delete(1, 4)) ist(str(set), "[0: [0-3]]") }) it("can map inside node decorations", () => { let {set} = buildMap(doc(blockquote(p("a"), p("b"))), {from: 4, to: 7, node: true}, tr => tr.replaceWith(5, 5, schema.text("c"))) ist(str(set), "[0: [3-7]]") }) it("removes partially overwritten node decorations", () => { let {set} = buildMap(doc(p("a"), p("b")), {from: 0, to: 3, node: true}, tr => tr.delete(2, 4)) ist(str(set), "[]") }) it("removes exactly overwritten node decorations", () => { let {set} = buildMap(doc(p("a"), p("b")), {from: 0, to: 3, node: true}, tr => tr.replaceWith(0, 3, schema.nodes.horizontal_rule.create())) ist(str(set), "[]") }) it("isn't inclusive by default", () => { let {set} = buildMap(doc(p("foo")), {from: 2, to: 3}, tr => tr.replaceWith(2, 2, schema.text(".")).replaceWith(4, 4, schema.text("?"))) ist(str(set), "[0: [2-3]]") }) it("understands unclusiveLeft", () => { let {set} = buildMap(doc(p("foo")), {from: 2, to: 3, inclusiveStart: true}, tr => tr.replaceWith(2, 2, schema.text(".")).replaceWith(4, 4, schema.text("?"))) ist(str(set), "[0: [1-3]]") }) it("understands unclusiveRight", () => { let {set} = buildMap(doc(p("foo")), {from: 2, to: 3, inclusiveEnd: true}, tr => tr.replaceWith(2, 2, schema.text(".")).replaceWith(4, 4, schema.text("?"))) ist(str(set), "[0: [2-4]]") }) it("preserves subtrees not touched by mapping", () => { let {oldSet, set} = buildMap(doc(p("foo"), blockquote(p("bar"), p("baz"))), {from: 2, to: 3}, {from: 8, to: 9}, {from: 13, to: 14}, tr => tr.delete(8, 9)) ist(str(set), "[0: [1-2], 5: [4: [1-2]]]") ist(set.children[2], oldSet.children[2]) // FIXME sane accessors? ist(set.children[5].children[2], oldSet.children[5].children[5]) }) it("rebuilds when a node is joined", () => { let {set} = buildMap(doc(p("foo"), p("bar")), {from: 2, to: 3}, {from: 7, to: 8}, tr => tr.join(5)) ist(str(set), "[0: [1-2, 4-5]]") }) it("rebuilds when a node is split", () => { let {set} = buildMap(doc(p("foobar")), {from: 2, to: 3}, {from: 5, to: 6}, tr => tr.split(4)) ist(str(set), "[0: [1-2], 5: [1-2]]") }) it("correctly rebuilds a deep structure", () => { let {oldSet, set} = buildMap(doc(blockquote(p("foo")), blockquote(blockquote(p("bar")))), {from: 3, to: 4}, {from: 11, to: 12}, tr => tr.join(7)) ist(str(oldSet), "[0: [0: [1-2]], 7: [0: [0: [1-2]]]]") ist(str(set), "[0: [0: [1-2], 5: [0: [1-2]]]]") }) it("calls onRemove when dropping decorations", () => { let d = doc(blockquote(p("hello"), p("abc"))) let set = build(d, {from: 3, to: 5, name: "a"}, {pos: 10, name: "b"}) let tr = new Transform(d).delete(2, 6), dropped = [] set.map(tr.mapping, tr.doc, {onRemove: o => dropped.push(o.name)}) ist(JSON.stringify(dropped), '["a"]') let tr2 = new Transform(d).delete(0, d.content.size), dropped2 = [] set.map(tr2.mapping, tr2.doc, {onRemove: o => dropped2.push(o.name)}) ist(JSON.stringify(dropped2.sort()), '["a","b"]') }) it("respects the side option on widgets", () => { let d = doc(p("foo")) let set = build(d, {pos: 3, side: -1, name: "a"}, {pos: 3, name: "b"}) let tr = new Transform(d).replaceWith(3, 3, schema.text("ay")) let result = set.map(tr.mapping, tr.doc).find().map(d => d.from + "-" + d.spec.name).sort().join(", ") ist(result, "3-a, 5-b") }) it("doesn't doubly map decorations nested in multiple nodes", () => { let d = doc(h1("u"), blockquote(p())) let set = build(d, {pos: 5}) let tr = new Transform(d).replaceWith(0, 3, schema.node("heading", {level: 1})) ist(set.map(tr.mapping, tr.doc).find().map(d => d.from).join(), "4") }) it("rebuilds subtrees correctly at an offset", () => { let d = doc(p("foobar"), ul(li(p("abc")), li(p("b")))) let set = build(d, {pos: 18}) let tr = new Transform(d).join(16) ist(set.map(tr.mapping, tr.doc).find().map(d => d.from).join(), "16") }) it("properly maps decorations after deleted siblings", () => { let d = doc(blockquote(blockquote(blockquote(p()), blockquote(p())), blockquote(blockquote(p()), blockquote(p())))) let set = build(d, {pos: 14}) let tr = new Transform(d).delete(2, 6).delete(8, 12) ist(set.map(tr.mapping, tr.doc).find().length, 0) }) it("can map the content of nodes that moved in the same transaction", () => { let d = doc(ul(li(p("a"))), p("bc")) let set = build(d, {from: 8, to: 10}) let tr = new Transform(d).step(new ReplaceAroundStep(0, 7, 2, 5, Slice.empty, 0, true)) let mapped = set.map(tr.mapping, tr.doc).find()[0] ist(mapped.from, 4) ist(mapped.to, 6) }) it("can handle nodes moving up multiple levels", () => { let d = doc(ul(li(p()))) let set = build(d, {node: true, from: 2, to: 4}) let range = new NodeRange(d.resolve(2), d.resolve(4), 2) let tr = new Transform(d).lift(range, liftTarget(range)) let mapped = set.map(tr.mapping, tr.doc).find() ist(mapped.length, 1) ist(mapped[0].from, 0) ist(mapped[0].to, 2) }) }) describe("add", () => { it("can add a local decoration", () => { ist(str(buildAdd(doc(p("foo"), p("bar")), {pos: 0}, {pos: 5})), "[0-0, 5-5]") }) it("can add a decoration in a new child", () => { ist(str(buildAdd(doc(p("foo"), p("bar")), {pos: 0}, {pos: 3})), "[0-0, 0: [2-2]]") }) it("can add a decoration to an existing child", () => { ist(str(buildAdd(doc(p("foo"), p("bar")), {pos: 1}, {pos: 3})), "[0: [0-0, 2-2]]") }) it("can add a decoration beyond an existing child", () => { ist(str(buildAdd(doc(blockquote(p("foo"))), {pos: 1}, {pos: 4})), "[0: [0-0, 0: [2-2]]]") }) it("can add multiple decorations", () => { ist(str(buildAdd(doc(p("foo"), p("bar")), {pos: 1}, {from: 6, to: 9}, [{from: 1, to: 4}, {pos: 7}, {pos: 8}])), "[0: [0-0, 0-3], 5: [0-3, 1-1, 2-2]]") }) }) describe("remove", () => { it("can delete a decoration", () => { let d = make({pos: 2}) ist(str(buildRem(doc(p("foo")), {pos: 1}, d, d)), "[0: [0-0]]") }) it("can delete multiple decorations", () => { let d1 = make({pos: 2}), d2 = make({from: 2, to: 8}) ist(str(buildRem(doc(p("foo"), p("bar")), {pos: 1}, {from: 6, to: 7}, d1, d2, [d1, d2])), "[0: [0-0], 5: [0-1]]") }) it("ignores decorations that don't exist", () => { ist(str(buildRem(doc(p("foo")), {pos: 5}, {pos: 2})), "[5-5]") }) it("compares by both position and type when removing", () => { let deco = DecorationSet.create(doc(p("one")), [[1, 2], [3, 4]].map(([from, to]) => Decoration.inline(from, to))) ist(deco.remove([deco.find()[0]]).find().length, 1) }) }) }) describe("removeOverlap", () => { it("returns the original array when there is no overlap", () => { let decs = [make({pos: 1}), make({from: 1, to: 4}), make({from: 1, to: 4})] ist(DecorationSet.removeOverlap(decs), decs) }) it("splits a partially overlapping decoration", () => { let decs = [make({from: 1, to: 2}), make({from: 1, to: 4}), make({from: 3, to: 4})] ist(arrayStr(DecorationSet.removeOverlap(decs)), "1-2, 1-2, 2-3, 3-4, 3-4") }) it("splits a decoration that spans multiple widgets", () => { let decs = [make({from: 1, to: 5}), make({pos: 2}), make({pos: 3})] ist(arrayStr(DecorationSet.removeOverlap(decs)), "1-2, 2-2, 2-3, 3-3, 3-5") }) it("correctly splits overlapping inline decorations", () => { let decs = [make({from: 0, to: 6}), make({from: 1, to: 4}), make({from: 3, to: 5})] ist(arrayStr(DecorationSet.removeOverlap(decs)), "0-1, 1-3, 1-3, 3-4, 3-4, 3-4, 4-5, 4-5, 5-6") }) }) prosemirror-view-1.23.13/test/test-domchange.js000066400000000000000000000350731423202571000214370ustar00rootroot00000000000000const ist = require("ist") const {eq, doc, p, pre, h1, a, em, img: img_, br, strong, blockquote} = require("prosemirror-test-builder") const {EditorState, TextSelection} = require("prosemirror-state") const {tempEditor, findTextNode} = require("./view") const img = img_({src: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="}) function setSel(aNode, aOff, fNode, fOff) { let r = document.createRange(), s = window.getSelection() r.setEnd(fNode || aNode, fNode ? fOff : aOff) r.setStart(aNode, aOff) s.removeAllRanges() s.addRange(r) } function flush(view) { view.domObserver.flush() } describe("DOM change", () => { it("notices when text is added", () => { let view = tempEditor({doc: doc(p("hello"))}) findTextNode(view.dom, "hello").nodeValue = "heLllo" flush(view) ist(view.state.doc, doc(p("heLllo")), eq) }) it("notices when text is removed", () => { let view = tempEditor({doc: doc(p("hello"))}) findTextNode(view.dom, "hello").nodeValue = "heo" flush(view) ist(view.state.doc, doc(p("heo")), eq) }) it("handles ambiguous changes", () => { let view = tempEditor({doc: doc(p("hello"))}) findTextNode(view.dom, "hello").nodeValue = "helo" flush(view) ist(view.state.doc, doc(p("helo")), eq) }) it("respects stored marks", () => { let view = tempEditor({doc: doc(p("hello"))}) view.dispatch(view.state.tr.addStoredMark(view.state.schema.marks.em.create())) findTextNode(view.dom, "hello").nodeValue = "helloo" flush(view) ist(view.state.doc, doc(p("hello", em("o"))), eq) }) it("can add a node", () => { let view = tempEditor({doc: doc(p("hello"))}) let txt = findTextNode(view.dom, "hello") txt.parentNode.appendChild(document.createTextNode("!")) flush(view) ist(view.state.doc, doc(p("hello!")), eq) }) it("can remove a text node", () => { let view = tempEditor({doc: doc(p("hello"))}) let txt = findTextNode(view.dom, "hello") txt.parentNode.removeChild(txt) flush(view) ist(view.state.doc, doc(p()), eq) }) it("can add a paragraph", () => { let view = tempEditor({doc: doc(p("hello"))}) view.dom.insertBefore(document.createElement("p"), view.dom.firstChild) .appendChild(document.createTextNode("hey")) flush(view) ist(view.state.doc, doc(p("hey"), p("hello")), eq) }) it("supports duplicating a paragraph", () => { let view = tempEditor({doc: doc(p("hello"))}) view.dom.insertBefore(document.createElement("p"), view.dom.firstChild) .appendChild(document.createTextNode("hello")) flush(view) ist(view.state.doc, doc(p("hello"), p("hello")), eq) }) it("support inserting repeated text", () => { let view = tempEditor({doc: doc(p("hello"))}) findTextNode(view.dom, "hello").nodeValue = "helhello" flush(view) ist(view.state.doc, doc(p("helhello")), eq) }) it("detects an enter press", () => { let enterPressed = false let view = tempEditor({ doc: doc(blockquote(p("foo"), p(""))), handleKeyDown: (_view, event) => { if (event.keyCode == 13) return enterPressed = true } }) let bq = view.dom.querySelector("blockquote") bq.appendChild(document.createElement("p")) flush(view) ist(enterPressed) }) it("detects a simple backspace press", () => { let backspacePressed = false let view = tempEditor({ doc: doc(p("foo"), p("bar")), handleKeyDown: (_view, event) => { if (event.keyCode == 8) return backspacePressed = true } }) view.dom.removeChild(view.dom.lastChild) view.dom.firstChild.textContent = "foobar" flush(view) ist(backspacePressed) }) it("detects a complex backspace press", () => { let backspacePressed = false let view = tempEditor({ doc: doc(blockquote(blockquote(p("foo")), p("", br, "bar"))), handleKeyDown: (_view, event) => { if (event.keyCode == 8) return backspacePressed = true } }) let bq = view.dom.firstChild bq.removeChild(bq.lastChild) bq.firstChild.firstChild.innerHTML = "foo
    bar" flush(view) ist(backspacePressed) }) it("doesn't route delete as backspace", () => { let backspacePressed = false let view = tempEditor({ doc: doc(p("foo
    "), p("bar")), handleKeyDown: (_view, event) => { if (event.keyCode == 8) return backspacePressed = true } }) view.dom.removeChild(view.dom.lastChild) view.dom.firstChild.textContent = "foobar" flush(view) ist(!backspacePressed) }) it("correctly adjusts the selection", () => { let view = tempEditor({doc: doc(p("abc"))}) let textNode = findTextNode(view.dom, "abc") textNode.nodeValue = "abcd" setSel(textNode, 3) flush(view) ist(view.state.doc, doc(p("abcd")), eq) ist(view.state.selection.anchor, 4) ist(view.state.selection.head, 4) }) it("handles splitting of a textblock", () => { let view = tempEditor({doc: doc(h1("abc"), p("defg"))}) let para = view.dom.querySelector("p") let split = para.parentNode.appendChild(para.cloneNode()) split.innerHTML = "fg" findTextNode(para, "defg").nodeValue = "dexy" setSel(split.firstChild, 1) flush(view) ist(view.state.doc, doc(h1("abc"), p("dexy"), p("fg")), eq) ist(view.state.selection.anchor, 13) }) it("handles a deep split of nodes", () => { let view = tempEditor({doc: doc(blockquote(p("abcd")))}) let quote = view.dom.querySelector("blockquote") let quote2 = view.dom.appendChild(quote.cloneNode(true)) findTextNode(quote, "abcd").nodeValue = "abx" let text2 = findTextNode(quote2, "abcd") text2.nodeValue = "cd" setSel(text2.parentNode, 0) flush(view) ist(view.state.doc, doc(blockquote(p("abx")), blockquote(p("cd"))), eq) ist(view.state.selection.anchor, 9) }) it("can delete the third instance of a character", () => { let view = tempEditor({doc: doc(p("foo xxx bar"))}) findTextNode(view.dom, "foo xxx bar").nodeValue = "foo xx bar" flush(view) ist(view.state.doc, doc(p("foo xx bar")), eq) }) it("can read a simple composition", () => { let view = tempEditor({doc: doc(p("hello"))}) findTextNode(view.dom, "hello").nodeValue = "hellox" flush(view) ist(view.state.doc, doc(p("hellox")), eq) }) it("can delete text in markup", () => { let view = tempEditor({doc: doc(p("a", em("b", img, strong("cd")), "e"))}) findTextNode(view.dom, "cd").nodeValue = "c" flush(view) ist(view.state.doc, doc(p("a", em("b", img, strong("c")), "e")), eq) }) it("recognizes typing inside markup", () => { let view = tempEditor({doc: doc(p("a", em("b", img, strong("cd")), "e"))}) findTextNode(view.dom, "cd").nodeValue = "cdxy" flush(view) ist(view.state.doc, doc(p("a", em("b", img, strong("cdxy")), "e")), eq) }) it("resolves ambiguous text input", () => { let view = tempEditor({doc: doc(p("foo"))}) view.dispatch(view.state.tr.addStoredMark(view.state.schema.marks.strong.create())) findTextNode(view.dom, "foo").nodeValue = "fooo" flush(view) ist(view.state.doc, doc(p("fo", strong("o"), "o")), eq) }) it("does not repaint a text node when it's typed into", () => { let view = tempEditor({doc: doc(p("foo"))}) findTextNode(view.dom, "foo").nodeValue = "fojo" let mutated = false, observer = new MutationObserver(() => mutated = true) observer.observe(view.dom, {subtree: true, characterData: true, childList: true}) flush(view) ist(view.state.doc, doc(p("fojo")), eq) ist(!mutated) observer.disconnect() }) it("understands text typed into an empty paragraph", () => { let view = tempEditor({doc: doc(p(""))}) view.dom.querySelector("p").textContent = "i" flush(view) ist(view.state.doc, doc(p("i")), eq) }) it("doesn't treat a placeholder BR as real content", () => { let view = tempEditor({doc: doc(p("i"))}) view.dom.querySelector("p").innerHTML = "
    " flush(view) ist(view.state.doc, doc(p()), eq) }) it("fixes text changes when input is ignored", () => { let view = tempEditor({doc: doc(p("foo")), dispatchTransaction: () => null}) findTextNode(view.dom, "foo").nodeValue = "food" flush(view) ist(view.dom.textContent, "foo") }) it("fixes structure changes when input is ignored", () => { let view = tempEditor({doc: doc(p("foo", br, "bar")), dispatchTransaction: () => null}) let para = view.dom.querySelector("p") para.replaceChild(document.createElement("img"), para.lastChild) flush(view) ist(view.dom.textContent, "foobar") }) it("aborts when an incompatible state is set", () => { let view = tempEditor({doc: doc(p("abcde"))}) findTextNode(view.dom, "abcde").nodeValue = "xabcde" view.dispatchEvent({type: "input"}) view.updateState(EditorState.create({doc: doc(p("uvw"))})) flush(view) ist(view.state.doc, doc(p("uvw")), eq) }) it("recognizes a mark change as such", () => { let view = tempEditor({doc: doc(p("one")), dispatchTransaction(tr) { ist(tr.steps.length, 1) ist(tr.steps[0].toJSON().stepType, "addMark") view.updateState(view.state.apply(tr)) }}) view.dom.querySelector("p").innerHTML = "one" flush(view) ist(view.state.doc, doc(p(strong("one"))), eq) }) it("preserves marks on deletion", () => { let view = tempEditor({doc: doc(p("one", em("x
    ")))}) view.dom.querySelector("em").innerText = "" view.dispatchEvent({type: "input"}) flush(view) view.dispatch(view.state.tr.insertText("y")) ist(view.state.doc, doc(p("one", em("y"))), eq) }) it("works when a node's contentDOM is deleted", () => { let view = tempEditor({doc: doc(p("one"), pre("two"))}) view.dom.querySelector("pre").innerText = "" view.dispatchEvent({type: "input"}) flush(view) ist(view.state.doc, doc(p("one"), pre()), eq) ist(view.state.selection.head, 6) }) it("doesn't redraw content with marks when typing in front", () => { let view = tempEditor({doc: doc(p("foo", em("bar"), strong("baz")))}) let bar = findTextNode(view.dom, "bar"), foo = findTextNode(view.dom, "foo") foo.nodeValue = "froo" flush(view) ist(view.state.doc, doc(p("froo", em("bar"), strong("baz"))), eq) ist(bar.parentNode && view.dom.contains(bar.parentNode)) ist(foo.parentNode && view.dom.contains(foo.parentNode)) }) it("doesn't redraw content with marks when typing inside mark", () => { let view = tempEditor({doc: doc(p("foo", em("bar"), strong("baz")))}) let bar = findTextNode(view.dom, "bar"), foo = findTextNode(view.dom, "foo") bar.nodeValue = "baar" flush(view) ist(view.state.doc, doc(p("foo", em("baar"), strong("baz"))), eq) ist(bar.parentNode && view.dom.contains(bar.parentNode)) ist(foo.parentNode && view.dom.contains(foo.parentNode)) }) it("maps input to coordsAtPos through pending changes", () => { let view = tempEditor({doc: doc(p("foo"))}) view.dispatchEvent({type: "input"}) view.dispatch(view.state.tr.insertText("more text")) ist(view.coordsAtPos(13)) }) it("notices text added to a cursor wrapper at the start of a mark", () => { let view = tempEditor({doc: doc(p(strong(a("foo"), "bar")))}) findTextNode(view.dom, "foo").nodeValue = "fooxy" flush(view) ist(view.state.doc, doc(p(strong(a("foo"), "xybar"))), eq) }) it("removes cursor wrapper text when the wrapper otherwise remains valid", () => { let view = tempEditor({doc: doc(p(a(strong("foo"), "bar")))}) findTextNode(view.dom, "foo").nodeValue = "fooq" flush(view) ist(view.state.doc, doc(p(a(strong("fooq"), "bar"))), eq) ist(!findTextNode(view.dom, "\ufeffq")) }) it("doesn't confuse backspace with delete", () => { let steps, view = tempEditor({ doc: doc(p("aa")), dispatchTransaction(tr) { steps = tr.steps view.updateState(view.state.apply(tr)) } }) view.lastKeyCode = 8 view.lastKeyCodeTime = Date.now() findTextNode(view.dom, "aa").nodeValue = "a" flush(view) ist(steps.length, 1) ist(steps[0].from, 1) ist(steps[0].to, 2) }) it("can disambiguate a multiple-character backspace event", () => { let steps, view = tempEditor({ doc: doc(p("foofoo")), dispatchTransaction(tr) { steps = tr.steps view.updateState(view.state.apply(tr)) } }) view.lastKeyCode = 8 view.lastKeyCodeTime = Date.now() findTextNode(view.dom, "foofoo").nodeValue = "foo" flush(view) ist(steps.length, 1) ist(steps[0].from, 1) ist(steps[0].to, 4) }) it("doesn't confuse delete with backspace", () => { let steps, view = tempEditor({ doc: doc(p("aa")), dispatchTransaction(tr) { steps = tr.steps view.updateState(view.state.apply(tr)) } }) findTextNode(view.dom, "aa").nodeValue = "a" flush(view) ist(steps.length, 1) ist(steps[0].from, 2) ist(steps[0].to, 3) }) it("doesn't confuse delete with backspace for multi-character deletions", () => { let steps, view = tempEditor({ doc: doc(p("one foofoo three")), dispatchTransaction(tr) { steps = tr.steps view.updateState(view.state.apply(tr)) } }) findTextNode(view.dom, "one foofoo three").nodeValue = "one foo three" flush(view) ist(steps.length, 1) ist(steps[0].from, 8) ist(steps[0].to, 11) }) it("creates a correct step for an ambiguous selection-deletion", () => { let steps, view = tempEditor({ doc: doc(p("lalala")), dispatchTransaction(tr) { steps = tr.steps view.updateState(view.state.apply(tr)) } }) view.lastKeyCode = 8 view.lastKeyCodeTime = Date.now() findTextNode(view.dom, "lalala").nodeValue = "lala" flush(view) ist(steps.length, 1) ist(steps[0].from, 3) ist(steps[0].to, 5) }) it("creates a step that covers the entire selection for partially-matching replacement", () => { let steps, view = tempEditor({ doc: doc(p("one two three")), dispatchTransaction(tr) { steps = tr.steps view.updateState(view.state.apply(tr)) } }) findTextNode(view.dom, "one two three").nodeValue = "one t three" flush(view) ist(steps.length, 1) ist(steps[0].from, 5) ist(steps[0].to, 8) ist(steps[0].slice.content.toString(), '<"t">') view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, 7, 12))) findTextNode(view.dom, "one t three").nodeValue = "one t e" flush(view) ist(steps.length, 1) ist(steps[0].from, 7) ist(steps[0].to, 12) ist(steps[0].slice.content.toString(), '<"e">') }) }) prosemirror-view-1.23.13/test/test-draw-decoration.js000066400000000000000000000450251423202571000225720ustar00rootroot00000000000000const {doc, p, h1, hr, em, strong, img, blockquote, schema} = require("prosemirror-test-builder") const {Plugin, TextSelection} = require("prosemirror-state") const {Schema} = require("prosemirror-model") const {tempEditor} = require("./view") const {DecorationSet, Decoration} = require("..") const ist = require("ist") function make(str) { if (typeof str != "string") return str let match = /^(\d+)(?:-(\d+))?-(.+)$/.exec(str) if (match[3] == "widget") { let widget = document.createElement("button") widget.textContent = "ω" return Decoration.widget(+match[1], widget) } return Decoration.inline(+match[1], +match[2], {class: match[3]}) } function decoPlugin(decos) { return new Plugin({ state: { init(config) { return DecorationSet.create(config.doc, decos.map(make)) }, apply(tr, set, state) { if (tr.docChanged) set = set.map(tr.mapping, tr.doc) let change = tr.getMeta("updateDecorations") if (change) { if (change.remove) set = set.remove(change.remove) if (change.add) set = set.add(state.doc, change.add) } return set } }, props: { decorations(state) { return this.getState(state) } } }) } function updateDeco(view, add, remove) { view.dispatch(view.state.tr.setMeta("updateDecorations", {add, remove})) } describe("Decoration drawing", () => { it("draws inline decorations", () => { let view = tempEditor({doc: doc(p("foobar")), plugins: [decoPlugin(["2-5-foo"])]}) let found = view.dom.querySelector(".foo") ist(found) ist(found.textContent, "oob") }) it("draws wrapping decorations", () => { let view = tempEditor({doc: doc(p("foo")), plugins: [decoPlugin([Decoration.inline(1, 5, {nodeName: "i"})])]}) let found = view.dom.querySelector("i") ist(found && found.innerHTML, "foo") }) it("draws node decorations", () => { let view = tempEditor({doc: doc(p("foo"), p("bar")), plugins: [decoPlugin([Decoration.node(5, 10, {class: "cls"})])]}) let found = view.dom.querySelectorAll(".cls") ist(found.length, 1) ist(found[0].nodeName, "P") ist(found[0].previousSibling.nodeName, "P") }) it("can update multi-level wrapping decorations", () => { let d2 = Decoration.inline(1, 5, {nodeName: "i", class: "b"}) let view = tempEditor({doc: doc(p("hello")), plugins: [decoPlugin([Decoration.inline(1, 5, {nodeName: "i", class: "a"}), d2])]}) ist(view.dom.querySelectorAll("i").length, 2) updateDeco(view, [Decoration.inline(1, 5, {nodeName: "i", class: "c"})], [d2]) let iNodes = view.dom.querySelectorAll("i") ist(iNodes.length, 2) ist(Array.prototype.map.call(iNodes, n => n.className).sort().join(), "a,c") }) it("draws overlapping inline decorations", () => { let view = tempEditor({doc: doc(p("abcdef")), plugins: [decoPlugin(["3-5-foo", "4-6-bar", "1-7-baz"])]}) let baz = view.dom.querySelectorAll(".baz") ist(baz.length, 5) ist(Array.prototype.map.call(baz, x => x.textContent).join("-"), "ab-c-d-e-f") function classes(n) { return n.className.split(" ").sort().join(" ") } ist(classes(baz[1]), "baz foo") ist(classes(baz[2]), "bar baz foo") ist(classes(baz[3]), "bar baz") }) it("draws multiple widgets", () => { let view = tempEditor({doc: doc(p("foobar")), plugins: [decoPlugin(["1-widget", "4-widget", "7-widget"])]}) let found = view.dom.querySelectorAll("button") ist(found.length, 3) ist(found[0].nextSibling.textContent, "foo") ist(found[1].nextSibling.textContent, "bar") ist(found[2].previousSibling.textContent, "bar") }) it("orders widgets by their side option", () => { let view = tempEditor({doc: doc(p("foobar")), plugins: [decoPlugin([Decoration.widget(4, document.createTextNode("B")), Decoration.widget(4, document.createTextNode("A"), {side: -100}), Decoration.widget(4, document.createTextNode("C"), {side: 2})])]}) ist(view.dom.textContent, "fooABCbar") }) it("draws a widget in an empty node", () => { let view = tempEditor({doc: doc(p()), plugins: [decoPlugin(["1-widget"])]}) ist(view.dom.querySelectorAll("button").length, 1) }) it("draws widgets on node boundaries", () => { let view = tempEditor({doc: doc(p("foo", em("bar"))), plugins: [decoPlugin(["4-widget"])]}) ist(view.dom.querySelectorAll("button").length, 1) }) it("draws decorations from multiple plugins", () => { let view = tempEditor({doc: doc(p("foo", em("bar"))), plugins: [decoPlugin(["2-widget"]), decoPlugin(["6-widget"])]}) ist(view.dom.querySelectorAll("button").length, 2) }) it("calls widget destroy methods", () => { let destroyed = false let view = tempEditor({ doc: doc(p("abc")), plugins: [decoPlugin([Decoration.widget(2, document.createElement("BUTTON"), { destroy: (node) => { destroyed = true ist(node.tagName, "BUTTON") } })])] }) view.dispatch(view.state.tr.delete(1, 4)) ist(destroyed) }) it("draws inline decorations spanning multiple parents", () => { let view = tempEditor({doc: doc(p("long first ", em("p"), "aragraph"), p("two")), plugins: [decoPlugin(["7-25-foo"])]}) let foos = view.dom.querySelectorAll(".foo") ist(foos.length, 4) ist(foos[0].textContent, "irst ") ist(foos[1].textContent, "p") ist(foos[2].textContent, "aragraph") ist(foos[3].textContent, "tw") }) it("draws inline decorations across empty paragraphs", () => { let view = tempEditor({doc: doc(p("first"), p(), p("second")), plugins: [decoPlugin(["3-12-foo"])]}) let foos = view.dom.querySelectorAll(".foo") ist(foos.length, 2) ist(foos[0].textContent, "rst") ist(foos[1].textContent, "se") }) it("can handle inline decorations ending at the start or end of a node", () => { let view = tempEditor({doc: doc(p(), p()), plugins: [decoPlugin(["1-3-foo"])]}) ist(!view.dom.querySelector(".foo")) }) it("can draw decorations with multiple classes", () => { let view = tempEditor({doc: doc(p("foo")), plugins: [decoPlugin(["1-4-foo bar"])]}) ist(view.dom.querySelectorAll(".foo").length, 1) ist(view.dom.querySelectorAll(".bar").length, 1) }) it("supports overlapping inline decorations", () => { let view = tempEditor({doc: doc(p("foobar")), plugins: [decoPlugin(["1-3-foo", "2-5-bar"])]}) let foos = view.dom.querySelectorAll(".foo") let bars = view.dom.querySelectorAll(".bar") ist(foos.length, 2) ist(bars.length, 2) ist(foos[0].textContent, "f") ist(foos[1].textContent, "o") ist(bars[0].textContent, "o") ist(bars[1].textContent, "ob") }) it("doesn't redraw when irrelevant decorations change", () => { let view = tempEditor({doc: doc(p("foo"), p("baz")), plugins: [decoPlugin(["7-8-foo"])]}) let para2 = view.dom.lastChild updateDeco(view, [make("2-3-bar")]) ist(view.dom.lastChild, para2) ist(view.dom.querySelector(".bar")) }) it("doesn't redraw when irrelevant content changes", () => { let view = tempEditor({doc: doc(p("foo"), p("baz")), plugins: [decoPlugin(["7-8-foo"])]}) let para2 = view.dom.lastChild view.dispatch(view.state.tr.delete(2, 3)) view.dispatch(view.state.tr.delete(2, 3)) ist(view.dom.lastChild, para2) }) it("can add a widget on a node boundary", () => { let view = tempEditor({doc: doc(p("foo", em("bar"))), plugins: [decoPlugin([])]}) updateDeco(view, [make("4-widget")]) ist(view.dom.querySelectorAll("button").length, 1) }) it("can remove a widget on a node boundary", () => { let dec = make("4-widget") let view = tempEditor({doc: doc(p("foo", em("bar"))), plugins: [decoPlugin([dec])]}) updateDeco(view, null, [dec]) ist(view.dom.querySelector("button"), null) }) it("can remove the class from a text node", () => { let dec = make("1-4-foo") let view = tempEditor({doc: doc(p("abc")), plugins: [decoPlugin([dec])]}) ist(view.dom.querySelector(".foo")) updateDeco(view, null, [dec]) ist(view.dom.querySelector(".foo"), null) }) it("can remove the class from part of a text node", () => { let dec = make("2-4-foo") let view = tempEditor({doc: doc(p("abcd")), plugins: [decoPlugin([dec])]}) ist(view.dom.querySelector(".foo")) updateDeco(view, null, [dec]) ist(view.dom.querySelector(".foo"), null) ist(view.dom.firstChild.innerHTML, "abcd") }) it("can remove the class for part of a text node", () => { let dec = make("2-4-foo") let view = tempEditor({doc: doc(p("abcd")), plugins: [decoPlugin([dec])]}) ist(view.dom.querySelector(".foo")) updateDeco(view, [make("2-4-bar")], [dec]) ist(view.dom.querySelector(".foo"), null) ist(view.dom.querySelector(".bar")) }) it("draws a widget added in the middle of a text node", () => { let view = tempEditor({doc: doc(p("foo")), plugins: [decoPlugin([])]}) updateDeco(view, [make("3-widget")]) ist(view.dom.firstChild.textContent, "foωo") }) it("can update a text node around a widget", () => { let view = tempEditor({doc: doc(p("bar")), plugins: [decoPlugin(["3-widget"])]}) view.dispatch(view.state.tr.delete(1, 2)) ist(view.dom.querySelectorAll("button").length, 1) ist(view.dom.firstChild.textContent, "aωr") }) it("can update a text node with an inline decoration", () => { let view = tempEditor({doc: doc(p("bar")), plugins: [decoPlugin(["1-3-foo"])]}) view.dispatch(view.state.tr.delete(1, 2)) let foo = view.dom.querySelector(".foo") ist(foo) ist(foo.textContent, "a") ist(foo.nextSibling.textContent, "r") }) it("correctly redraws a partially decorated node when a widget is added", () => { let view = tempEditor({doc: doc(p("one", em("two"))), plugins: [decoPlugin(["1-6-foo"])]}) updateDeco(view, [make("6-widget")]) let foos = view.dom.querySelectorAll(".foo") ist(foos.length, 2) ist(foos[0].textContent, "one") ist(foos[1].textContent, "tw") }) it("correctly redraws when skipping split text node", () => { let view = tempEditor({doc: doc(p("foo")), plugins: [decoPlugin(["3-widget", "3-4-foo"])]}) updateDeco(view, [make("4-widget")]) ist(view.dom.querySelectorAll("button").length, 2) }) it("drops removed node decorations from the view", () => { let deco = Decoration.node(1, 6, {class: "cls"}) let view = tempEditor({doc: doc(blockquote(p("foo"), p("bar"))), plugins: [decoPlugin([deco])]}) updateDeco(view, null, [deco]) ist(!view.dom.querySelector(".cls")) }) it("can update a node's attributes without replacing the node", () => { let deco = Decoration.node(0, 5, {title: "title", class: "foo"}) let view = tempEditor({doc: doc(p("foo")), plugins: [decoPlugin([deco])]}) let para = view.dom.querySelector("p") updateDeco(view, [Decoration.node(0, 5, {class: "foo bar"})], [deco]) ist(view.dom.querySelector("p"), para) ist(para.className, "foo bar") ist(!para.title) }) it("can add and remove CSS custom properties from a node", () => { let deco = Decoration.node(0, 5, {style: '--my-custom-property:36px'}) let view = tempEditor({doc: doc(p("foo")), plugins: [decoPlugin([deco])]}) ist(view.dom.querySelector("p").style.getPropertyValue('--my-custom-property'), "36px") updateDeco(view, null, [deco]) ist(view.dom.querySelector("p").style.getPropertyValue('--my-custom-property'), "") }) it("updates decorated nodes even if a widget is added before them", () => { let view = tempEditor({doc: doc(p("a"), p("b")), plugins: [decoPlugin([])]}) let lastP = view.dom.querySelectorAll("p")[1] updateDeco(view, [make("3-widget"), Decoration.node(3, 6, {style: "color: red"})]) ist(lastP.style.color, "red") }) it("doesn't redraw nodes when a widget before them is replaced", () => { let w0 = make("3-widget") let view = tempEditor({doc: doc(h1("a"), p("b")), plugins: [decoPlugin([w0])]}) let initialP = view.dom.querySelector("p") view.dispatch(view.state.tr.setMeta("updateDecorations", {add: [make("3-widget")], remove: [w0]}) .insertText("c", 5)) ist(view.dom.querySelector("p"), initialP) }) it("can add and remove inline style", () => { let deco = Decoration.inline(1, 6, {style: "color: rgba(0,10,200,.4); text-decoration: underline"}) let view = tempEditor({doc: doc(p("al", img, "lo")), plugins: [decoPlugin([deco])]}) ist(/rgba/.test(view.dom.querySelector("img").style.color)) ist(view.dom.querySelector("img").previousSibling.style.textDecoration, "underline") updateDeco(view, null, [deco]) ist(view.dom.querySelector("img").style.color, "") ist(view.dom.querySelector("img").style.textDecoration, "") }) it("passes decorations to a node view", () => { let current = "" let view = tempEditor({ doc: doc(p("foo"), hr), plugins: [decoPlugin([])], nodeViews: {horizontal_rule: () => ({ update(_, decos) { current = decos.map(d => d.spec.name).join() } })} }) let a = Decoration.node(5, 6, {}, {name: "a"}) updateDeco(view, [a], []) ist(current, "a") updateDeco(view, [Decoration.node(5, 6, {}, {name: "b"}), Decoration.node(5, 6, {}, {name: "c"})], [a]) ist(current, "b,c") }) it("draws the specified marks around a widget", () => { let view = tempEditor({ doc: doc(p("foobar")), plugins: [decoPlugin([Decoration.widget(4, document.createElement("img"), {marks: [schema.mark("em")]})])] }) ist(view.dom.querySelector("em img")) }) it("draws widgets inside the marks for their side", () => { let view = tempEditor({ doc: doc(p(em("foo"), strong("bar"))), plugins: [decoPlugin([Decoration.widget(4, document.createElement("img"), {side: -1})]), decoPlugin([Decoration.widget(4, document.createElement("br"))]), decoPlugin([Decoration.widget(7, document.createElement("span"))], {side: 1})] }) ist(view.dom.querySelector("em img")) ist(!view.dom.querySelector("strong img")) ist(view.dom.querySelector("strong br")) ist(!view.dom.querySelector("em br")) ist(!view.dom.querySelector("strong span")) }) it("draws decorations inside node views", () => { let view = tempEditor({ doc: doc(p("foo")), nodeViews: {paragraph() { let p = document.createElement("p"); return {dom: p, contentDOM: p} }}, plugins: [decoPlugin([Decoration.widget(2, document.createElement("img"))])] }) ist(view.dom.querySelector("img")) }) it("can delay widget drawing to render time", () => { let view = tempEditor({ doc: doc(p("hi")), decorations(state) { return DecorationSet.create(state.doc, [Decoration.widget(3, view => { ist(view.state, state) let elt = document.createElement("span") elt.textContent = "!" return elt })]) } }) ist(view.dom.textContent, "hi!") }) it("supports widgets querying their own position", () => { let get tempEditor({ doc: doc(p("hi")), decorations(state) { return DecorationSet.create(state.doc, [Decoration.widget(3, (_view, getPos) => { ist(getPos(), 3) get = getPos return document.createElement("button") })]) } }) ist(get(), 3) }) it("doesn't redraw widgets with matching keys", () => { let mkButton = () => document.createElement("button") let view = tempEditor({ doc: doc(p("hi")), decorations(state) { return DecorationSet.create(state.doc, [Decoration.widget(2, mkButton, {key: "myButton"})]) } }) let widgetDOM = view.dom.querySelector("button") view.dispatch(view.state.tr.insertText("!", 2, 2)) ist(view.dom.querySelector("button"), widgetDOM) }) it("doesn't redraw widgets with identical specs", () => { let toDOM = () => document.createElement("button") let view = tempEditor({ doc: doc(p("hi")), decorations(state) { return DecorationSet.create(state.doc, [Decoration.widget(2, toDOM, {side: 1})]) } }) let widgetDOM = view.dom.querySelector("button") view.dispatch(view.state.tr.insertText("!", 2, 2)) ist(view.dom.querySelector("button"), widgetDOM) }) it("doesn't get confused by split text nodes", () => { let view = tempEditor({doc: doc(p("abab")), decorations(state) { return state.selection.from <= 1 ? null : DecorationSet.create(view.state.doc, [Decoration.inline(1, 2, {class: "foo"}), Decoration.inline(3, 4, {class: "foo"})]) }}) view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, 5))) ist(view.dom.textContent, "abab") }) it("only draws inline decorations on the innermost level", () => { let s = new Schema({ nodes: { doc: {content: "(text | thing)*"}, text: {}, thing: {inline: true, content: "text*", toDOM: () => ["strong", 0], parseDOM: [{tag: "strong"}]} } }) let view = tempEditor({ doc: s.node("doc", null, [s.text("abc"), s.node("thing", null, [s.text("def")]), s.text("ghi")]), decorations: s => DecorationSet.create(s.doc, [Decoration.inline(1, 10, {class: "dec"})]) }) let styled = view.dom.querySelectorAll(".dec") ist(styled.length, 3) ist(Array.prototype.map.call(styled, n => n.textContent).join(), "bc,def,gh") ist(styled[1].parentNode.nodeName, "STRONG") }) it("can handle nodeName decoration overlapping with classes", () => { let view = tempEditor({ doc: doc(p("one two three")), plugins: [decoPlugin([Decoration.inline(2, 13, {class: "foo"}), Decoration.inline(5, 8, {nodeName: "em"})])] }) ist(view.dom.firstChild.innerHTML, 'one two three') }) }) prosemirror-view-1.23.13/test/test-draw.js000066400000000000000000000153341423202571000204450ustar00rootroot00000000000000const {doc, strong, pre, h1, p, hr, schema} = require("prosemirror-test-builder") const {Plugin} = require("prosemirror-state") const {Schema} = require("prosemirror-model") const {tempEditor} = require("./view") const ist = require("ist") describe("EditorView draw", () => { it("updates the DOM", () => { let view = tempEditor({doc: doc(p("foo"))}) view.dispatch(view.state.tr.insertText("bar")) ist(view.dom.textContent, "barfoo") }) it("doesn't redraw nodes after changes", () => { let view = tempEditor({doc: doc(h1("foo"), p("bar"))}) let oldP = view.dom.querySelector("p") view.dispatch(view.state.tr.insertText("!")) ist(view.dom.querySelector("p"), oldP) }) it("doesn't redraw nodes before changes", () => { let view = tempEditor({doc: doc(p("foo"), h1("bar"))}) let oldP = view.dom.querySelector("p") view.dispatch(view.state.tr.insertText("!", 2)) ist(view.dom.querySelector("p"), oldP) }) it("doesn't redraw nodes between changes", () => { let view = tempEditor({doc: doc(p("foo"), h1("bar"), pre("baz"))}) let oldP = view.dom.querySelector("p") let oldPre = view.dom.querySelector("pre") view.dispatch(view.state.tr.insertText("!", 2)) ist(view.dom.querySelector("p"), oldP) ist(view.dom.querySelector("pre"), oldPre) }) it("doesn't redraw siblings of a split node", () => { let view = tempEditor({doc: doc(p("foo"), h1("bar"), pre("baz"))}) let oldP = view.dom.querySelector("p") let oldPre = view.dom.querySelector("pre") view.dispatch(view.state.tr.split(8)) ist(view.dom.querySelector("p"), oldP) ist(view.dom.querySelector("pre"), oldPre) }) it("doesn't redraw siblings of a joined node", () => { let view = tempEditor({doc: doc(p("foo"), h1("bar"), h1("x"), pre("baz"))}) let oldP = view.dom.querySelector("p") let oldPre = view.dom.querySelector("pre") view.dispatch(view.state.tr.join(10)) ist(view.dom.querySelector("p"), oldP) ist(view.dom.querySelector("pre"), oldPre) }) it("doesn't redraw after a big deletion", () => { let view = tempEditor({doc: doc(p(), p(), p(), p(), p(), p(), p(), p(), h1("!"), p(), p())}) let oldH = view.dom.querySelector("h1") view.dispatch(view.state.tr.delete(2, 14)) ist(view.dom.querySelector("h1"), oldH) }) it("adds classes from the attributes prop", () => { let view = tempEditor({doc: doc(p()), attributes: {class: "foo bar"}}) ist(view.dom.classList.contains("foo")) ist(view.dom.classList.contains("bar")) ist(view.dom.classList.contains("ProseMirror")) view.update({state: view.state, attributes: {class: "baz"}}) ist(!view.dom.classList.contains("foo")) ist(view.dom.classList.contains("baz")) }) it("adds style from the attributes prop", () => { let view = tempEditor({doc: doc(p()), attributes: {style: "border: 1px solid red;"}, plugins: [new Plugin({props: { attributes: {style: "background: red;"}}}), new Plugin({props: { attributes: {style: "color: red;"}}})]}) ist(view.dom.style.border, "1px solid red") ist(view.dom.style.backgroundColor, "red") ist(view.dom.style.color, "red") }) it("can set other attributes", () => { let view = tempEditor({doc: doc(p()), attributes: {spellcheck: "false", "aria-label": "hello"}}) ist(view.dom.spellcheck, false) ist(view.dom.getAttribute("aria-label"), "hello") view.update({state: view.state, attributes: {style: "background-color: yellow"}}) ist(view.dom.hasAttribute("aria-label"), false) ist(view.dom.style.backgroundColor, "yellow") }) it("can't set the contenteditable attribute", () => { let view = tempEditor({doc: doc(p()), attributes: {contenteditable: "false"}}) ist(view.dom.contentEditable, "true") }) it("understands the editable prop", () => { let view = tempEditor({doc: doc(p()), editable: () => false}) ist(view.dom.contentEditable, "false") view.update({state: view.state}) ist(view.dom.contentEditable, "true") }) it("doesn't redraw following paragraphs when a paragraph is split", () => { let view = tempEditor({doc: doc(p("abcde"), p("fg"))}) let lastPara = view.dom.lastChild view.dispatch(view.state.tr.split(3)) ist(view.dom.lastChild, lastPara) }) it("doesn't greedily match nodes that have another match", () => { let view = tempEditor({doc: doc(p("a"), p("b"), p())}) let secondPara = view.dom.querySelectorAll("p")[1] view.dispatch(view.state.tr.split(2)) ist(view.dom.querySelectorAll("p")[2], secondPara) }) it("creates and destroys plugin views", () => { let events = [] class PluginView { update() { events.push("update") } destroy() { events.push("destroy") } } let plugin = new Plugin({ view() { events.push("create"); return new PluginView } }) let view = tempEditor({plugins: [plugin]}) view.dispatch(view.state.tr.insertText("u")) view.destroy() ist(events.join(" "), "create update destroy") }) it("redraws changed node views", () => { let view = tempEditor({doc: doc(p("foo"), hr)}) ist(view.dom.querySelector("hr")) view.setProps({nodeViews: {horizontal_rule: () => { return {dom: document.createElement("var")} }}}) ist(!view.dom.querySelector("hr")) ist(view.dom.querySelector("var")) }) it("doesn't get confused by merged nodes", () => { let view = tempEditor({doc: doc(p(strong("one"), " two ", strong("three")))}) view.dispatch(view.state.tr.removeMark(1, 4, schema.marks.strong)) ist(view.dom.querySelectorAll("strong").length, 1) }) it("doesn't redraw too much when marks are present", () => { let s = new Schema({ nodes: { doc: {content: "paragraph+", marks: "m"}, text: {group: "inline"}, paragraph: schema.spec.nodes.get("paragraph") }, marks: { m: { toDOM: () => ["div", {class: "m"}, 0], parseDOM: [{tag: "div.m"}] } } }) let paragraphs = [] for (let i = 1; i <= 10; i++) paragraphs.push(s.node("paragraph", null, [s.text("para " + i)], [s.mark("m")])) let view = tempEditor({ doc: s.node("doc", null, paragraphs), }) let initialChildren = Array.from(view.dom.querySelectorAll("p")) let newParagraphs = [] for (let i = -6; i < 0; i++) newParagraphs.push(s.node("paragraph", null, [s.text("para " + i)], [s.mark("m")])) view.dispatch(view.state.tr.replaceWith(0, 8, newParagraphs)) let currentChildren = Array.from(view.dom.querySelectorAll("p")), sameAtEnd = 0 while (sameAtEnd < currentChildren.length && sameAtEnd < initialChildren.length && currentChildren[currentChildren.length - sameAtEnd - 1] == initialChildren[initialChildren.length - sameAtEnd - 1]) sameAtEnd++ ist(sameAtEnd, 9) }) }) prosemirror-view-1.23.13/test/test-endOfTextblock.js000066400000000000000000000112001423202571000224070ustar00rootroot00000000000000const {schema, doc, p, a} = require("prosemirror-test-builder") const ist = require("ist") const {tempEditor} = require("./view") const {Decoration, DecorationSet} = require("..") const {TextSelection} = require("prosemirror-state") describe("EditorView.endOfTextblock", () => { it("works at the left side of a textblock", () => { let view = tempEditor({doc: doc(p("a"), p("bcd"))}) ist(view.endOfTextblock("left")) ist(view.endOfTextblock("backward")) ist(!view.endOfTextblock("right")) ist(!view.endOfTextblock("forward")) }) it("works at the right side of a textblock", () => { let view = tempEditor({doc: doc(p("abc"), p("d"))}) ist(!view.endOfTextblock("left")) ist(!view.endOfTextblock("backward")) ist(view.endOfTextblock("right")) ist(view.endOfTextblock("forward")) }) it("works in the middle of a textblock", () => { let view = tempEditor({doc: doc(p("ab"))}) ist(!view.endOfTextblock("left")) ist(!view.endOfTextblock("backward")) ist(!view.endOfTextblock("right")) ist(!view.endOfTextblock("forward")) }) it("works at the start of the document", () => { let view = tempEditor({doc: doc(p("bcd"))}) ist(view.endOfTextblock("left")) ist(view.endOfTextblock("backward")) ist(!view.endOfTextblock("right")) ist(!view.endOfTextblock("forward")) }) it("works at the end of the document", () => { let view = tempEditor({doc: doc(p("abc"))}) ist(!view.endOfTextblock("left")) ist(!view.endOfTextblock("backward")) ist(view.endOfTextblock("right")) ist(view.endOfTextblock("forward")) }) it("works for vertical motion in a one-line block", () => { let view = tempEditor({doc: doc(p("abc"))}) ist(view.endOfTextblock("up")) ist(view.endOfTextblock("down")) }) it("works for vertical motion at the end of a wrapped block", () => { let view = tempEditor({doc: doc(p(new Array(100).join("foo ") + "foo"))}) ist(!view.endOfTextblock("up")) ist(view.endOfTextblock("down")) }) it("works for vertical motion at the start of a wrapped block", () => { let view = tempEditor({doc: doc(p("foo " + new Array(100).join("foo ")))}) ist(view.endOfTextblock("up")) ist(!view.endOfTextblock("down")) }) it("works for virtual motion when in a mark", () => { let view = tempEditor({doc: doc(p(a("foo "), new Array(50).join("foo "), a("foo "), new Array(50).join("foo "), a("foo")))}) ist(view.endOfTextblock("up")) ist(!view.endOfTextblock("down")) view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, view.state.doc.tag.b))) ist(!view.endOfTextblock("up")) ist(!view.endOfTextblock("down")) view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, view.state.doc.tag.c))) ist(!view.endOfTextblock("up")) ist(view.endOfTextblock("down")) }) // Bidi functionality only works when the browser has Selection.modify if (!getSelection().modify) return it("works at the start of an RTL block", () => { let view = tempEditor({doc: doc(p("مرآة"))}) view.dom.style.direction = "rtl" ist(!view.endOfTextblock("left")) ist(view.endOfTextblock("backward")) ist(view.endOfTextblock("right")) ist(!view.endOfTextblock("forward")) }) it("works at the end of an RTL block", () => { let view = tempEditor({doc: doc(p("مرآة"))}) view.dom.style.direction = "rtl" ist(view.endOfTextblock("left")) ist(!view.endOfTextblock("backward")) ist(!view.endOfTextblock("right")) ist(view.endOfTextblock("forward")) }) it("works inside an RTL block", () => { let view = tempEditor({doc: doc(p("مرآة"))}) view.dom.style.direction = "rtl" ist(!view.endOfTextblock("left")) ist(!view.endOfTextblock("backward")) ist(!view.endOfTextblock("right")) ist(!view.endOfTextblock("forward")) }) it("works in a bidirectional block", () => { let view = tempEditor({doc: doc(p("proseمرآة"))}) ist(!view.endOfTextblock("left")) ist(!view.endOfTextblock("backward")) ist(view.endOfTextblock("forward")) }) it("works in a cursor wrapper", () => { let view = tempEditor({doc: doc(p("foo"))}) view.dispatch(view.state.tr.setStoredMarks([schema.marks.em.create()])) ist(!view.endOfTextblock("backward")) }) it("works after a widget", () => { let d = doc(p("foo")), w = document.createElement("span") w.textContent = "!" let view = tempEditor({doc: d, decorations() { return DecorationSet.create(d, [Decoration.widget(3, w)]) }}) ist(!view.endOfTextblock("backward")) }) }) prosemirror-view-1.23.13/test/test-nodeview.js000066400000000000000000000127521423202571000213310ustar00rootroot00000000000000const {doc, p, br, blockquote} = require("prosemirror-test-builder") const {Plugin} = require("prosemirror-state") const {tempEditor} = require("./view") const {DecorationSet, Decoration} = require("..") const ist = require("ist") describe("nodeViews prop", () => { it("can replace a node's representation", () => { let view = tempEditor({doc: doc(p("foo", br)), nodeViews: {hard_break() { return {dom: document.createElement("var")}}}}) ist(view.dom.querySelector("var")) }) it("can override drawing of a node's content", () => { let view = tempEditor({ doc: doc(p("foo")), nodeViews: {paragraph(node) { let dom = document.createElement("p") dom.textContent = node.textContent.toUpperCase() return {dom} }} }) ist(view.dom.querySelector("p").textContent, "FOO") view.dispatch(view.state.tr.insertText("a")) ist(view.dom.querySelector("p").textContent, "AFOO") }) it("can register its own update method", () => { let view = tempEditor({ doc: doc(p("foo")), nodeViews: {paragraph(node) { let dom = document.createElement("p") dom.textContent = node.textContent.toUpperCase() return {dom, update(node) { dom.textContent = node.textContent.toUpperCase(); return true }} }} }) let para = view.dom.querySelector("p") view.dispatch(view.state.tr.insertText("a")) ist(view.dom.querySelector("p"), para) ist(para.textContent, "AFOO") }) it("allows decoration updates for node views with an update method", () => { let view = tempEditor({ doc: doc(p("foo")), nodeViews: {paragraph(node) { let dom = document.createElement("p") return {dom, contentDOM: dom, update(node_) { return node.sameMarkup(node_) }} }} }) view.setProps({ decorations(state) { return DecorationSet.create(state.doc, [ Decoration.inline(2, 3, {someattr: "ok"}), Decoration.node(0, 5, {otherattr: "ok"}) ]) } }) ist(view.dom.querySelector("[someattr]")) ist(view.dom.querySelector("[otherattr]")) }) it("can provide a contentDOM property", () => { let view = tempEditor({ doc: doc(p("foo")), nodeViews: {paragraph() { let dom = document.createElement("p") return {dom, contentDOM: dom} }} }) let para = view.dom.querySelector("p") view.dispatch(view.state.tr.insertText("a")) ist(view.dom.querySelector("p"), para) ist(para.textContent, "afoo") }) it("has its destroy method called", () => { let destroyed = false, view = tempEditor({ doc: doc(p("foo", br)), nodeViews: {hard_break() { return {destroy: () => destroyed = true}}} }) ist(!destroyed) view.dispatch(view.state.tr.delete(3, 5)) ist(destroyed) }) it("can query its own position", () => { let get, view = tempEditor({ doc: doc(blockquote(p("abc"), p("foo", br))), nodeViews: {hard_break(_n, _v, getPos) { ist(getPos(), 10) get = getPos return {} }} }) ist(get(), 10) view.dispatch(view.state.tr.insertText("a")) ist(get(), 11) }) it("has access to outer decorations", () => { let plugin = new Plugin({ state: { init() { return null }, apply(tr, prev) { return tr.getMeta("setDeco") || prev } }, props: { decorations(state) { let deco = this.getState(state) return deco && DecorationSet.create(state.doc, [Decoration.inline(0, state.doc.content.size, null, {name: deco})]) } } }) let view = tempEditor({ doc: doc(p("foo", br)), plugins: [plugin], nodeViews: {hard_break(_n, _v, _p, deco) { let dom = document.createElement("var") function update(deco) { dom.textContent = deco.length ? deco[0].spec.name : "[]" } update(deco) return {dom, update(_, deco) { update(deco); return true }} }} }) ist(view.dom.querySelector("var").textContent, "[]") view.dispatch(view.state.tr.setMeta("setDeco", "foo")) ist(view.dom.querySelector("var").textContent, "foo") view.dispatch(view.state.tr.setMeta("setDeco", "bar")) ist(view.dom.querySelector("var").textContent, "bar") }) it("provides access to inner decorations in the constructor", () => { tempEditor({ doc: doc(p("foo")), nodeViews: {paragraph(_node, _v, _pos, _outer, innerDeco) { let dom = document.createElement("p") ist(innerDeco.find().map(d => `${d.from}-${d.to}`).join(), "1-2") return {dom, contentDOM: dom} }}, decorations(state) { return DecorationSet.create(state.doc, [ Decoration.inline(2, 3, {someattr: "ok"}), Decoration.node(0, 5, {otherattr: "ok"}) ]) } }) }) it("provides access to inner decorations in the update method", () => { let innerDecos = [] let view = tempEditor({ doc: doc(p("foo")), nodeViews: {paragraph(node) { let dom = document.createElement("p") return {dom, contentDOM: dom, update(node_, _, innerDecoSet) { innerDecos = innerDecoSet.find().map(d => `${d.from}-${d.to}`) return node.sameMarkup(node_) }} }} }) view.setProps({ decorations(state) { return DecorationSet.create(state.doc, [ Decoration.inline(2, 3, {someattr: "ok"}), Decoration.node(0, 5, {otherattr: "ok"}) ]) } }) ist(innerDecos.join(), "1-2") }) }) prosemirror-view-1.23.13/test/test-selection.js000066400000000000000000000274101423202571000214730ustar00rootroot00000000000000const {doc, blockquote, p, em, img: img_, strong, code, code_block, br, hr, ul, li} = require("prosemirror-test-builder") const ist = require("ist") const {Selection, NodeSelection} = require("prosemirror-state") const {tempEditor, findTextNode} = require("./view") const {Decoration, DecorationSet} = require("..") const img = img_({src: "data:image/gif;base64,R0lGODlhBQAFAIABAAAAAP///yH5BAEKAAEALAAAAAAFAAUAAAIEjI+pWAA7"}) function allPositions(doc) { let found = [] function scan(node, start) { if (node.isTextblock) { for (let i = 0; i <= node.content.size; i++) found.push(start + i) } else { node.forEach((child, offset) => scan(child, start + offset + 1)) } } scan(doc, 0) return found } function setDOMSel(node, offset) { let range = document.createRange() range.setEnd(node, offset) range.setStart(node, offset) let sel = window.getSelection() sel.removeAllRanges() sel.addRange(range) } function getSel() { let sel = window.getSelection() let node = sel.focusNode, offset = sel.focusOffset while (node && node.nodeType != 3) { let after = offset < node.childNodes.length && node.childNodes[offset] let before = offset > 0 && node.childNodes[offset - 1] if (after) { node = after; offset = 0 } else if (before) { node = before; offset = node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length } else break } return {node: node, offset: offset} } function setSel(view, sel) { if (typeof sel == "number") sel = Selection.near(view.state.doc.resolve(sel)) view.dispatch(view.state.tr.setSelection(sel)) } function event(code) { let event = document.createEvent("Event") event.initEvent("keydown", true, true) event.keyCode = code return event } const LEFT = 37, RIGHT = 39, UP = 38, DOWN = 40 if (!document.hasFocus()) console["warn"]("Document doesn't have focus. Skipping some tests.") describe("EditorView", () => { it("can read the DOM selection", () => { // disabled when the document doesn't have focus, since that causes this to fail if (!document.hasFocus()) return let view = tempEditor({doc: doc(p("one"), hr, blockquote(p("two")))}) function test(node, offset, expected) { setDOMSel(node, offset) view.dom.focus() view.domObserver.flush() let sel = view.state.selection ist(sel.head == null ? sel.from : sel.head, expected) } let one = findTextNode(view.dom, "one") let two = findTextNode(view.dom, "two") test(one, 0, 1) test(one, 1, 2) test(one, 3, 4) test(one.parentNode, 0, 1) test(one.parentNode, 1, 4) test(two, 0, 8) test(two, 3, 11) test(two.parentNode, 1, 11) test(view.dom, 1, 4) test(view.dom, 2, 8) test(view.dom, 3, 11) }) it("syncs the DOM selection with the editor selection", () => { // disabled when the document doesn't have focus, since that causes this to fail if (!document.hasFocus()) return let view = tempEditor({doc: doc(p("one"), hr, blockquote(p("two")))}) function test(pos, node, offset) { setSel(view, pos) let sel = getSel() ist(sel.node, node) ist(sel.offset, offset) } let one = findTextNode(view.dom, "one") let two = findTextNode(view.dom, "two") view.focus() test(1, one, 0) test(2, one, 1) test(4, one, 3) test(8, two, 0) test(10, two, 2) }) it("returns sensible screen coordinates", () => { let view = tempEditor({doc: doc(p("one"), p("two"))}) let p00 = view.coordsAtPos(1) let p01 = view.coordsAtPos(2) let p03 = view.coordsAtPos(4) let p10 = view.coordsAtPos(6) let p13 = view.coordsAtPos(9) ist(p00.bottom, p00.top, ">") ist(p13.bottom, p13.top, ">") ist(p00.top, p01.top) ist(p01.top, p03.top) ist(p00.bottom, p03.bottom) ist(p10.top, p13.top) ist(p01.left, p00.left, ">") ist(p03.left, p01.left, ">") ist(p10.top, p00.top, ">") ist(p13.left, p10.left, ">") }) it("returns proper coordinates in code blocks", () => { let view = tempEditor({doc: doc(code_block("a\nb\n"))}), p = [] for (let i = 1; i <= 5; i++) p.push(view.coordsAtPos(i)) let [p0, p1, p2, p3, p4] = p ist(p0.top, p1.top) ist(p0.left, p1.left, "<") ist(p2.top, p1.top, ">") ist(p2.top, p3.top) ist(p2.left, p3.left, "<") ist(p2.left, p0.left) ist(p4.top, p3.top, ">") // This one shows a small (0.01 pixel) difference in Firefox for // some reason. ist(Math.round(p4.left), Math.round(p2.left)) }) it("produces sensible screen coordinates in corner cases", () => { let view = tempEditor({doc: doc(p("one", em("two", strong("three"), img), br, code("foo")), p())}) return new Promise(ok => { setTimeout(() => { allPositions(view.state.doc).forEach(pos => { let coords = view.coordsAtPos(pos) let found = view.posAtCoords({top: coords.top + 1, left: coords.left}).pos ist(found, pos) setSel(view, pos) }) ok() }, 20) }) }) it("doesn't return zero-height rectangles after leaves", () => { let view = tempEditor({doc: doc(p(img))}) let coords = view.coordsAtPos(2, 1) ist(coords.bottom - coords.top, 5, ">") }) it("produces horizontal rectangles for positions between blocks", () => { let view = tempEditor({doc: doc(p("ha"), hr, blockquote(p("ba")))}) let a = view.coordsAtPos(0) ist(a.top, a.bottom) ist(a.top, view.dom.firstChild.getBoundingClientRect().top) ist(a.left, a.right, "<") let b = view.coordsAtPos(4) ist(b.top, b.bottom) ist(b.top, a.top, ">") ist(b.left, b.right, "<") let c = view.coordsAtPos(5) ist(c.top, c.bottom) ist(c.top, b.top, ">") let d = view.coordsAtPos(6) ist(d.top, d.bottom) ist(d.left, d.right, "<") ist(d.top, view.dom.getBoundingClientRect().bottom, "<") }) it("produces sensible screen coordinates around line breaks", () => { let view = tempEditor({doc: doc(p("one two three four five-six-seven-eight"))}) view.dom.style.width = "4em" let prevBefore, prevAfter allPositions(view.state.doc).forEach(pos => { let coords = view.coordsAtPos(pos, 1) if (prevAfter) ist(prevAfter.top < coords.top || prevAfter.top == coords.top && prevAfter.left < coords.left) prevAfter = coords let found = view.posAtCoords({top: coords.top + 1, left: coords.left}).pos ist(found, pos) let coordsBefore = view.coordsAtPos(pos, -1) if (prevBefore) ist(prevBefore.top < coordsBefore.top || prevBefore.top == coordsBefore.top && prevBefore.left < coordsBefore.left) prevBefore = coordsBefore }) }) it("can find coordinates on node boundaries", () => { let view = tempEditor({doc: doc(p("one ", em("two"), " ", em(strong("three"))))}) let prev allPositions(view.state.doc).forEach(pos => { let coords = view.coordsAtPos(pos, 1) if (prev) ist(prev.top < coords.top || Math.abs(prev.top - coords.top) < 4 && prev.left < coords.left) prev = coords }) }) it("finds proper coordinates in RTL text", () => { let view = tempEditor({doc: doc(p("مرآة نثرية"))}) view.dom.style.direction = "rtl" let prev allPositions(view.state.doc).forEach(pos => { let coords = view.coordsAtPos(pos, 1) if (prev) ist(prev.top < coords.top || Math.abs(prev.top - coords.top) < 4 && prev.left > coords.left) prev = coords }) }) it("can go back and forth between screen coordsa and document positions", () => { let view = tempEditor({doc: doc(p("one"), blockquote(p("two"), p("three")))}) ;[1, 2, 4, 7, 14, 15].forEach(pos => { let coords = view.coordsAtPos(pos) let found = view.posAtCoords({top: coords.top + 1, left: coords.left}).pos ist(found, pos) }) }) it("returns correct screen coordinates for wrapped lines", () => { let view = tempEditor({}) let top = view.coordsAtPos(1), pos = 1, end for (let i = 0; i < 100; i++) { view.dispatch(view.state.tr.insertText("abc def ghi ")) pos += 12 end = view.coordsAtPos(pos) if (end.bottom > top.bottom + 4) break } ist(view.posAtCoords({left: end.left + 50, top: end.top + 5}).pos, pos) }) it("makes arrow motion go through selectable inline nodes", () => { let view = tempEditor({doc: doc(p("foo", img, "bar"))}) view.dispatchEvent(event(RIGHT)) ist(view.state.selection.from, 4) view.dispatchEvent(event(RIGHT)) ist(view.state.selection.head, 5) ist(view.state.selection.anchor, 5) view.dispatchEvent(event(LEFT)) ist(view.state.selection.from, 4) view.dispatchEvent(event(LEFT)) ist(view.state.selection.head, 4) ist(view.state.selection.anchor, 4) }) it("makes arrow motion go through selectable block nodes", () => { let view = tempEditor({doc: doc(p("hello"), hr, ul(li(p("there"))))}) view.dispatchEvent(event(DOWN)) ist(view.state.selection.from, 7) setSel(view, 11) view.dispatchEvent(event(UP)) ist(view.state.selection.from, 7) }) it("supports arrow motion through adjacent blocks", () => { let view = tempEditor({doc: doc(blockquote(p("hello")), hr, hr, p("there"))}) view.dispatchEvent(event(DOWN)) ist(view.state.selection.from, 9) view.dispatchEvent(event(DOWN)) ist(view.state.selection.from, 10) setSel(view, 14) view.dispatchEvent(event(UP)) ist(view.state.selection.from, 10) view.dispatchEvent(event(UP)) ist(view.state.selection.from, 9) }) it("support horizontal motion through blocks", () => { let view = tempEditor({doc: doc(p("foo"), hr, hr, p("bar"))}) view.dispatchEvent(event(RIGHT)) ist(view.state.selection.from, 5) view.dispatchEvent(event(RIGHT)) ist(view.state.selection.from, 6) view.dispatchEvent(event(RIGHT)) ist(view.state.selection.head, 8) view.dispatchEvent(event(LEFT)) ist(view.state.selection.from, 6) view.dispatchEvent(event(LEFT)) ist(view.state.selection.from, 5) view.dispatchEvent(event(LEFT)) ist(view.state.selection.head, 4) }) it("allows moving directly from an inline node to a block node", () => { let view = tempEditor({doc: doc(p("foo", img), hr, p(img, "bar"))}) setSel(view, NodeSelection.create(view.state.doc, 4)) view.dispatchEvent(event(DOWN)) ist(view.state.selection.from, 6) setSel(view, NodeSelection.create(view.state.doc, 8)) view.dispatchEvent(event(UP)) ist(view.state.selection.from, 6) }) it("updates the selection even if the DOM parameters look unchanged", () => { if (!document.hasFocus()) return let view = tempEditor({doc: doc(p("foobar"))}) view.focus() let decos = DecorationSet.create(view.state.doc, [Decoration.inline(1, 4, {color: "green"})]) view.setProps({decorations() { return decos }}) view.setProps({decorations: null}) view.setProps({decorations() { return decos }}) let range = document.createRange() range.setEnd(document.getSelection().anchorNode, document.getSelection().anchorOffset) range.setStart(view.dom, 0) ist(range.toString(), "foobar") }) it("sets selection even if Selection.extend throws DOMException", () => { let originalExtend = window.Selection.prototype.extend window.Selection.prototype.extend = () => { // declare global: DOMException throw new DOMException("failed") } try { let view = tempEditor({doc: doc(p("foo", img), hr, p(img, "bar"))}) setSel(view, NodeSelection.create(view.state.doc, 4)) view.dispatchEvent(event(DOWN)) ist(view.state.selection.from, 6) } finally { window.Selection.prototype.extend = originalExtend } }) it("doesn't put the cursor after BR hack nodes", () => { if (!document.hasFocus()) return let view = tempEditor({doc: doc(p())}) view.focus() ist(getSelection().focusOffset, 0) }) }) prosemirror-view-1.23.13/test/test-view.js000066400000000000000000000076111423202571000204610ustar00rootroot00000000000000const {schema, doc, ul, li, p, strong, em, hr} = require("prosemirror-test-builder") const {EditorState} = require("prosemirror-state") const {Schema} = require("prosemirror-model") const {EditorView} = require("..") const {tempEditor} = require("./view") const ist = require("ist") let space = document.querySelector("#workspace") describe("EditorView", () => { it("can mount an existing node", () => { let dom = space.appendChild(document.createElement("article")) let view = new EditorView({mount: dom}, { state: EditorState.create({doc: doc(p("hi"))}) }) ist(view.dom, dom) ist(dom.contentEditable, "true") ist(dom.firstChild.nodeName, "P") view.destroy() ist(dom.contentEditable, "inherit") space.removeChild(dom) }) it("reflects the current state in .props", () => { let view = tempEditor() ist(view.state, view.props.state) view.dispatch(view.state.tr.insertText("x")) ist(view.state, view.props.state) }) it("can update props with setProp", () => { let view = tempEditor({scrollThreshold: 100}) view.setProps({ scrollThreshold: null, scrollMargin: 10, state: view.state.apply(view.state.tr.insertText("y")) }) ist(view.state.doc.content.size, 3) ist(view.props.scrollThreshold, null) ist(view.props.scrollMargin, 10) }) it("can update with a state using a different schema", () => { let testSchema = new Schema({nodes: schema.spec.nodes}) let view = tempEditor({doc: doc(p(strong("foo")))}) view.updateState(EditorState.create({doc: testSchema.nodes.doc.createAndFill()})) ist(!view.dom.querySelector("strong")) }) it("calls handleScrollToSelection when appropriate", () => { let called = 0 let view = tempEditor({doc: doc(p("foo")), handleScrollToSelection() { called++; return false }}) view.dispatch(view.state.tr.scrollIntoView()) ist(called, 1) }) it("can be queried for the DOM position at a doc position", () => { let view = tempEditor({doc: doc(ul(li(p(strong("foo")))))}) let inText = view.domAtPos(4) ist(inText.offset, 1) ist(inText.node.nodeValue, "foo") let beforeLI = view.domAtPos(1) ist(beforeLI.offset, 0) ist(beforeLI.node.nodeName, "UL") let afterP = view.domAtPos(7) ist(afterP.offset, 1) ist(afterP.node.nodeName, "LI") }) it("can bias DOM position queries to enter nodes", () => { let view = tempEditor({doc: doc(p(em(strong("a"), "b"), "c"))}) let get = (pos, bias) => { let r = view.domAtPos(pos, bias) return (r.node.nodeType == 1 ? r.node.nodeName : r.node.nodeValue) + "@" + r.offset } ist(get(1, 0), "P@0") ist(get(1, -1), "P@0") ist(get(1, 1), "a@0") ist(get(2, -1), "a@1") ist(get(2, 0), "EM@1") ist(get(2, 1), "b@0") ist(get(3, -1), "b@1") ist(get(3, 0), "P@1") ist(get(3, 1), "c@0") ist(get(4, -1), "c@1") ist(get(4, 0), "P@2") ist(get(4, 1), "P@2") }) it("can be queried for a node's DOM representation", () => { let view = tempEditor({doc: doc(p("foo"), hr)}) ist(view.nodeDOM(0).nodeName, "P") ist(view.nodeDOM(5).nodeName, "HR") ist(view.nodeDOM(3), null) }) it("can map DOM positions to doc positions", () => { let view = tempEditor({doc: doc(p("foo"), hr)}) ist(view.posAtDOM(view.dom.firstChild.firstChild, 2), 3) ist(view.posAtDOM(view.dom, 1), 5) ist(view.posAtDOM(view.dom, 2), 6) ist(view.posAtDOM(view.dom.lastChild, 0, -1), 5) ist(view.posAtDOM(view.dom.lastChild, 0, 1), 6) }) it("binds this to itself in dispatchTransaction prop", () => { const dom = document.createElement("div") let thisBinding let view = new EditorView(dom, { state: EditorState.create({doc: doc(p("hi"))}), dispatchTransaction: function() { thisBinding = this } }) view.dispatch(view.state.tr.insertText("x")) ist(view, thisBinding) }) }) prosemirror-view-1.23.13/test/test.js000066400000000000000000000005551423202571000175110ustar00rootroot00000000000000require("mocha/mocha") // declare global: mocha mocha.setup("bdd") require("./test-view") require("./test-draw") require("./test-selection") require("./test-domchange") require("./test-composition") require("./test-decoration") require("./test-draw-decoration") require("./test-nodeview") require("./test-clipboard") require("./test-endOfTextblock") mocha.run() prosemirror-view-1.23.13/test/view.js000066400000000000000000000030561423202571000175030ustar00rootroot00000000000000const {EditorView} = require("..") const {EditorState, Selection, TextSelection, NodeSelection} = require("prosemirror-state") const {schema} = require("prosemirror-test-builder") function selFor(doc) { let a = doc.tag.a if (a != null) { let $a = doc.resolve(a) if ($a.parent.inlineContent) return new TextSelection($a, doc.tag.b != null ? doc.resolve(doc.tag.b) : undefined) else return new NodeSelection($a) } return Selection.atStart(doc) } let tempView = null function tempEditor(inProps) { let space = document.querySelector("#workspace") if (tempView) { tempView.destroy() tempView = null } let props = {} for (let n in inProps) if (n != "plugins") props[n] = inProps[n] props.state = EditorState.create({doc: props.doc, schema, selection: props.doc && selFor(props.doc), plugins: inProps && inProps.plugins}) return tempView = new EditorView(space, props) } exports.tempEditor = tempEditor function findTextNode(node, text) { if (node.nodeType == 3) { if (node.nodeValue == text) return node } else if (node.nodeType == 1) { for (let ch = node.firstChild; ch; ch = ch.nextSibling) { let found = findTextNode(ch, text) if (found) return found } } } exports.findTextNode = findTextNode function requireFocus(pm) { if (!document.hasFocus()) throw new Error("The document doesn't have focus, which is needed for this test") pm.focus() return pm } exports.requireFocus = requireFocus