diff --git a/.gitignore b/.gitignore index 2e4e8d35..5c496db9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ # Compiled code -/dist/* +# /dist/* # We need to check in the package.json for /dist/cjs so that # consumers know to treat it as cjs -!/dist/cjs -/dist/cjs/* -!/dist/cjs/package.json +# !/dist/cjs +# /dist/cjs/* +# !/dist/cjs/package.json # Yarn PnP artifacts .pnp.* diff --git a/dist/cjs/browser.js b/dist/cjs/browser.js new file mode 100644 index 00000000..b28da86d --- /dev/null +++ b/dist/cjs/browser.js @@ -0,0 +1,51 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "browser", { + enumerable: true, + get: ()=>browser +}); +const nav = typeof navigator != "undefined" ? navigator : null; +const doc = typeof document != "undefined" ? document : null; +const agent = nav && nav.userAgent || ""; +const ie_edge = /Edge\/(\d+)/.exec(agent); +const ie_upto10 = /MSIE \d/.exec(agent); +const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent); +const ie = !!(ie_upto10 || ie_11up || ie_edge); +const ie_version = ie_upto10 ? document.documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0; +const gecko = !ie && /gecko\/(\d+)/i.test(agent); +const gecko_version = gecko && +(/Firefox\/(\d+)/.exec(agent) || [ + 0, + 0 +])[1]; +const _chrome = !ie && /Chrome\/(\d+)/.exec(agent); +const chrome = !!_chrome; +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +const chrome_version = _chrome ? +_chrome[1] : 0; +const safari = !ie && !!nav && /Apple Computer/.test(nav.vendor); +// Is true for both iOS and iPadOS for convenience +const ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2); +const mac = ios || (nav ? /Mac/.test(nav.platform) : false); +const windows = nav ? /Win/.test(nav.platform) : false; +const android = /Android \d/.test(agent); +const webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style; +const webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [ + 0, + 0 +])[1] : 0; +const browser = { + ie, + ie_version, + gecko, + gecko_version, + chrome, + chrome_version, + safari, + ios, + mac, + windows, + android, + webkit, + webkit_version +}; diff --git a/dist/cjs/components/ChildNodeViews.js b/dist/cjs/components/ChildNodeViews.js new file mode 100644 index 00000000..e770696d --- /dev/null +++ b/dist/cjs/components/ChildNodeViews.js @@ -0,0 +1,344 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + wrapInDeco: ()=>wrapInDeco, + ChildNodeViews: ()=>ChildNodeViews +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _editorContextJs = require("../contexts/EditorContext.js"); +const _iterDecoJs = require("../decorations/iterDeco.js"); +const _useReactKeysJs = require("../hooks/useReactKeys.js"); +const _propsJs = require("../props.js"); +const _markViewJs = require("./MarkView.js"); +const _nativeWidgetViewJs = require("./NativeWidgetView.js"); +const _nodeViewJs = require("./NodeView.js"); +const _separatorHackViewJs = require("./SeparatorHackView.js"); +const _textNodeViewJs = require("./TextNodeView.js"); +const _trailingHackViewJs = require("./TrailingHackView.js"); +const _widgetViewJs = require("./WidgetView.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +function wrapInDeco(reactNode, deco) { + const { nodeName , ...attrs } = deco.type.attrs; + const props = (0, _propsJs.htmlAttrsToReactProps)(attrs); + // We auto-wrap text nodes in spans so that we can apply attributes + // and styles, but we want to avoid double-wrapping the same + // text node + if (nodeName || typeof reactNode === "string") { + return /*#__PURE__*/ (0, _react.createElement)(nodeName ?? "span", props, reactNode); + } + return /*#__PURE__*/ (0, _react.cloneElement)(reactNode, (0, _propsJs.mergeReactProps)(reactNode.props, props)); +} +const ChildView = /*#__PURE__*/ (0, _react.memo)(function ChildView(param) { + let { child , getInnerPos } = param; + const { view } = (0, _react.useContext)(_editorContextJs.EditorContext); + const getChildPos = (0, _react.useRef)(()=>getInnerPos.current() + child.offset); + getChildPos.current = ()=>getInnerPos.current() + child.offset; + const reactKeys = (0, _useReactKeysJs.useReactKeys)(); + const key = createKey(getInnerPos.current(), child, reactKeys?.posToKey); + return child.type === "widget" ? /*#__PURE__*/ _react.default.createElement(_widgetViewJs.WidgetView, { + key: key, + widget: child.widget, + getPos: getChildPos + }) : child.type === "native-widget" ? /*#__PURE__*/ _react.default.createElement(_nativeWidgetViewJs.NativeWidgetView, { + key: key, + widget: child.widget, + getPos: getChildPos + }) : child.node.isText ? /*#__PURE__*/ _react.default.createElement(_childDescriptorsContextJs.ChildDescriptorsContext.Consumer, { + key: key + }, (param)=>/*#__PURE__*/ { + let { siblingsRef , parentRef } = param; + return _react.default.createElement(_textNodeViewJs.TextNodeView, { + view: view, + node: child.node, + getPos: getChildPos, + siblingsRef: siblingsRef, + parentRef: parentRef, + decorations: child.outerDeco + }); + }) : /*#__PURE__*/ _react.default.createElement(_nodeViewJs.NodeView, { + key: key, + node: child.node, + getPos: getChildPos, + outerDeco: child.outerDeco, + innerDeco: child.innerDeco + }); +}); +const InlinePartition = /*#__PURE__*/ (0, _react.memo)(function InlinePartition(param) { + let { childViews , getInnerPos } = param; + const reactKeys = (0, _useReactKeysJs.useReactKeys)(); + const firstChild = childViews[0]; + const getFirstChildPos = (0, _react.useRef)(()=>getInnerPos.current() + firstChild.offset); + getFirstChildPos.current = ()=>getInnerPos.current() + firstChild.offset; + const firstMark = firstChild.marks[0]; + if (!firstMark) { + return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, childViews.map((child)=>{ + const key = createKey(getInnerPos.current(), child, reactKeys?.posToKey); + return /*#__PURE__*/ _react.default.createElement(ChildView, { + key: key, + child: child, + getInnerPos: getInnerPos + }); + })); + } + return /*#__PURE__*/ _react.default.createElement(_markViewJs.MarkView, { + getPos: getFirstChildPos, + key: createKey(// editorState?.doc, + getInnerPos.current(), firstChild, reactKeys?.posToKey), + mark: firstMark + }, /*#__PURE__*/ _react.default.createElement(InlineView, { + key: createKey(// editorState?.doc, + getInnerPos.current(), firstChild, reactKeys?.posToKey), + getInnerPos: getInnerPos, + childViews: childViews.map((child)=>({ + ...child, + marks: child.marks.slice(1) + })) + })); +}); +const InlineView = /*#__PURE__*/ (0, _react.memo)(function InlineView(param) { + let { getInnerPos , childViews } = param; + // const editorState = useEditorState(); + const reactKeys = (0, _useReactKeysJs.useReactKeys)(); + const partitioned = childViews.reduce((acc, child)=>{ + const lastPartition = acc[acc.length - 1]; + if (!lastPartition) { + return [ + [ + child + ] + ]; + } + const lastChild = lastPartition[lastPartition.length - 1]; + if (!lastChild) { + return [ + ...acc.slice(0, acc.length), + [ + child + ] + ]; + } + if (!child.marks.length && !lastChild.marks.length || child.marks.length && lastChild.marks.length && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + child.marks[0]?.eq(lastChild.marks[0])) { + return [ + ...acc.slice(0, acc.length - 1), + [ + ...lastPartition.slice(0, lastPartition.length), + child + ] + ]; + } + return [ + ...acc, + [ + child + ] + ]; + }, []); + return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, partitioned.map((childViews)=>{ + const firstChild = childViews[0]; + if (!firstChild) return null; + const key = createKey(getInnerPos.current(), firstChild, reactKeys?.posToKey); + return /*#__PURE__*/ _react.default.createElement(InlinePartition, { + key: key, + childViews: childViews, + getInnerPos: getInnerPos + }); + })); +}); +function createKey(// doc: Node | undefined, +innerPos, child, posToKey) { + const pos = innerPos + child.offset; + const key = posToKey?.get(pos); + if (child.type === "widget" || child.type === "native-widget") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (child.widget.type.spec.key) // eslint-disable-next-line @typescript-eslint/no-explicit-any + return child.widget.type.spec.key; + // eslint-disable-next-line no-console + console.warn(`Widget at position ${pos} doesn't have a key specified. This has negative performance implications.`); + return `${key}-${child.index}`; + } + if (key) return key; + // if (!doc) return pos; + // const parentPos = doc.resolve(pos).start() - 1; + // const parentKey = posToKey?.get(parentPos); + // if (parentKey) return `${parentKey}-${child.offset}`; + return pos; +} +function adjustWidgetMarksForward(children) { + const lastChild = children[children.length - 1]; + if (lastChild?.type !== "widget" && lastChild?.type !== "native-widget" || // Using internal Decoration property, "type" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lastChild.widget.type.side >= 0) return; + let lastNodeChild = null; + for(let i = children.length - 2; i >= 0; i--){ + const child = children[i]; + if (child?.type === "node") { + lastNodeChild = child; + break; + } + } + if (!lastNodeChild || !lastNodeChild.node.isInline) return; + const marksToSpread = lastNodeChild.marks; + lastChild.marks = lastChild.marks.reduce((acc, mark)=>mark.addToSet(acc), marksToSpread); +} +function adjustWidgetMarksBack(children) { + const lastChild = children[children.length - 1]; + if (lastChild?.type !== "node" || !lastChild.node.isInline) return; + const marksToSpread = lastChild.marks; + for(let i = children.length - 2; i >= 0; i--){ + const child = children[i]; + if (child?.type !== "widget" && child?.type !== "native-widget" || // Using internal Decoration property, "type" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + child.widget.type.side < 0) break; + child.marks = child.marks.reduce((acc, mark)=>mark.addToSet(acc), marksToSpread); + } +} +const ChildElement = /*#__PURE__*/ (0, _react.memo)(function ChildElement(param) { + let { child , getInnerPos , posToKey } = param; + const getNodePos = (0, _react.useRef)(()=>getInnerPos.current() + child.offset); + getNodePos.current = ()=>getInnerPos.current() + child.offset; + const key = createKey(getInnerPos.current(), child, posToKey); + if (child.type === "node") { + return /*#__PURE__*/ _react.default.createElement(_nodeViewJs.NodeView, { + key: key, + outerDeco: child.outerDeco, + node: child.node, + innerDeco: child.innerDeco, + getPos: getNodePos + }); + } else { + return /*#__PURE__*/ _react.default.createElement(InlineView, { + key: key, + childViews: [ + child + ], + getInnerPos: getInnerPos + }); + } +}); +function createChildElements(children, getInnerPos, // doc: Node | undefined, +posToKey) { + if (!children.length) return []; + if (children.every((child)=>child.type !== "node" || child.node.isInline)) { + return [ + /*#__PURE__*/ _react.default.createElement(InlineView, { + key: createKey(// doc, + getInnerPos.current(), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + children[0], posToKey), + childViews: children, + getInnerPos: getInnerPos + }) + ]; + } + return children.map((child)=>{ + const key = createKey(getInnerPos.current(), child, posToKey); + return /*#__PURE__*/ _react.default.createElement(ChildElement, { + key: key, + child: child, + posToKey: posToKey, + getInnerPos: getInnerPos + }); + }); +} +const ChildNodeViews = /*#__PURE__*/ (0, _react.memo)(function ChildNodeViews(param) { + let { getPos , node , innerDecorations } = param; + // const editorState = useEditorState(); + const reactKeys = (0, _useReactKeysJs.useReactKeys)(); + const getInnerPos = (0, _react.useRef)(()=>getPos.current() + 1); + if (!node) return null; + const children = []; + (0, _iterDecoJs.iterDeco)(node, innerDecorations, (widget, isNative, offset, index)=>{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const widgetMarks = widget.type.spec.marks ?? []; + if (isNative) { + children.push({ + type: "native-widget", + widget: widget, + marks: widgetMarks, + offset, + index + }); + } else { + children.push({ + type: "widget", + widget: widget, + marks: widgetMarks, + offset, + index + }); + } + adjustWidgetMarksForward(children); + }, (childNode, outerDeco, innerDeco, offset)=>{ + children.push({ + type: "node", + node: childNode, + marks: childNode.marks, + innerDeco, + outerDeco, + offset + }); + adjustWidgetMarksBack(children); + }); + const childElements = createChildElements(children, getInnerPos, // editorState.doc, + reactKeys?.posToKey); + const lastChild = children[children.length - 1]; + if (!lastChild || lastChild.type !== "node" || lastChild.node.isInline && !lastChild.node.isText || // RegExp.test actually handles undefined just fine + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + /\n$/.test(lastChild.node.text)) { + childElements.push(/*#__PURE__*/ _react.default.createElement(_separatorHackViewJs.SeparatorHackView, { + getPos: getInnerPos, + key: "trailing-hack-img" + }), /*#__PURE__*/ _react.default.createElement(_trailingHackViewJs.TrailingHackView, { + getPos: getInnerPos, + key: "trailing-hack-br" + })); + } + return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, childElements); +}); diff --git a/dist/cjs/components/CursorWrapper.js b/dist/cjs/components/CursorWrapper.js new file mode 100644 index 00000000..b77a6661 --- /dev/null +++ b/dist/cjs/components/CursorWrapper.js @@ -0,0 +1,100 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "CursorWrapper", { + enumerable: true, + get: ()=>CursorWrapper +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _domJs = require("../dom.js"); +const _useEditorEffectJs = require("../hooks/useEditorEffect.js"); +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +const CursorWrapper = /*#__PURE__*/ (0, _react.forwardRef)(function CursorWrapper(param, ref) { + let { widget , getPos , ...props } = param; + const [shouldRender, setShouldRender] = (0, _react.useState)(true); + const innerRef = (0, _react.useRef)(null); + (0, _react.useImperativeHandle)(ref, ()=>{ + return innerRef.current; + }, []); + (0, _useEditorEffectJs.useEditorEffect)((view)=>{ + if (!view || !innerRef.current) return; + // @ts-expect-error Internal property - domObserver + view.domObserver.disconnectSelection(); + // @ts-expect-error Internal property - domSelection + const domSel = view.domSelection(); + const range = document.createRange(); + const node = innerRef.current; + const img = node.nodeName == "IMG"; + if (img && node.parentNode) { + range.setEnd(node.parentNode, (0, _domJs.domIndex)(node) + 1); + } else { + range.setEnd(node, 0); + } + range.collapse(false); + domSel.removeAllRanges(); + domSel.addRange(range); + setShouldRender(false); + // @ts-expect-error Internal property - domObserver + view.domObserver.connectSelection(); + }, []); + return shouldRender ? /*#__PURE__*/ _react.default.createElement("img", _extends({ + ref: innerRef, + className: "ProseMirror-separator", + // eslint-disable-next-line react/no-unknown-property + "mark-placeholder": "true", + alt: "" + }, props)) : null; +}); diff --git a/dist/cjs/components/DocNodeView.js b/dist/cjs/components/DocNodeView.js new file mode 100644 index 00000000..1159ebfc --- /dev/null +++ b/dist/cjs/components/DocNodeView.js @@ -0,0 +1,100 @@ +// TODO: I must be missing something, but I do not know why +// this linting rule is only broken in this file +/* eslint-disable react/prop-types */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "DocNodeView", { + enumerable: true, + get: ()=>DocNodeView +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _useNodeViewDescriptorJs = require("../hooks/useNodeViewDescriptor.js"); +const _childNodeViewsJs = require("./ChildNodeViews.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +const getPos = { + current () { + return -1; + } +}; +const DocNodeView = /*#__PURE__*/ (0, _react.memo)(/*#__PURE__*/ (0, _react.forwardRef)(function DocNodeView(param, ref) { + let { className , node , innerDeco , outerDeco , as , viewDesc , ...elementProps } = param; + const innerRef = (0, _react.useRef)(null); + (0, _react.useImperativeHandle)(ref, ()=>{ + return innerRef.current; + }, []); + const { childDescriptors , nodeViewDescRef } = (0, _useNodeViewDescriptorJs.useNodeViewDescriptor)(node, ()=>getPos.current(), innerRef, innerRef, innerDeco, outerDeco, viewDesc); + const childContextValue = (0, _react.useMemo)(()=>({ + parentRef: nodeViewDescRef, + siblingsRef: childDescriptors + }), [ + childDescriptors, + nodeViewDescRef + ]); + const props = { + ...elementProps, + ref: innerRef, + className, + suppressContentEditableWarning: true + }; + const element = as ? /*#__PURE__*/ (0, _react.cloneElement)(as, props, /*#__PURE__*/ _react.default.createElement(_childDescriptorsContextJs.ChildDescriptorsContext.Provider, { + value: childContextValue + }, /*#__PURE__*/ _react.default.createElement(_childNodeViewsJs.ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + }))) : /*#__PURE__*/ (0, _react.createElement)("div", props, /*#__PURE__*/ _react.default.createElement(_childDescriptorsContextJs.ChildDescriptorsContext.Provider, { + value: childContextValue + }, /*#__PURE__*/ _react.default.createElement(_childNodeViewsJs.ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + }))); + if (!node) return element; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeDecorations = outerDeco.filter((deco)=>!deco.inline); + if (!nodeDecorations.length) { + return element; + } + const wrapped = nodeDecorations.reduce(_childNodeViewsJs.wrapInDeco, element); + return wrapped; +})); diff --git a/dist/cjs/components/LayoutGroup.js b/dist/cjs/components/LayoutGroup.js new file mode 100644 index 00000000..bb0f9417 --- /dev/null +++ b/dist/cjs/components/LayoutGroup.js @@ -0,0 +1,107 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "LayoutGroup", { + enumerable: true, + get: ()=>LayoutGroup +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _layoutGroupContextJs = require("../contexts/LayoutGroupContext.js"); +const _useForceUpdateJs = require("../hooks/useForceUpdate.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +function LayoutGroup(param) { + let { children } = param; + const createQueue = (0, _react.useRef)(new Set()).current; + const destroyQueue = (0, _react.useRef)(new Set()).current; + const isMounted = (0, _react.useRef)(false); + const forceUpdate = (0, _useForceUpdateJs.useForceUpdate)(); + const isUpdatePending = (0, _react.useRef)(true); + const ensureFlush = (0, _react.useCallback)(()=>{ + if (!isUpdatePending.current) { + forceUpdate(); + isUpdatePending.current = true; + } + }, [ + forceUpdate + ]); + const register = (0, _react.useCallback)((effect)=>{ + let destroy; + const create = ()=>{ + destroy = effect(); + }; + createQueue.add(create); + ensureFlush(); + return ()=>{ + createQueue.delete(create); + if (destroy) { + if (isMounted.current) { + destroyQueue.add(destroy); + ensureFlush(); + } else { + destroy(); + } + } + }; + }, [ + createQueue, + destroyQueue, + ensureFlush + ]); + (0, _react.useLayoutEffect)(()=>{ + isUpdatePending.current = false; + createQueue.forEach((create)=>create()); + createQueue.clear(); + return ()=>{ + destroyQueue.forEach((destroy)=>destroy()); + destroyQueue.clear(); + }; + }); + (0, _react.useLayoutEffect)(()=>{ + isMounted.current = true; + return ()=>{ + isMounted.current = false; + }; + }, []); + return /*#__PURE__*/ _react.default.createElement(_layoutGroupContextJs.LayoutGroupContext.Provider, { + value: register + }, children); +} diff --git a/dist/cjs/components/MarkView.js b/dist/cjs/components/MarkView.js new file mode 100644 index 00000000..6874f0d8 --- /dev/null +++ b/dist/cjs/components/MarkView.js @@ -0,0 +1,110 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "MarkView", { + enumerable: true, + get: ()=>MarkView +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _viewdescJs = require("../viewdesc.js"); +const _outputSpecJs = require("./OutputSpec.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +const MarkView = /*#__PURE__*/ (0, _react.memo)(/*#__PURE__*/ (0, _react.forwardRef)(function MarkView(param, ref) { + let { mark , getPos , children } = param; + const { siblingsRef , parentRef } = (0, _react.useContext)(_childDescriptorsContextJs.ChildDescriptorsContext); + const viewDescRef = (0, _react.useRef)(undefined); + const childDescriptors = (0, _react.useRef)([]); + const domRef = (0, _react.useRef)(null); + (0, _react.useImperativeHandle)(ref, ()=>{ + return domRef.current; + }, []); + const outputSpec = (0, _react.useMemo)(()=>mark.type.spec.toDOM?.(mark, true), [ + mark + ]); + if (!outputSpec) throw new Error(`Mark spec for ${mark.type.name} is missing toDOM`); + (0, _react.useLayoutEffect)(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + (0, _react.useLayoutEffect)(()=>{ + if (!domRef.current) return; + const firstChildDesc = childDescriptors.current[0]; + if (!viewDescRef.current) { + viewDescRef.current = new _viewdescJs.MarkViewDesc(parentRef.current, childDescriptors.current, getPos.current(), mark, domRef.current, firstChildDesc?.dom.parentElement ?? domRef.current); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.dom = domRef.current; + viewDescRef.current.contentDOM = firstChildDesc?.dom.parentElement ?? domRef.current; + viewDescRef.current.mark = mark; + viewDescRef.current.pos = getPos.current(); + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + for (const childDesc of childDescriptors.current){ + childDesc.parent = viewDescRef.current; + } + }); + const childContextValue = (0, _react.useMemo)(()=>({ + parentRef: viewDescRef, + siblingsRef: childDescriptors + }), [ + childDescriptors, + viewDescRef + ]); + return /*#__PURE__*/ _react.default.createElement(_outputSpecJs.OutputSpec, { + ref: domRef, + outputSpec: outputSpec + }, /*#__PURE__*/ _react.default.createElement(_childDescriptorsContextJs.ChildDescriptorsContext.Provider, { + value: childContextValue + }, children)); +})); diff --git a/dist/cjs/components/NativeWidgetView.js b/dist/cjs/components/NativeWidgetView.js new file mode 100644 index 00000000..737d67a8 --- /dev/null +++ b/dist/cjs/components/NativeWidgetView.js @@ -0,0 +1,105 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "NativeWidgetView", { + enumerable: true, + get: ()=>NativeWidgetView +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _useEditorEffectJs = require("../hooks/useEditorEffect.js"); +const _viewdescJs = require("../viewdesc.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +function NativeWidgetView(param) { + let { widget , getPos } = param; + const { siblingsRef , parentRef } = (0, _react.useContext)(_childDescriptorsContextJs.ChildDescriptorsContext); + const viewDescRef = (0, _react.useRef)(null); + const rootDomRef = (0, _react.useRef)(null); + (0, _react.useLayoutEffect)(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + (0, _useEditorEffectJs.useEditorEffect)((view)=>{ + if (!rootDomRef.current) return; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const toDOM = widget.type.toDOM; + let dom = typeof toDOM === "function" ? toDOM(view, ()=>getPos.current()) : toDOM; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!widget.type.spec.raw) { + if (dom.nodeType != 1) { + const wrap = document.createElement("span"); + wrap.appendChild(dom); + dom = wrap; + } + dom.contentEditable = "false"; + dom.classList.add("ProseMirror-widget"); + } + if (rootDomRef.current.firstElementChild === dom) return; + rootDomRef.current.replaceChildren(dom); + }); + (0, _react.useLayoutEffect)(()=>{ + if (!rootDomRef.current) return; + if (!viewDescRef.current) { + viewDescRef.current = new _viewdescJs.WidgetViewDesc(parentRef.current, getPos.current(), widget, rootDomRef.current); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.widget = widget; + viewDescRef.current.pos = getPos.current(); + viewDescRef.current.dom = rootDomRef.current; + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + }); + return /*#__PURE__*/ _react.default.createElement("span", { + ref: rootDomRef + }); +} diff --git a/dist/cjs/components/NodeView.js b/dist/cjs/components/NodeView.js new file mode 100644 index 00000000..672929e9 --- /dev/null +++ b/dist/cjs/components/NodeView.js @@ -0,0 +1,219 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "NodeView", { + enumerable: true, + get: ()=>NodeView +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _reactDom = require("react-dom"); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _editorContextJs = require("../contexts/EditorContext.js"); +const _nodeViewContextJs = require("../contexts/NodeViewContext.js"); +const _stopEventContextJs = require("../contexts/StopEventContext.js"); +const _useNodeViewDescriptorJs = require("../hooks/useNodeViewDescriptor.js"); +const _childNodeViewsJs = require("./ChildNodeViews.js"); +const _markViewJs = require("./MarkView.js"); +const _outputSpecJs = require("./OutputSpec.js"); +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +const NodeView = /*#__PURE__*/ (0, _react.memo)(function NodeView(param) { + let { outerDeco , getPos , node , innerDeco , ...props } = param; + const domRef = (0, _react.useRef)(null); + const nodeDomRef = (0, _react.useRef)(null); + const contentDomRef = (0, _react.useRef)(null); + const getPosFunc = (0, _react.useRef)(()=>getPos.current()).current; + // this is ill-conceived; should revisit + const initialNode = (0, _react.useRef)(node); + const initialOuterDeco = (0, _react.useRef)(outerDeco); + const initialInnerDeco = (0, _react.useRef)(innerDeco); + const customNodeViewRootRef = (0, _react.useRef)(null); + const customNodeViewRef = (0, _react.useRef)(null); + // const state = useEditorState(); + const { nodeViews } = (0, _react.useContext)(_nodeViewContextJs.NodeViewContext); + const { view } = (0, _react.useContext)(_editorContextJs.EditorContext); + let element = null; + const Component = nodeViews[node.type.name]; + const outputSpec = (0, _react.useMemo)(()=>node.type.spec.toDOM?.(node), [ + node + ]); + // TODO: Would be great to pull all of the custom node view stuff into + // a hook + const customNodeView = view?.someProp("nodeViews", (nodeViews)=>nodeViews?.[node.type.name]); + (0, _react.useLayoutEffect)(()=>{ + if (!customNodeViewRef.current || !customNodeViewRootRef.current) return; + const { dom } = customNodeViewRef.current; + nodeDomRef.current = customNodeViewRootRef.current; + customNodeViewRootRef.current.appendChild(dom); + return ()=>{ + customNodeViewRef.current?.destroy?.(); + }; + }, []); + (0, _react.useLayoutEffect)(()=>{ + if (!customNodeView || !customNodeViewRef.current) return; + const { destroy , update } = customNodeViewRef.current; + const updated = update?.call(customNodeViewRef.current, node, outerDeco, innerDeco) ?? true; + if (updated) return; + destroy?.call(customNodeViewRef.current); + if (!customNodeViewRootRef.current) return; + initialNode.current = node; + initialOuterDeco.current = outerDeco; + initialInnerDeco.current = innerDeco; + customNodeViewRef.current = customNodeView(initialNode.current, // customNodeView will only be set if view is set, and we can only reach + // this line if customNodeView is set + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + view, ()=>getPos.current(), initialOuterDeco.current, initialInnerDeco.current); + const { dom } = customNodeViewRef.current; + nodeDomRef.current = customNodeViewRootRef.current; + customNodeViewRootRef.current.appendChild(dom); + }, [ + customNodeView, + view, + innerDeco, + node, + outerDeco, + getPos + ]); + const { hasContentDOM , childDescriptors , setStopEvent , nodeViewDescRef } = (0, _useNodeViewDescriptorJs.useNodeViewDescriptor)(node, ()=>getPos.current(), domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef); + const finalProps = { + ...props, + ...!hasContentDOM && { + contentEditable: false + } + }; + const nodeProps = (0, _react.useMemo)(()=>({ + node: node, + getPos: getPosFunc, + decorations: outerDeco, + innerDecorations: innerDeco, + isSelected: false + }), [ + getPosFunc, + innerDeco, + node, + outerDeco + ]); + if (Component) { + element = /*#__PURE__*/ _react.default.createElement(Component, _extends({}, finalProps, { + ref: nodeDomRef, + nodeProps: nodeProps + }), /*#__PURE__*/ _react.default.createElement(_childNodeViewsJs.ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + })); + } else if (customNodeView) { + if (!customNodeViewRef.current) { + customNodeViewRef.current = customNodeView(initialNode.current, // customNodeView will only be set if view is set, and we can only reach + // this line if customNodeView is set + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + view, ()=>getPos.current(), initialOuterDeco.current, initialInnerDeco.current); + } + const { contentDOM } = customNodeViewRef.current; + contentDomRef.current = contentDOM ?? null; + element = /*#__PURE__*/ (0, _react.createElement)(node.isInline ? "span" : "div", { + ref: customNodeViewRootRef, + contentEditable: !!contentDOM, + suppressContentEditableWarning: true + }, contentDOM && /*#__PURE__*/ (0, _reactDom.createPortal)(/*#__PURE__*/ _react.default.createElement(_childNodeViewsJs.ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + }), contentDOM)); + } else { + if (outputSpec) { + element = /*#__PURE__*/ _react.default.createElement(_outputSpecJs.OutputSpec, _extends({}, finalProps, { + ref: nodeDomRef, + outputSpec: outputSpec + }), /*#__PURE__*/ _react.default.createElement(_childNodeViewsJs.ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + })); + } + } + if (!element) { + throw new Error(`Node spec for ${node.type.name} is missing toDOM`); + } + const decoratedElement = /*#__PURE__*/ (0, _react.cloneElement)(outerDeco.reduce(_childNodeViewsJs.wrapInDeco, element), // eslint-disable-next-line @typescript-eslint/no-explicit-any + outerDeco.some((d)=>d.type.attrs.nodeName) ? { + ref: domRef + } : // we've already passed the domRef to the NodeView component + // as a prop + undefined); + // TODO: Should we only be wrapping non-inline elements? Inline elements have + // already been wrapped in ChildNodeViews/InlineView? + const markedElement = node.marks.reduce((element, mark)=>/*#__PURE__*/ _react.default.createElement(_markViewJs.MarkView, { + getPos: getPos, + mark: mark + }, element), decoratedElement); + const childContextValue = (0, _react.useMemo)(()=>({ + parentRef: nodeViewDescRef, + siblingsRef: childDescriptors + }), [ + childDescriptors, + nodeViewDescRef + ]); + return /*#__PURE__*/ _react.default.createElement(_stopEventContextJs.StopEventContext.Provider, { + value: setStopEvent + }, /*#__PURE__*/ _react.default.createElement(_childDescriptorsContextJs.ChildDescriptorsContext.Provider, { + value: childContextValue + }, /*#__PURE__*/ (0, _react.cloneElement)(markedElement, node.marks.length || // eslint-disable-next-line @typescript-eslint/no-explicit-any + outerDeco.some((d)=>d.type.attrs.nodeName) ? { + ref: domRef + } : // we've already passed the domRef to the NodeView component + // as a prop + undefined))); +}); diff --git a/dist/cjs/components/NodeViewComponentProps.js b/dist/cjs/components/NodeViewComponentProps.js new file mode 100644 index 00000000..b62a6d55 --- /dev/null +++ b/dist/cjs/components/NodeViewComponentProps.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); diff --git a/dist/cjs/components/OutputSpec.js b/dist/cjs/components/OutputSpec.js new file mode 100644 index 00000000..aed86b73 --- /dev/null +++ b/dist/cjs/components/OutputSpec.js @@ -0,0 +1,84 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "OutputSpec", { + enumerable: true, + get: ()=>ForwardedOutputSpec +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _propsJs = require("../props.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +const ForwardedOutputSpec = /*#__PURE__*/ (0, _react.memo)(/*#__PURE__*/ (0, _react.forwardRef)(function OutputSpec(param, ref) { + let { outputSpec , children , ...propOverrides } = param; + if (typeof outputSpec === "string") { + return /*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, outputSpec); + } + if (!Array.isArray(outputSpec)) { + throw new Error("@nytimes/react-prosemirror only supports strings and arrays in toDOM"); + } + const tagSpec = outputSpec[0]; + const tagName = tagSpec.replace(" ", ":"); + const attrs = outputSpec[1]; + let props = { + ref, + ...propOverrides + }; + let start = 1; + if (attrs && typeof attrs === "object" && attrs.nodeType == null && !Array.isArray(attrs)) { + start = 2; + props = (0, _propsJs.mergeReactProps)((0, _propsJs.htmlAttrsToReactProps)(attrs), props); + } + const content = []; + for(let i = start; i < outputSpec.length; i++){ + const child = outputSpec[i]; + if (child === 0) { + if (i < outputSpec.length - 1 || i > start) { + throw new RangeError("Content hole must be the only child of its parent node"); + } + return /*#__PURE__*/ (0, _react.createElement)(tagName, props, children); + } + content.push(/*#__PURE__*/ _react.default.createElement(ForwardedOutputSpec, { + outputSpec: child + }, children)); + } + return /*#__PURE__*/ (0, _react.createElement)(tagName, props, ...content); +})); diff --git a/dist/cjs/components/ProseMirror.js b/dist/cjs/components/ProseMirror.js new file mode 100644 index 00000000..0740ac44 --- /dev/null +++ b/dist/cjs/components/ProseMirror.js @@ -0,0 +1,113 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "ProseMirror", { + enumerable: true, + get: ()=>ProseMirror +}); +const _prosemirrorView = require("prosemirror-view"); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _editorContextJs = require("../contexts/EditorContext.js"); +const _editorStateContextJs = require("../contexts/EditorStateContext.js"); +const _nodeViewContextJs = require("../contexts/NodeViewContext.js"); +const _computeDocDecoJs = require("../decorations/computeDocDeco.js"); +const _viewDecorationsJs = require("../decorations/viewDecorations.js"); +const _useEditorJs = require("../hooks/useEditor.js"); +const _layoutGroupJs = require("./LayoutGroup.js"); +const _proseMirrorDocJs = require("./ProseMirrorDoc.js"); +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +const EMPTY_OUTER_DECOS = []; +function ProseMirrorInner(param) { + let { className , children , nodeViews ={} , customNodeViews , ...props } = param; + const [mount, setMount] = (0, _react.useState)(null); + const { editor , state } = (0, _useEditorJs.useEditor)(mount, { + ...props, + nodeViews: customNodeViews + }); + const innerDecos = editor.view ? (0, _viewDecorationsJs.viewDecorations)(editor.view, editor.cursorWrapper) : _prosemirrorView.DecorationSet.empty; + const outerDecos = editor.view ? (0, _computeDocDecoJs.computeDocDeco)(editor.view) : EMPTY_OUTER_DECOS; + const nodeViewContextValue = (0, _react.useMemo)(()=>({ + nodeViews + }), [ + nodeViews + ]); + const docNodeViewContextValue = (0, _react.useMemo)(()=>({ + className: className, + setMount: setMount, + node: editor.view?.state.doc, + innerDeco: innerDecos, + outerDeco: outerDecos, + viewDesc: editor.docViewDescRef.current + }), [ + className, + editor.docViewDescRef, + editor.view?.state.doc, + innerDecos, + outerDecos + ]); + return /*#__PURE__*/ _react.default.createElement(_editorContextJs.EditorContext.Provider, { + value: editor + }, /*#__PURE__*/ _react.default.createElement(_editorStateContextJs.EditorStateContext.Provider, { + value: state + }, /*#__PURE__*/ _react.default.createElement(_nodeViewContextJs.NodeViewContext.Provider, { + value: nodeViewContextValue + }, /*#__PURE__*/ _react.default.createElement(_proseMirrorDocJs.DocNodeViewContext.Provider, { + value: docNodeViewContextValue + }, children)))); +} +function ProseMirror(props) { + return /*#__PURE__*/ _react.default.createElement(_layoutGroupJs.LayoutGroup, null, /*#__PURE__*/ _react.default.createElement(ProseMirrorInner, _extends({}, props))); +} diff --git a/dist/cjs/components/ProseMirrorDoc.js b/dist/cjs/components/ProseMirrorDoc.js new file mode 100644 index 00000000..1203cbbe --- /dev/null +++ b/dist/cjs/components/ProseMirrorDoc.js @@ -0,0 +1,99 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + DocNodeViewContext: ()=>DocNodeViewContext, + ProseMirrorDoc: ()=>ForwardedProseMirrorDoc +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _docNodeViewJs = require("./DocNodeView.js"); +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +const DocNodeViewContext = /*#__PURE__*/ (0, _react.createContext)(null); +function ProseMirrorDoc(param, ref) { + let { as , ...props } = param; + const childDescriptors = (0, _react.useRef)([]); + const innerRef = (0, _react.useRef)(null); + const { setMount , ...docProps } = (0, _react.useContext)(DocNodeViewContext); + const viewDescRef = (0, _react.useRef)(undefined); + (0, _react.useImperativeHandle)(ref, ()=>{ + return innerRef.current; + }, []); + const childContextValue = (0, _react.useMemo)(()=>({ + parentRef: viewDescRef, + siblingsRef: childDescriptors + }), [ + childDescriptors, + viewDescRef + ]); + return /*#__PURE__*/ _react.default.createElement(_childDescriptorsContextJs.ChildDescriptorsContext.Provider, { + value: childContextValue + }, /*#__PURE__*/ _react.default.createElement(_docNodeViewJs.DocNodeView, _extends({ + ref: (el)=>{ + innerRef.current = el; + setMount(el); + } + }, props, docProps, { + as: as + }))); +} +const ForwardedProseMirrorDoc = /*#__PURE__*/ (0, _react.forwardRef)(ProseMirrorDoc); diff --git a/dist/cjs/components/SeparatorHackView.js b/dist/cjs/components/SeparatorHackView.js new file mode 100644 index 00000000..e1e6e648 --- /dev/null +++ b/dist/cjs/components/SeparatorHackView.js @@ -0,0 +1,95 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "SeparatorHackView", { + enumerable: true, + get: ()=>SeparatorHackView +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _browserJs = require("../browser.js"); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _viewdescJs = require("../viewdesc.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +function SeparatorHackView(param) { + let { getPos } = param; + const { siblingsRef , parentRef } = (0, _react.useContext)(_childDescriptorsContextJs.ChildDescriptorsContext); + const viewDescRef = (0, _react.useRef)(null); + const ref = (0, _react.useRef)(null); + const [shouldRender, setShouldRender] = (0, _react.useState)(false); + (0, _react.useLayoutEffect)(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + // There's no risk of an infinite loop here, because + // we call setShouldRender conditionally + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, _react.useLayoutEffect)(()=>{ + const lastSibling = siblingsRef.current[siblingsRef.current.length - 1]; + if ((_browserJs.browser.safari || _browserJs.browser.chrome) && (lastSibling?.dom)?.contentEditable == "false") { + setShouldRender(true); + return; + } + if (!ref.current) return; + if (!viewDescRef.current) { + viewDescRef.current = new _viewdescJs.TrailingHackViewDesc(parentRef.current, [], getPos.current(), ref.current, null); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.dom = ref.current; + viewDescRef.current.pos = getPos.current(); + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + }); + return shouldRender ? /*#__PURE__*/ _react.default.createElement("img", { + ref: ref, + className: "ProseMirror-separator" + }) : null; +} diff --git a/dist/cjs/components/TextNodeView.js b/dist/cjs/components/TextNodeView.js new file mode 100644 index 00000000..b62d0f85 --- /dev/null +++ b/dist/cjs/components/TextNodeView.js @@ -0,0 +1,113 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "TextNodeView", { + enumerable: true, + get: ()=>TextNodeView +}); +const _prosemirrorView = require("prosemirror-view"); +const _react = require("react"); +const _reactDom = require("react-dom"); +const _viewdescJs = require("../viewdesc.js"); +const _childNodeViewsJs = require("./ChildNodeViews.js"); +function shallowEqual(objA, objB) { + if (objA === objB) { + return true; + } + if (!objA || !objB) { + return false; + } + const aKeys = Object.keys(objA); + const bKeys = Object.keys(objB); + const len = aKeys.length; + if (bKeys.length !== len) { + return false; + } + for(let i = 0; i < len; i++){ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const key = aKeys[i]; + if (objA[key] !== objB[key] || !Object.prototype.hasOwnProperty.call(objB, key)) { + return false; + } + } + return true; +} +let TextNodeView = class TextNodeView extends _react.Component { + updateEffect() { + const { view , decorations , siblingsRef , parentRef , getPos , node } = this.props; + // There simply is no other way to ref a text node + // eslint-disable-next-line react/no-find-dom-node + const dom = (0, _reactDom.findDOMNode)(this); + // We only need to explicitly create a CompositionViewDesc + // when a composition was started that produces a new text node. + // Otherwise we just rely on re-rendering the renderRef + if (!dom) { + if (!view?.composing) return; + this.viewDescRef = new _viewdescJs.CompositionViewDesc(parentRef.current, getPos.current(), // These are just placeholders/dummies. We can't + // actually find the correct DOM nodes from here, + // so we let our parent do it. + // Passing a valid element here just so that the + // ViewDesc constructor doesn't blow up. + document.createElement("div"), document.createTextNode(node.text ?? ""), node.text ?? ""); + return; + } + let textNode = dom; + while(textNode.firstChild){ + textNode = textNode.firstChild; + } + if (!this.viewDescRef || this.viewDescRef instanceof _viewdescJs.CompositionViewDesc) { + this.viewDescRef = new _viewdescJs.TextViewDesc(undefined, [], getPos.current(), node, decorations, _prosemirrorView.DecorationSet.empty, dom, textNode); + } else { + this.viewDescRef.parent = parentRef.current; + this.viewDescRef.children = []; + this.viewDescRef.node = node; + this.viewDescRef.pos = getPos.current(); + this.viewDescRef.outerDeco = decorations; + this.viewDescRef.innerDeco = _prosemirrorView.DecorationSet.empty; + this.viewDescRef.dom = dom; + // @ts-expect-error We have our own ViewDesc implementations + this.viewDescRef.dom.pmViewDesc = this.viewDescRef; + this.viewDescRef.nodeDOM = textNode; + } + if (!siblingsRef.current.includes(this.viewDescRef)) { + siblingsRef.current.push(this.viewDescRef); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + } + shouldComponentUpdate(nextProps) { + return !shallowEqual(this.props, nextProps); + } + componentDidMount() { + this.updateEffect(); + } + componentDidUpdate() { + this.updateEffect(); + } + componentWillUnmount() { + const { siblingsRef } = this.props; + if (!this.viewDescRef) return; + if (siblingsRef.current.includes(this.viewDescRef)) { + const index = siblingsRef.current.indexOf(this.viewDescRef); + siblingsRef.current.splice(index, 1); + } + } + render() { + const { view , getPos , node , decorations } = this.props; + // During a composition, it's crucial that we don't try to + // update the DOM that the user is working in. If there's + // an active composition and the selection is in this node, + // we freeze the DOM of this element so that it doesn't + // interrupt the composition + if (view?.composing && view.state.selection.from >= getPos.current() && view.state.selection.from <= getPos.current() + node.nodeSize) { + return this.renderRef; + } + this.renderRef = decorations.reduce(_childNodeViewsJs.wrapInDeco, node.text); + return this.renderRef; + } + constructor(...args){ + super(...args); + this.viewDescRef = null; + this.renderRef = null; + } +}; diff --git a/dist/cjs/components/TrailingHackView.js b/dist/cjs/components/TrailingHackView.js new file mode 100644 index 00000000..2d416e49 --- /dev/null +++ b/dist/cjs/components/TrailingHackView.js @@ -0,0 +1,86 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "TrailingHackView", { + enumerable: true, + get: ()=>TrailingHackView +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _viewdescJs = require("../viewdesc.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +function TrailingHackView(param) { + let { getPos } = param; + const { siblingsRef , parentRef } = (0, _react.useContext)(_childDescriptorsContextJs.ChildDescriptorsContext); + const viewDescRef = (0, _react.useRef)(null); + const ref = (0, _react.useRef)(null); + (0, _react.useLayoutEffect)(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + (0, _react.useLayoutEffect)(()=>{ + if (!ref.current) return; + if (!viewDescRef.current) { + viewDescRef.current = new _viewdescJs.TrailingHackViewDesc(parentRef.current, [], getPos.current(), ref.current, null); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.dom = ref.current; + viewDescRef.current.pos = getPos.current(); + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + }); + return /*#__PURE__*/ _react.default.createElement("br", { + ref: ref, + className: "ProseMirror-trailingBreak" + }); +} diff --git a/dist/cjs/components/WidgetView.js b/dist/cjs/components/WidgetView.js new file mode 100644 index 00000000..4869317a --- /dev/null +++ b/dist/cjs/components/WidgetView.js @@ -0,0 +1,91 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "WidgetView", { + enumerable: true, + get: ()=>WidgetView +}); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _viewdescJs = require("../viewdesc.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +function WidgetView(param) { + let { widget , getPos } = param; + const { siblingsRef , parentRef } = (0, _react.useContext)(_childDescriptorsContextJs.ChildDescriptorsContext); + const viewDescRef = (0, _react.useRef)(null); + const getPosFunc = (0, _react.useRef)(()=>getPos.current()).current; + const domRef = (0, _react.useRef)(null); + (0, _react.useLayoutEffect)(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + (0, _react.useLayoutEffect)(()=>{ + if (!domRef.current) return; + if (!viewDescRef.current) { + viewDescRef.current = new _viewdescJs.WidgetViewDesc(parentRef.current, getPos.current(), widget, domRef.current); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.widget = widget; + viewDescRef.current.pos = getPos.current(); + viewDescRef.current.dom = domRef.current; + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + }); + const { Component } = widget.type; + return Component && /*#__PURE__*/ _react.default.createElement(Component, { + ref: domRef, + widget: widget, + getPos: getPosFunc, + contentEditable: false + }); +} diff --git a/dist/cjs/components/WidgetViewComponentProps.js b/dist/cjs/components/WidgetViewComponentProps.js new file mode 100644 index 00000000..b62a6d55 --- /dev/null +++ b/dist/cjs/components/WidgetViewComponentProps.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); diff --git a/dist/cjs/components/__tests__/ProseMirror.composition.test.js b/dist/cjs/components/__tests__/ProseMirror.composition.test.js new file mode 100644 index 00000000..be784942 --- /dev/null +++ b/dist/cjs/components/__tests__/ProseMirror.composition.test.js @@ -0,0 +1,398 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _prosemirrorTestBuilder = require("prosemirror-test-builder"); +const _prosemirrorView = require("prosemirror-view"); +const _editorViewTestHelpersJs = require("../../testing/editorViewTestHelpers.js"); +function endComposition(view, forceUpdate) { + (0, _prosemirrorView["__endComposition"])(view, forceUpdate); +} +function event(pm, type) { + pm.dom.dispatchEvent(new CompositionEvent(type)); +} +function edit(node) { + let text = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : "", from = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : node.nodeValue.length, to = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : from; + const 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) { + let options = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : {}; + event(pm, "compositionstart"); + expect(pm.composing).toBeTruthy(); + let node; + const sel = document.getSelection(); + for(let i = -1; i < update.length; i++){ + if (i < 0) node = start(); + else update[i](node); + const { focusNode , focusOffset } = sel; + // @ts-expect-error Internal property + pm.domObserver.flush(); + if (options.cancel && i == update.length - 1) { + expect(hasCompositionNode(pm)).toBeFalsy(); + } else { + expect(node.parentNode && pm.dom.contains(node.parentNode)).toBeTruthy(); + expect(sel.focusNode === focusNode).toBeTruthy(); + expect(sel.focusOffset === focusOffset).toBeTruthy(); + if (options.node) expect(hasCompositionNode(pm)).toBeTruthy(); + } + } + event(pm, "compositionend"); + if (options.end) { + options.end(node); + // @ts-expect-error Internal property + pm.domObserver.flush(); + } + endComposition(pm); + expect(pm.composing).toBeFalsy(); + expect(hasCompositionNode(pm)).toBeFalsy(); +} +// function wordDeco(state: EditorState) { +// const re = /\w+/g, +// deco: Decoration[] = []; +// 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 }, +// }); +// const Widget = forwardRef(function Widget( +// { widget, pos, ...props }: WidgetViewComponentProps, +// ref: Ref +// ) { +// return ( +// +// × +// +// ); +// }); +// function widgets(positions: number[], sides: number[]) { +// return new Plugin({ +// state: { +// init(state) { +// const deco = positions.map((p, i) => +// widget(p, Widget, { side: sides[i] }) +// ); +// return DecorationSet.create(state.doc!, deco); +// }, +// apply(tr, deco) { +// return deco.map(tr.mapping, tr.doc); +// }, +// }, +// props: { +// decorations(this: Plugin, state) { +// return this.getState(state); +// }, +// }, +// }); +// } +// These unfortunately aren't working at the moment, though +// composition seems to be working generally. +describe.skip("EditorView composition", ()=>{ + it("supports composition in an empty block", ()=>{ + const { view: pm } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("")) + }); + compose(pm, ()=>edit(pm.dom.firstChild.appendChild(document.createTextNode("a"))), [ + (n)=>edit(n, "b"), + (n)=>edit(n, "c") + ], { + node: true + }); + expect(pm.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abc"))); + }); + it("supports composition at end of block", ()=>{ + const { view: pm } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")) + }); + compose(pm, ()=>edit((0, _editorViewTestHelpersJs.findTextNode)(pm.dom, "foo")), [ + (n)=>edit(n, "!"), + (n)=>edit(n, "?") + ]); + expect(pm.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo!?"))); + }); + it("supports composition at end of block in a new node", ()=>{ + const { view: pm } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")) + }); + compose(pm, ()=>edit(pm.dom.firstChild.appendChild(document.createTextNode("!"))), [ + (n)=>edit(n, "?") + ], // $$FORK: We don't use composition view descriptors except for in initially empty nodes + { + node: false + }); + expect(pm.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo!?"))); + }); + it("supports composition at start of block in a new node", ()=>{ + const { view: pm } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")) + }); + compose(pm, ()=>{ + const p = pm.dom.firstChild; + return edit(p.insertBefore(document.createTextNode("!"), p.firstChild)); + }, [ + (n)=>edit(n, "?") + ], // $$FORK: We don't use composition view descriptors except for in initially empty nodes + { + node: false + }); + expect(pm.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("!?foo"))); + }); + it("supports composition inside existing text", ()=>{ + const { view: pm } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")) + }); + compose(pm, ()=>edit((0, _editorViewTestHelpersJs.findTextNode)(pm.dom, "foo")), [ + (n)=>edit(n, "x", 1), + (n)=>edit(n, "y", 2), + (n)=>edit(n, "z", 3) + ]); + expect(pm.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("fxyzoo"))); + }); + // TODO: Offset out of bound + // eslint-disable-next-line jest/no-disabled-tests + it.skip("can deal with Android-style newline-after-composition", ()=>{ + const { view: pm } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abcdef")) + }); + compose(pm, ()=>edit((0, _editorViewTestHelpersJs.findTextNode)(pm.dom, "abcdef")), [ + (n)=>edit(n, "x", 3), + (n)=>edit(n, "y", 4) + ], { + end: (n)=>{ + const line = pm.dom.appendChild(document.createElement("div")); + line.textContent = "def"; + n.nodeValue = "abcxy"; + document.getSelection().collapse(line, 0); + } + }); + expect(pm.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abcxy"), (0, _prosemirrorTestBuilder.p)("def"))); + }); + it("handles replacement of existing words", ()=>{ + const { view: pm } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one two three")) + }); + compose(pm, ()=>edit((0, _editorViewTestHelpersJs.findTextNode)(pm.dom, "one two three"), "five", 4, 7), [ + (n)=>edit(n, "seven", 4, 8), + (n)=>edit(n, "zero", 4, 9) + ]); + expect(pm.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one zero three"))); + }); + it("handles composition inside marks", ()=>{ + const { view: pm } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one ", (0, _prosemirrorTestBuilder.em)("two"))) + }); + compose(pm, ()=>edit((0, _editorViewTestHelpersJs.findTextNode)(pm.dom, "two"), "o"), [ + (n)=>edit(n, "o"), + (n)=>edit(n, "w") + ]); + expect(pm.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one ", (0, _prosemirrorTestBuilder.em)("twooow")))); + }); + it.skip("handles composition in a mark that has multiple children", ()=>{ + const { view: pm } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one ", (0, _prosemirrorTestBuilder.em)("two", (0, _prosemirrorTestBuilder.strong)(" three")))) + }); + compose(pm, ()=>edit((0, _editorViewTestHelpersJs.findTextNode)(pm.dom, "two"), "o"), [ + (n)=>edit(n, "o"), + (n)=>edit(n, "w") + ]); + expect(pm.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one ", (0, _prosemirrorTestBuilder.em)("twooow", (0, _prosemirrorTestBuilder.strong)(" three"))))); + }); +// it("supports composition in a cursor wrapper", () => { +// const { view: pm } = 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", () => { +// const { view: pm } = requireFocus( +// tempEditor({ doc: doc(p("one ", em("two", strong(" three")))) }) +// ); +// pm.dispatch(pm.state.tr.addStoredMark(schema.marks.code.create())); +// const 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", () => { +// const { view: 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", () => { +// const { view: 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", () => { +// const { view: 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: Text) => { +// 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", () => { +// const { view: pm } = requireFocus( +// tempEditor({ doc: doc(p("")), plugins: [widgets([1, 1], [-1, 1])] }) +// ); +// compose( +// pm, +// () => { +// const 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", () => { +// const { view: 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", () => { +// const { view: 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", () => { +// const { view: 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", () => { +// const { view: 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", () => { +// const { view: pm } = tempEditor({ doc: doc(p("one"), p("two")) }); +// event(pm, "compositionstart"); +// const one = findTextNode(pm.dom, "one")!; +// edit(one, "!"); +// pm.domObserver.flush(); +// event(pm, "compositionend"); +// one.nodeValue = "one!!"; +// const L2 = pm.dom.lastChild; +// event(pm, "compositionstart"); +// const two = findTextNode(pm.dom, "two")!; +// ist(pm.dom.lastChild, L2); +// edit(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 = false) { +// const { view: 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!); +// const target = pm.dom.firstChild!.firstChild as Text; +// 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)); +}); diff --git a/dist/cjs/components/__tests__/ProseMirror.domchange.test.js b/dist/cjs/components/__tests__/ProseMirror.domchange.test.js new file mode 100644 index 00000000..e4a6a82b --- /dev/null +++ b/dist/cjs/components/__tests__/ProseMirror.domchange.test.js @@ -0,0 +1,270 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _react = require("@testing-library/react"); +const _prosemirrorState = require("prosemirror-state"); +const _prosemirrorTestBuilder = require("prosemirror-test-builder"); +const _webdriverio = require("webdriverio"); +const _editorViewTestHelpersJs = require("../../testing/editorViewTestHelpers.js"); +describe("DOM change", ()=>{ + it("notices when text is added", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hello")) + }); + view.focus(); + await browser.keys([ + _webdriverio.Key.Shift, + "l" + ]); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("heLllo"))); + }); + it("notices when text is removed", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hello")) + }); + view.focus(); + await browser.keys(_webdriverio.Key.Backspace); + await browser.keys(_webdriverio.Key.Backspace); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("heo"))); + }); + it("respects stored marks", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hello")) + }); + view.dispatch(view.state.tr.addStoredMark(view.state.schema.marks.em.create())); + view.focus(); + await browser.keys("o"); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hello", (0, _prosemirrorTestBuilder.em)("o")))); + }); + it("support inserting repeated text", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hello")) + }); + view.focus(); + await browser.keys("h"); + await browser.keys("e"); + await browser.keys("l"); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("helhello"))); + }); + it("detects an enter press", async ()=>{ + let enterPressed = false; + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.blockquote)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.p)(""))), + handleKeyDown: (_, event)=>{ + if (event.key === "Enter") return enterPressed = true; + return false; + } + }); + view.focus(); + await browser.keys(_webdriverio.Key.Enter); + expect(enterPressed).toBeTruthy(); + }); + it("detects a simple backspace press", async ()=>{ + let backspacePressed = false; + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.blockquote)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.p)(""))), + handleKeyDown: (_, event)=>{ + if (event.key === "Backspace") return backspacePressed = true; + return false; + } + }); + view.focus(); + await browser.keys(_webdriverio.Key.Backspace); + expect(backspacePressed).toBeTruthy(); + }); + it("correctly adjusts the selection", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abc")) + }); + view.focus(); + await browser.keys("d"); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abcd"))); + expect(view.state.selection.anchor).toBe(5); + expect(view.state.selection.head).toBe(5); + }); + // todoit("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); + // }); + // $$FORK: We _do_ repaint text nodes when they're typed into. + // Unlike prosemirror-view, we prevent user inputs from modifying + // the dom until after we've turned them into transactions. + // This test instead ensures that we only modify the character data, + // rather than replacing entire nodes. + it("does not replace a text node when it's typed into", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")) + }); + let mutated = false; + const observer = new MutationObserver(()=>mutated = true); + observer.observe(view.dom, { + subtree: true, + characterData: false, + childList: true + }); + view.focus(); + await browser.keys("j"); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("fojo"))); + expect(mutated).toBeFalsy(); + observer.disconnect(); + }); + it("understands text typed into an empty paragraph", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("")) + }); + view.focus(); + await browser.keys("i"); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("i"))); + }); + it("fixes text changes when input is ignored", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + controlled: true, + dispatchTransaction () { + // intentionally do nothing + } + }); + view.focus(); + await browser.keys("i"); + expect(view.dom.textContent).toBe("foo"); + }); + it("aborts when an incompatible state is set", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abcde")) + }); + view.dispatchEvent({ + type: "input" + }); + view.focus(); + await browser.keys("x"); + view.updateState(_prosemirrorState.EditorState.create({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("uvw")) + })); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("uvw"))); + }); + it("preserves marks on deletion", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one", (0, _prosemirrorTestBuilder.em)("x"))) + }); + view.focus(); + await browser.keys(_webdriverio.Key.Backspace); + view.dispatchEvent({ + type: "input" + }); + view.dispatch(view.state.tr.insertText("y")); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one", (0, _prosemirrorTestBuilder.em)("y")))); + }); + it("works when a node's contentDOM is deleted", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one"), (0, _prosemirrorTestBuilder.pre)("two")) + }); + view.focus(); + await browser.keys(_webdriverio.Key.Backspace); + await browser.keys(_webdriverio.Key.Backspace); + await browser.keys(_webdriverio.Key.Backspace); + view.dispatchEvent({ + type: "input" + }); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one"), (0, _prosemirrorTestBuilder.pre)())); + expect(view.state.selection.head).toBe(6); + }); + it("doesn't redraw content with marks when typing in front", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.em)("bar"), (0, _prosemirrorTestBuilder.strong)("baz"))) + }); + const bar = await _react.screen.findByText("bar"); + const foo = await _react.screen.findByText("foo"); + view.focus(); + await browser.keys("r"); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("froo", (0, _prosemirrorTestBuilder.em)("bar"), (0, _prosemirrorTestBuilder.strong)("baz")))); + expect(bar.parentNode).toBeTruthy(); + expect(view.dom.contains(bar.parentNode)).toBeTruthy(); + expect(foo.parentNode).toBeTruthy(); + expect(view.dom.contains(foo.parentNode)).toBeTruthy(); + }); + it("doesn't redraw content with marks when typing inside mark", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.em)("bar"), (0, _prosemirrorTestBuilder.strong)("baz"))) + }); + const bar = await _react.screen.findByText("bar"); + const foo = await _react.screen.findByText("foo"); + view.focus(); + await browser.keys("a"); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.em)("baar"), (0, _prosemirrorTestBuilder.strong)("baz")))); + expect(bar.parentNode).toBeTruthy(); + expect(view.dom.contains(bar.parentNode)).toBeTruthy(); + expect(foo.parentNode).toBeTruthy(); + expect(view.dom.contains(foo.parentNode)).toBeTruthy(); + }); + it("maps input to coordsAtPos through pending changes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")) + }); + view.dispatchEvent({ + type: "input" + }); + view.dispatch(view.state.tr.insertText("more text")); + expect(view.coordsAtPos(13)).toBeTruthy(); + }); + it("notices text added to a cursor wrapper at the start of a mark", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)((0, _prosemirrorTestBuilder.strong)((0, _prosemirrorTestBuilder.a)("foo"), "bar"))) + }); + view.focus(); + await browser.keys("xy"); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)((0, _prosemirrorTestBuilder.strong)((0, _prosemirrorTestBuilder.a)("foo"), "xybar")))); + }); + it("removes cursor wrapper text when the wrapper otherwise remains valid", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)((0, _prosemirrorTestBuilder.a)((0, _prosemirrorTestBuilder.strong)("foo"), "bar"))) + }); + view.focus(); + await browser.keys("q"); + expect(view.state.doc).toEqualNode((0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)((0, _prosemirrorTestBuilder.a)((0, _prosemirrorTestBuilder.strong)("fooq"), "bar")))); + const found = _react.screen.queryByText("\uFEFFq"); + expect(found).toBeNull(); + }); + it("creates a correct step for an ambiguous selection-deletion", async ()=>{ + let steps; + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("lalala")), + dispatchTransaction (tr) { + steps = tr.steps; + view.updateState(view.state.apply(tr)); + } + }); + view.input.lastKeyCode = 8; + view.input.lastKeyCodeTime = Date.now(); + view.focus(); + await browser.keys(_webdriverio.Key.Backspace); + expect(steps).toHaveLength(1); + expect(steps[0].from).toBe(3); + expect(steps[0].to).toBe(5); + }); + it("creates a step that covers the entire selection for partially-matching replacement", async ()=>{ + let steps; + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one two three")), + dispatchTransaction (tr) { + steps = tr.steps; + view.updateState(view.state.apply(tr)); + } + }); + view.focus(); + await browser.keys("t"); + expect(steps).toHaveLength(1); + expect(steps[0].from).toBe(5); + expect(steps[0].to).toBe(8); + expect(steps[0].slice.content.toString()).toBe('<"t">'); + view.dispatch(view.state.tr.setSelection(_prosemirrorState.TextSelection.create(view.state.doc, 7, 12))); + view.focus(); + await browser.keys("e"); + expect(steps).toHaveLength(1); + expect(steps[0].from).toBe(7); + expect(steps[0].to).toBe(12); + expect(steps[0].slice.content.toString()).toBe('<"e">'); + }); +}); diff --git a/dist/cjs/components/__tests__/ProseMirror.draw-decoration.test.js b/dist/cjs/components/__tests__/ProseMirror.draw-decoration.test.js new file mode 100644 index 00000000..abfc1902 --- /dev/null +++ b/dist/cjs/components/__tests__/ProseMirror.draw-decoration.test.js @@ -0,0 +1,1001 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ // TODO: figure out whether it's possible to support native +// widgets. Right now, I'm not sure how we'd do it without +// wrapping them in another element, which would re-introduce +// all of the issues we had before with node views. +// +// For now, we've updated the factory in this file to use +// our React widgets. +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _prosemirrorModel = require("prosemirror-model"); +const _prosemirrorState = require("prosemirror-state"); +const _prosemirrorTestBuilder = require("prosemirror-test-builder"); +const _prosemirrorView = require("prosemirror-view"); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _reactWidgetTypeJs = require("../../decorations/ReactWidgetType.js"); +const _useEditorEffectJs = require("../../hooks/useEditorEffect.js"); +const _editorViewTestHelpersJs = require("../../testing/editorViewTestHelpers.js"); +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +const Widget = /*#__PURE__*/ (0, _react.forwardRef)(function Widget(param, ref) { + let { widget , getPos , ...props } = param; + return /*#__PURE__*/ _react.default.createElement("button", _extends({ + ref: ref + }, props), "ω"); +}); +function make(str) { + if (typeof str != "string") return str; + const match = /^(\d+)(?:-(\d+))?-(.+)$/.exec(str); + if (match[3] == "widget") { + return (0, _reactWidgetTypeJs.widget)(+match[1], Widget, { + key: str + }); + } + return _prosemirrorView.Decoration.inline(+match[1], +match[2], { + class: match[3] + }); +} +function decoPlugin(decos) { + return new _prosemirrorState.Plugin({ + state: { + init (config) { + return config.doc ? _prosemirrorView.DecorationSet.create(config.doc, decos.map(make)) : _prosemirrorView.DecorationSet.empty; + }, + apply (tr, set, state) { + if (tr.docChanged) set = set.map(tr.mapping, tr.doc); + const 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", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foobar")), + plugins: [ + decoPlugin([ + "2-5-foo" + ]) + ] + }); + const found = view.dom.querySelector(".foo"); + expect(found).not.toBeNull(); + expect(found.textContent).toBe("oob"); + }); + it("draws wrapping decorations", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + plugins: [ + decoPlugin([ + _prosemirrorView.Decoration.inline(1, 5, { + nodeName: "i" + }) + ]) + ] + }); + const found = view.dom.querySelector("i"); + expect(found && found.innerHTML).toBe("foo"); + }); + it("draws node decorations", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.p)("bar")), + plugins: [ + decoPlugin([ + _prosemirrorView.Decoration.node(5, 10, { + class: "cls" + }) + ]) + ] + }); + const found = view.dom.querySelectorAll(".cls"); + expect(found).toHaveLength(1); + expect(found[0].nodeName).toBe("P"); + expect(found[0].previousSibling.nodeName).toBe("P"); + }); + it("can update multi-level wrapping decorations", async ()=>{ + const d2 = _prosemirrorView.Decoration.inline(1, 5, { + nodeName: "i", + class: "b" + }); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hello")), + plugins: [ + decoPlugin([ + _prosemirrorView.Decoration.inline(1, 5, { + nodeName: "i", + class: "a" + }), + d2 + ]) + ] + }); + expect(view.dom.querySelectorAll("i")).toHaveLength(2); + updateDeco(view, [ + _prosemirrorView.Decoration.inline(1, 5, { + nodeName: "i", + class: "c" + }) + ], [ + d2 + ]); + const iNodes = view.dom.querySelectorAll("i"); + expect(iNodes).toHaveLength(2); + expect(Array.prototype.map.call(iNodes, (n)=>n.className).sort().join()).toBe("a,c"); + }); + it("draws overlapping inline decorations", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abcdef")), + plugins: [ + decoPlugin([ + "3-5-foo", + "4-6-bar", + "1-7-baz" + ]) + ] + }); + const baz = view.dom.querySelectorAll(".baz"); + expect(baz).toHaveLength(5); + expect(Array.prototype.map.call(baz, (x)=>x.textContent).join("-")).toBe("ab-c-d-e-f"); + function classes(n) { + return n.className.split(" ").sort().join(" "); + } + expect(classes(baz[1])).toBe("baz foo"); + expect(classes(baz[2])).toBe("bar baz foo"); + expect(classes(baz[3])).toBe("bar baz"); + }); + it("draws multiple widgets", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foobar")), + plugins: [ + decoPlugin([ + "1-widget", + "4-widget", + "7-widget" + ]) + ] + }); + const found = view.dom.querySelectorAll("button"); + expect(found).toHaveLength(3); + expect(found[0].nextSibling.textContent).toBe("foo"); + expect(found[1].nextSibling.textContent).toBe("bar"); + expect(found[2].previousSibling.textContent).toBe("bar"); + }); + it("orders widgets by their side option", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foobar")), + plugins: [ + decoPlugin([ + (0, _reactWidgetTypeJs.widget)(4, /*#__PURE__*/ (0, _react.forwardRef)(function B(props, ref) { + return /*#__PURE__*/ _react.default.createElement("span", _extends({ + ref: ref + }, props), "B"); + }), { + key: "widget-b" + }), + (0, _reactWidgetTypeJs.widget)(4, /*#__PURE__*/ (0, _react.forwardRef)(function A(props, ref) { + return /*#__PURE__*/ _react.default.createElement("span", _extends({ + ref: ref + }, props), "A"); + }), { + side: -100, + key: "widget-a" + }), + (0, _reactWidgetTypeJs.widget)(4, /*#__PURE__*/ (0, _react.forwardRef)(function C(props, ref) { + return /*#__PURE__*/ _react.default.createElement("span", _extends({ + ref: ref + }, props), "C"); + }), { + side: 2, + key: "widget-c" + }) + ]) + ] + }); + expect(view.dom.textContent).toBe("fooABCbar"); + }); + it("draws a widget in an empty node", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()), + plugins: [ + decoPlugin([ + "1-widget" + ]) + ] + }); + expect(view.dom.querySelectorAll("button")).toHaveLength(1); + }); + it("draws widgets on node boundaries", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.em)("bar"))), + plugins: [ + decoPlugin([ + "4-widget" + ]) + ] + }); + expect(view.dom.querySelectorAll("button")).toHaveLength(1); + }); + it("draws decorations from multiple plugins", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.em)("bar"))), + plugins: [ + decoPlugin([ + "2-widget" + ]), + decoPlugin([ + "6-widget" + ]) + ] + }); + expect(view.dom.querySelectorAll("button")).toHaveLength(2); + }); + it("calls widget destroy methods", async ()=>{ + let destroyed = false; + const DestroyableWidget = /*#__PURE__*/ (0, _react.forwardRef)(function DestroyableWidget(props, ref) { + (0, _react.useEffect)(()=>{ + destroyed = true; + }); + return /*#__PURE__*/ _react.default.createElement("button", _extends({ + ref: ref + }, props)); + }); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abc")), + plugins: [ + decoPlugin([ + (0, _reactWidgetTypeJs.widget)(2, DestroyableWidget, { + key: "destroyable-widget" + }) + ]) + ] + }); + view.dispatch(view.state.tr.delete(1, 4)); + expect(destroyed).toBeTruthy(); + }); + it("draws inline decorations spanning multiple parents", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("long first ", (0, _prosemirrorTestBuilder.em)("p"), "aragraph"), (0, _prosemirrorTestBuilder.p)("two")), + plugins: [ + decoPlugin([ + "7-25-foo" + ]) + ] + }); + const foos = view.dom.querySelectorAll(".foo"); + expect(foos).toHaveLength(4); + expect(foos[0].textContent).toBe("irst "); + expect(foos[1].textContent).toBe("p"); + expect(foos[2].textContent).toBe("aragraph"); + expect(foos[3].textContent).toBe("tw"); + }); + it("draws inline decorations across empty paragraphs", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("first"), (0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)("second")), + plugins: [ + decoPlugin([ + "3-12-foo" + ]) + ] + }); + const foos = view.dom.querySelectorAll(".foo"); + expect(foos).toHaveLength(2); + expect(foos[0].textContent).toBe("rst"); + expect(foos[1].textContent).toBe("se"); + }); + it("can handle inline decorations ending at the start or end of a node", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)()), + plugins: [ + decoPlugin([ + "1-3-foo" + ]) + ] + }); + expect(view.dom.querySelector(".foo")).toBeNull(); + }); + it("can draw decorations with multiple classes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + plugins: [ + decoPlugin([ + "1-4-foo bar" + ]) + ] + }); + expect(view.dom.querySelectorAll(".foo")).toHaveLength(1); + expect(view.dom.querySelectorAll(".bar")).toHaveLength(1); + }); + it("supports overlapping inline decorations", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foobar")), + plugins: [ + decoPlugin([ + "1-3-foo", + "2-5-bar" + ]) + ] + }); + const foos = view.dom.querySelectorAll(".foo"); + const bars = view.dom.querySelectorAll(".bar"); + expect(foos).toHaveLength(2); + expect(bars).toHaveLength(2); + expect(foos[0].textContent).toBe("f"); + expect(foos[1].textContent).toBe("o"); + expect(bars[0].textContent).toBe("o"); + expect(bars[1].textContent).toBe("ob"); + }); + it("doesn't redraw when irrelevant decorations change", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.p)("baz")), + plugins: [ + decoPlugin([ + "7-8-foo" + ]) + ] + }); + const para2 = view.dom.lastChild; + updateDeco(view, [ + make("2-3-bar") + ]); + expect(view.dom.lastChild).toBe(para2); + expect(view.dom.querySelector(".bar")).not.toBeNull(); + }); + it("doesn't redraw when irrelevant content changes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.p)("baz")), + plugins: [ + decoPlugin([ + "7-8-foo" + ]) + ] + }); + const para2 = view.dom.lastChild; + view.dispatch(view.state.tr.delete(2, 3)); + view.dispatch(view.state.tr.delete(2, 3)); + expect(view.dom.lastChild).toBe(para2); + }); + it("can add a widget on a node boundary", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.em)("bar"))), + plugins: [ + decoPlugin([]) + ] + }); + updateDeco(view, [ + make("4-widget") + ]); + expect(view.dom.querySelectorAll("button")).toHaveLength(1); + }); + it("can remove a widget on a node boundary", async ()=>{ + const dec = make("4-widget"); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.em)("bar"))), + plugins: [ + decoPlugin([ + dec + ]) + ] + }); + updateDeco(view, null, [ + dec + ]); + expect(view.dom.querySelector("button")).toBeNull(); + }); + it("can remove the class from a text node", async ()=>{ + const dec = make("1-4-foo"); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abc")), + plugins: [ + decoPlugin([ + dec + ]) + ] + }); + expect(view.dom.querySelector(".foo")).not.toBeNull(); + updateDeco(view, null, [ + dec + ]); + expect(view.dom.querySelector(".foo")).toBeNull(); + }); + it("can remove the class from part of a text node", async ()=>{ + const dec = make("2-4-foo"); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abcd")), + plugins: [ + decoPlugin([ + dec + ]) + ] + }); + expect(view.dom.querySelector(".foo")).not.toBeNull(); + updateDeco(view, null, [ + dec + ]); + expect(view.dom.querySelector(".foo")).toBeNull(); + expect(view.dom.firstChild.innerHTML).toBe("abcd"); + }); + it("can change the class for part of a text node", async ()=>{ + const dec = make("2-4-foo"); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abcd")), + plugins: [ + decoPlugin([ + dec + ]) + ] + }); + expect(view.dom.querySelector(".foo")).not.toBeNull(); + updateDeco(view, [ + make("2-4-bar") + ], [ + dec + ]); + expect(view.dom.querySelector(".foo")).toBeNull(); + expect(view.dom.querySelector(".bar")).not.toBeNull(); + }); + it("draws a widget added in the middle of a text node", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + plugins: [ + decoPlugin([]) + ] + }); + updateDeco(view, [ + make("3-widget") + ]); + expect(view.dom.firstChild.textContent).toBe("foωo"); + }); + it("can update a text node around a widget", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("bar")), + plugins: [ + decoPlugin([ + "3-widget" + ]) + ] + }); + view.dispatch(view.state.tr.delete(1, 2)); + expect(view.dom.querySelectorAll("button")).toHaveLength(1); + expect(view.dom.firstChild.textContent).toBe("aωr"); + }); + it("can update a text node with an inline decoration", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("bar")), + plugins: [ + decoPlugin([ + "1-3-foo" + ]) + ] + }); + view.dispatch(view.state.tr.delete(1, 2)); + const foo = view.dom.querySelector(".foo"); + expect(foo).not.toBeNull(); + expect(foo.textContent).toBe("a"); + expect(foo.nextSibling.textContent).toBe("r"); + }); + it("correctly redraws a partially decorated node when a widget is added", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one", (0, _prosemirrorTestBuilder.em)("two"))), + plugins: [ + decoPlugin([ + "1-6-foo" + ]) + ] + }); + updateDeco(view, [ + make("6-widget") + ]); + const foos = view.dom.querySelectorAll(".foo"); + expect(foos).toHaveLength(2); + expect(foos[0].textContent).toBe("one"); + expect(foos[1].textContent).toBe("tw"); + }); + it("correctly redraws when skipping split text node", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + plugins: [ + decoPlugin([ + "3-widget", + "3-4-foo" + ]) + ] + }); + updateDeco(view, [ + make("4-widget") + ]); + expect(view.dom.querySelectorAll("button")).toHaveLength(2); + }); + it("drops removed node decorations from the view", async ()=>{ + const deco = _prosemirrorView.Decoration.node(1, 6, { + class: "cls" + }); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.blockquote)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.p)("bar"))), + plugins: [ + decoPlugin([ + deco + ]) + ] + }); + updateDeco(view, null, [ + deco + ]); + expect(view.dom.querySelector(".cls")).toBeNull(); + }); + it("can update a node's attributes without replacing the node", async ()=>{ + const deco = _prosemirrorView.Decoration.node(0, 5, { + title: "title", + class: "foo" + }); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + plugins: [ + decoPlugin([ + deco + ]) + ] + }); + const para = view.dom.querySelector("p"); + updateDeco(view, [ + _prosemirrorView.Decoration.node(0, 5, { + class: "foo bar" + }) + ], [ + deco + ]); + expect(view.dom.querySelector("p")).toBe(para); + expect(para.className).toBe("foo bar"); + expect(para.title).toBeFalsy(); + }); + it("can add and remove CSS custom properties from a node", async ()=>{ + const deco = _prosemirrorView.Decoration.node(0, 5, { + style: "--my-custom-property:36px" + }); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + plugins: [ + decoPlugin([ + deco + ]) + ] + }); + expect(view.dom.querySelector("p").style.getPropertyValue("--my-custom-property")).toBe("36px"); + updateDeco(view, null, [ + deco + ]); + expect(view.dom.querySelector("p").style.getPropertyValue("--my-custom-property")).toBe(""); + }); + it("updates decorated nodes even if a widget is added before them", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("a"), (0, _prosemirrorTestBuilder.p)("b")), + plugins: [ + decoPlugin([]) + ] + }); + const lastP = view.dom.querySelectorAll("p")[1]; + updateDeco(view, [ + make("3-widget"), + _prosemirrorView.Decoration.node(3, 6, { + style: "color: red" + }) + ]); + expect(lastP.style.color).toBe("red"); + }); + it("doesn't redraw nodes when a widget before them is replaced", async ()=>{ + const w0 = make("3-widget"); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.h1)("a"), (0, _prosemirrorTestBuilder.p)("b")), + plugins: [ + decoPlugin([ + w0 + ]) + ] + }); + const initialP = view.dom.querySelector("p"); + view.dispatch(view.state.tr.setMeta("updateDecorations", { + add: [ + make("3-widget") + ], + remove: [ + w0 + ] + }).insertText("c", 5)); + expect(view.dom.querySelector("p")).toBe(initialP); + }); + it("can add and remove inline style", async ()=>{ + const deco = _prosemirrorView.Decoration.inline(1, 6, { + style: "color: rgba(0,10,200,.4); text-decoration: underline" + }); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("al", (0, _prosemirrorTestBuilder.img)(), "lo")), + plugins: [ + decoPlugin([ + deco + ]) + ] + }); + expect(view.dom.querySelector("img").style.color).toMatch(/rgba/); + expect(view.dom.querySelector("img").previousSibling.style.textDecoration).toBe("underline"); + updateDeco(view, null, [ + deco + ]); + expect(view.dom.querySelector("img").style.color).toBe(""); + expect(view.dom.querySelector("img").style.textDecoration).toBe(""); + }); + it("passes decorations to a node view", async ()=>{ + let current = ""; + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.hr)()), + plugins: [ + decoPlugin([]) + ], + nodeViews: { + horizontal_rule: /*#__PURE__*/ (0, _react.forwardRef)(function HR(param, ref) { + let { nodeProps , children , ...props } = param; + current = nodeProps.decorations.map((d)=>d.spec.name).join(); + return /*#__PURE__*/ _react.default.createElement("hr", _extends({ + ref: ref + }, props)); + }) + } + }); + const a = _prosemirrorView.Decoration.node(5, 6, {}, { + name: "a" + }); + updateDeco(view, [ + a + ], []); + expect(current).toBe("a"); + updateDeco(view, [ + _prosemirrorView.Decoration.node(5, 6, {}, { + name: "b" + }), + _prosemirrorView.Decoration.node(5, 6, {}, { + name: "c" + }) + ], [ + a + ]); + expect(current).toBe("b,c"); + }); + it("draws the specified marks around a widget", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foobar")), + plugins: [ + decoPlugin([ + (0, _reactWidgetTypeJs.widget)(4, /*#__PURE__*/ (0, _react.forwardRef)(function Img(props, ref) { + return /*#__PURE__*/ _react.default.createElement("img", _extends({}, props, { + ref: ref + })); + }), { + marks: [ + _prosemirrorTestBuilder.schema.mark("em") + ], + key: "img-widget" + }) + ]) + ] + }); + expect(view.dom.querySelector("em img")).not.toBeNull(); + }); + it("draws widgets inside the marks for their side", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)((0, _prosemirrorTestBuilder.em)("foo"), (0, _prosemirrorTestBuilder.strong)("bar"))), + plugins: [ + decoPlugin([ + (0, _reactWidgetTypeJs.widget)(4, /*#__PURE__*/ (0, _react.forwardRef)(function Img(props, ref) { + return /*#__PURE__*/ _react.default.createElement("img", _extends({}, props, { + ref: ref + })); + }), { + side: -1, + key: "img-widget" + }) + ]), + decoPlugin([ + (0, _reactWidgetTypeJs.widget)(4, /*#__PURE__*/ (0, _react.forwardRef)(function BR(props, ref) { + return /*#__PURE__*/ _react.default.createElement("br", _extends({}, props, { + ref: ref + })); + }), { + key: "br-widget" + }) + ]), + decoPlugin([ + (0, _reactWidgetTypeJs.widget)(7, /*#__PURE__*/ (0, _react.forwardRef)(function Span(param, ref) { + let { widget , getPos , ...props } = param; + return /*#__PURE__*/ _react.default.createElement("span", _extends({}, props, { + ref: ref + })); + }), { + side: 1, + key: "span-widget" + }) + ]) + ] + }); + expect(view.dom.querySelector("em img")).not.toBeNull(); + expect(view.dom.querySelector("strong img")).toBeNull(); + expect(view.dom.querySelector("strong br")).not.toBeNull(); + expect(view.dom.querySelector("em br")).toBeNull(); + expect(view.dom.querySelector("strong span")).toBeNull(); + }); + it("draws decorations inside node views", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ (0, _react.forwardRef)(function Paragraph(param, ref) { + let { nodeProps , children , ...props } = param; + return /*#__PURE__*/ _react.default.createElement("p", _extends({ + ref: ref + }, props), children); + }) + }, + plugins: [ + decoPlugin([ + (0, _reactWidgetTypeJs.widget)(2, /*#__PURE__*/ (0, _react.forwardRef)(function Img(props, ref) { + return /*#__PURE__*/ _react.default.createElement("img", _extends({}, props, { + ref: ref + })); + }), { + key: "img-widget" + }) + ]) + ] + }); + expect(view.dom.querySelector("img")).not.toBeNull(); + }); + it("can delay widget drawing to render time", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hi")), + decorations (state) { + return _prosemirrorView.DecorationSet.create(state.doc, [ + (0, _reactWidgetTypeJs.widget)(3, /*#__PURE__*/ (0, _react.forwardRef)(function Span(props, ref) { + (0, _useEditorEffectJs.useEditorEffect)((view)=>{ + expect(view.state).toBe(state); + }); + return /*#__PURE__*/ _react.default.createElement("span", _extends({}, props, { + ref: ref + }), "!"); + }), { + key: "span-widget" + }) + ]); + } + }); + expect(view.dom.textContent).toBe("hi!"); + }); + it("supports widgets querying their own position", async ()=>{ + (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hi")), + decorations (state) { + return _prosemirrorView.DecorationSet.create(state.doc, [ + (0, _reactWidgetTypeJs.widget)(3, /*#__PURE__*/ (0, _react.forwardRef)(function Widget(param, ref) { + let { getPos , ...props } = param; + expect(getPos()).toBe(3); + return /*#__PURE__*/ _react.default.createElement("button", _extends({ + ref: ref + }, props), "ω"); + }), { + key: "button-widget" + }) + ]); + } + }); + }); + it("doesn't redraw widgets with matching keys", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hi")), + decorations (state) { + return _prosemirrorView.DecorationSet.create(state.doc, [ + (0, _reactWidgetTypeJs.widget)(2, Widget, { + key: "myButton" + }) + ]); + } + }); + const widgetDOM = view.dom.querySelector("button"); + view.dispatch(view.state.tr.insertText("!", 2, 2)); + expect(view.dom.querySelector("button")).toBe(widgetDOM); + }); + it("doesn't redraw widgets with identical specs", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hi")), + decorations (state) { + return _prosemirrorView.DecorationSet.create(state.doc, [ + (0, _reactWidgetTypeJs.widget)(2, Widget, { + side: 1, + key: "widget" + }) + ]); + } + }); + const widgetDOM = view.dom.querySelector("button"); + view.dispatch(view.state.tr.insertText("!", 2, 2)); + expect(view.dom.querySelector("button")).toBe(widgetDOM); + }); + it("doesn't get confused by split text nodes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abab")), + decorations (state) { + return state.selection.from <= 1 ? null : _prosemirrorView.DecorationSet.create(view.state.doc, [ + _prosemirrorView.Decoration.inline(1, 2, { + class: "foo" + }), + _prosemirrorView.Decoration.inline(3, 4, { + class: "foo" + }) + ]); + } + }); + view.dispatch(view.state.tr.setSelection(_prosemirrorState.TextSelection.create(view.state.doc, 5))); + expect(view.dom.textContent).toBe("abab"); + }); + it("only draws inline decorations on the innermost level", async ()=>{ + const s = new _prosemirrorModel.Schema({ + nodes: { + doc: { + content: "(text | thing)*" + }, + text: {}, + thing: { + inline: true, + content: "text*", + toDOM: ()=>[ + "strong", + 0 + ], + parseDOM: [ + { + tag: "strong" + } + ] + } + } + }); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: s.node("doc", null, [ + s.text("abc"), + s.node("thing", null, [ + s.text("def") + ]), + s.text("ghi") + ]), + decorations: (s)=>_prosemirrorView.DecorationSet.create(s.doc, [ + _prosemirrorView.Decoration.inline(1, 10, { + class: "dec" + }) + ]) + }); + const styled = view.dom.querySelectorAll(".dec"); + expect(styled).toHaveLength(3); + expect(Array.prototype.map.call(styled, (n)=>n.textContent).join()).toBe("bc,def,gh"); + expect(styled[1].parentNode.nodeName).toBe("STRONG"); + }); + it("can handle nodeName decoration overlapping with classes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one two three")), + plugins: [ + decoPlugin([ + _prosemirrorView.Decoration.inline(2, 13, { + class: "foo" + }), + _prosemirrorView.Decoration.inline(5, 8, { + nodeName: "em" + }) + ]) + ] + }); + expect(view.dom.firstChild.innerHTML).toBe('one two three'); + }); + it("can handle combining decorations from parent editors in child editors", async ()=>{ + let decosFromFirstEditor; + let { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one two three")), + plugins: [ + decoPlugin([ + _prosemirrorView.Decoration.inline(2, 13, { + class: "foo" + }) + ]), + decoPlugin([ + _prosemirrorView.Decoration.inline(2, 13, { + class: "bar" + }) + ]) + ], + nodeViews: { + paragraph: /*#__PURE__*/ (0, _react.forwardRef)(function Paragraph(param, ref) { + let { nodeProps , children , ...props } = param; + decosFromFirstEditor = nodeProps.innerDecorations; + return /*#__PURE__*/ _react.default.createElement("p", _extends({ + ref: ref + }, props), children); + }) + } + }); + ({ view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one two three")), + plugins: [ + decoPlugin([ + _prosemirrorView.Decoration.inline(1, 12, { + class: "baz" + }) + ]) + ], + decorations: ()=>decosFromFirstEditor + })); + expect(view.dom.querySelectorAll(".foo")).toHaveLength(1); + expect(view.dom.querySelectorAll(".bar")).toHaveLength(1); + expect(view.dom.querySelectorAll(".baz")).toHaveLength(1); + }); +}); diff --git a/dist/cjs/components/__tests__/ProseMirror.draw.test.js b/dist/cjs/components/__tests__/ProseMirror.draw.test.js new file mode 100644 index 00000000..7b717732 --- /dev/null +++ b/dist/cjs/components/__tests__/ProseMirror.draw.test.js @@ -0,0 +1,326 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _prosemirrorModel = require("prosemirror-model"); +const _prosemirrorState = require("prosemirror-state"); +const _prosemirrorTestBuilder = require("prosemirror-test-builder"); +const _react = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _editorViewTestHelpersJs = require("../../testing/editorViewTestHelpers.js"); +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +describe("EditorView draw", ()=>{ + it("updates the DOM", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")) + }); + view.dispatch(view.state.tr.insertText("bar")); + expect(view.dom.textContent).toBe("barfoo"); + }); + it("doesn't redraw nodes after changes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.h1)("foo"), (0, _prosemirrorTestBuilder.p)("bar")) + }); + const oldP = view.dom.querySelector("p"); + view.dispatch(view.state.tr.insertText("!")); + expect(view.dom.querySelector("p")).toBe(oldP); + }); + it("doesn't redraw nodes before changes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.h1)("bar")) + }); + const oldP = view.dom.querySelector("p"); + view.dispatch(view.state.tr.insertText("!", 2)); + expect(view.dom.querySelector("p")).toBe(oldP); + }); + it("doesn't redraw nodes between changes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.h1)("bar"), (0, _prosemirrorTestBuilder.pre)("baz")) + }); + const oldP = view.dom.querySelector("p"); + const oldPre = view.dom.querySelector("pre"); + view.dispatch(view.state.tr.insertText("!", 2)); + expect(view.dom.querySelector("p")).toBe(oldP); + expect(view.dom.querySelector("pre")).toBe(oldPre); + }); + it("doesn't redraw siblings of a split node", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.h1)("bar"), (0, _prosemirrorTestBuilder.pre)("baz")) + }); + const oldP = view.dom.querySelector("p"); + const oldPre = view.dom.querySelector("pre"); + view.dispatch(view.state.tr.split(8)); + expect(view.dom.querySelector("p")).toBe(oldP); + expect(view.dom.querySelector("pre")).toBe(oldPre); + }); + it("doesn't redraw siblings of a joined node", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.h1)("bar"), (0, _prosemirrorTestBuilder.h1)("x"), (0, _prosemirrorTestBuilder.pre)("baz")) + }); + const oldP = view.dom.querySelector("p"); + const oldPre = view.dom.querySelector("pre"); + view.dispatch(view.state.tr.join(10)); + expect(view.dom.querySelector("p")).toBe(oldP); + expect(view.dom.querySelector("pre")).toBe(oldPre); + }); + it("doesn't redraw after a big deletion", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.h1)("!"), (0, _prosemirrorTestBuilder.p)(), (0, _prosemirrorTestBuilder.p)()) + }); + const oldH = view.dom.querySelector("h1"); + view.dispatch(view.state.tr.delete(2, 14)); + expect(view.dom.querySelector("h1")).toBe(oldH); + }); + it("adds classes from the attributes prop", async ()=>{ + const { view , rerender } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()), + attributes: { + class: "foo bar" + } + }); + expect(view.dom.classList.contains("foo")).toBeTruthy(); + expect(view.dom.classList.contains("bar")).toBeTruthy(); + expect(view.dom.classList.contains("ProseMirror")).toBeTruthy(); + rerender({ + attributes: { + class: "baz" + } + }); + expect(!view.dom.classList.contains("foo")).toBeTruthy(); + expect(view.dom.classList.contains("baz")).toBeTruthy(); + }); + it("adds style from the attributes prop", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()), + attributes: { + style: "border: 1px solid red;" + }, + plugins: [ + new _prosemirrorState.Plugin({ + props: { + attributes: { + style: "background: red;" + } + } + }), + new _prosemirrorState.Plugin({ + props: { + attributes: { + style: "color: red;" + } + } + }) + ] + }); + expect(view.dom.style.border).toBe("1px solid red"); + expect(view.dom.style.backgroundColor).toBe("red"); + expect(view.dom.style.color).toBe("red"); + }); + it("can set other attributes", async ()=>{ + const { view , rerender } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()), + attributes: { + spellcheck: "false", + "aria-label": "hello" + } + }); + expect(view.dom.spellcheck).toBe(false); + expect(view.dom.getAttribute("aria-label")).toBe("hello"); + rerender({ + attributes: { + style: "background-color: yellow" + } + }); + expect(view.dom.hasAttribute("aria-label")).toBe(false); + expect(view.dom.style.backgroundColor).toBe("yellow"); + }); + it("can't set the contenteditable attribute", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()), + attributes: { + contenteditable: "false" + } + }); + expect(view.dom.contentEditable).toBe("true"); + }); + it("understands the editable prop", async ()=>{ + const { view , rerender } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()), + editable: ()=>false + }); + expect(view.dom.contentEditable).toBe("false"); + rerender({ + editable: ()=>true + }); + expect(view.dom.contentEditable).toBe("true"); + }); + it("doesn't redraw following paragraphs when a paragraph is split", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("abcde"), (0, _prosemirrorTestBuilder.p)("fg")) + }); + const lastPara = view.dom.lastChild; + view.dispatch(view.state.tr.split(3)); + expect(view.dom.lastChild).toBe(lastPara); + }); + it("doesn't greedily match nodes that have another match", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("a"), (0, _prosemirrorTestBuilder.p)("b"), (0, _prosemirrorTestBuilder.p)()) + }); + const secondPara = view.dom.querySelectorAll("p")[1]; + view.dispatch(view.state.tr.split(2)); + expect(view.dom.querySelectorAll("p")[2]).toBe(secondPara); + }); + it("creates and destroys plugin views", async ()=>{ + const events = []; + let PluginView = class PluginView { + update() { + events.push("update"); + } + destroy() { + events.push("destroy"); + } + }; + const plugin = new _prosemirrorState.Plugin({ + view () { + events.push("create"); + return new PluginView(); + } + }); + const { view , unmount } = (0, _editorViewTestHelpersJs.tempEditor)({ + plugins: [ + plugin + ] + }); + view.dispatch(view.state.tr.insertText("u")); + unmount(); + expect(events.join(" ")).toBe("create update destroy"); + }); + it("redraws changed node views", async ()=>{ + const { view , rerender } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.hr)()) + }); + expect(view.dom.querySelector("hr")).toBeTruthy(); + rerender({ + nodeViews: { + horizontal_rule: /*#__PURE__*/ (0, _react.forwardRef)(function HorizontalRule(param, ref) { + let { nodeProps , ...props } = param; + return /*#__PURE__*/ _react.default.createElement("var", _extends({ + ref: ref + }, props)); + }) + } + }); + expect(!view.dom.querySelector("hr")).toBeTruthy(); + expect(view.dom.querySelector("var")).toBeTruthy(); + }); + it("doesn't get confused by merged nodes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)((0, _prosemirrorTestBuilder.strong)("one"), " two ", (0, _prosemirrorTestBuilder.strong)("three"))) + }); + view.dispatch(view.state.tr.removeMark(1, 4, _prosemirrorTestBuilder.schema.marks.strong)); + expect(view.dom.querySelectorAll("strong")).toHaveLength(1); + }); + it("doesn't redraw too much when marks are present", async ()=>{ + const s = new _prosemirrorModel.Schema({ + nodes: { + doc: { + content: "paragraph+", + marks: "m" + }, + text: { + group: "inline" + }, + paragraph: _prosemirrorTestBuilder.schema.spec.nodes.get("paragraph") + }, + marks: { + m: { + toDOM: ()=>[ + "div", + { + class: "m" + }, + 0 + ], + parseDOM: [ + { + tag: "div.m" + } + ] + } + } + }); + const paragraphs = []; + for(let i = 1; i <= 10; i++)paragraphs.push(s.node("paragraph", null, [ + s.text("para " + i) + ], [ + s.mark("m") + ])); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + // @ts-expect-error this is fine + doc: s.node("doc", null, paragraphs) + }); + const initialChildren = Array.from(view.dom.querySelectorAll("p")); + const 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)); + const currentChildren = Array.from(view.dom.querySelectorAll("p")); + let sameAtEnd = 0; + while(sameAtEnd < currentChildren.length && sameAtEnd < initialChildren.length && currentChildren[currentChildren.length - sameAtEnd - 1] == initialChildren[initialChildren.length - sameAtEnd - 1])sameAtEnd++; + expect(sameAtEnd).toBe(9); + }); +}); diff --git a/dist/cjs/components/__tests__/ProseMirror.node-view.test.js b/dist/cjs/components/__tests__/ProseMirror.node-view.test.js new file mode 100644 index 00000000..25b13d9f --- /dev/null +++ b/dist/cjs/components/__tests__/ProseMirror.node-view.test.js @@ -0,0 +1,315 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _react = require("@testing-library/react"); +const _prosemirrorState = require("prosemirror-state"); +const _prosemirrorTestBuilder = require("prosemirror-test-builder"); +const _prosemirrorView = require("prosemirror-view"); +const _react1 = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _useEditorStateJs = require("../../hooks/useEditorState.js"); +const _useStopEventJs = require("../../hooks/useStopEvent.js"); +const _editorViewTestHelpersJs = require("../../testing/editorViewTestHelpers.js"); +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +describe("nodeViews prop", ()=>{ + it("can replace a node's representation", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.br)())), + nodeViews: { + hard_break: /*#__PURE__*/ (0, _react1.forwardRef)(function Var(props, ref) { + return /*#__PURE__*/ _react1.default.createElement("var", { + ref: ref + }, props.children); + }) + } + }); + expect(view.dom.querySelector("var")).not.toBeNull(); + }); + it("can override drawing of a node's content", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ (0, _react1.forwardRef)(function Paragraph(props, ref) { + return /*#__PURE__*/ _react1.default.createElement("p", { + ref: ref + }, props.nodeProps.node.textContent.toUpperCase()); + }) + } + }); + expect(view.dom.querySelector("p").textContent).toBe("FOO"); + view.dispatch(view.state.tr.insertText("a")); + expect(view.dom.querySelector("p").textContent).toBe("AFOO"); + }); + // React makes this more or less trivial; the render + // method of the component _is_ the update (and create) + // method + // eslint-disable-next-line jest/no-disabled-tests + it.skip("can register its own update method", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ (0, _react1.forwardRef)(function Paragraph(props, ref) { + return /*#__PURE__*/ _react1.default.createElement("p", { + ref: ref + }, props.nodeProps.node.textContent.toUpperCase()); + }) + } + }); + const para = view.dom.querySelector("p"); + view.dispatch(view.state.tr.insertText("a")); + expect(view.dom.querySelector("p")).toBe(para); + expect(para.textContent).toBe("AFOO"); + }); + it("allows decoration updates for node views with an update method", async ()=>{ + const { view , rerender } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ (0, _react1.forwardRef)(function Paragraph(param, ref) { + let { children , nodeProps , ...props } = param; + return /*#__PURE__*/ _react1.default.createElement("p", _extends({ + ref: ref + }, props), children); + }) + } + }); + rerender({ + decorations (state) { + return _prosemirrorView.DecorationSet.create(state.doc, [ + _prosemirrorView.Decoration.inline(2, 3, { + someattr: "ok" + }), + _prosemirrorView.Decoration.node(0, 5, { + otherattr: "ok" + }) + ]); + } + }); + expect(view.dom.querySelector("[someattr]")).not.toBeNull(); + expect(view.dom.querySelector("[otherattr]")).not.toBeNull(); + }); + it("can provide a contentDOM property", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ (0, _react1.forwardRef)(function Paragraph(props, ref) { + return(// ContentDOM is inferred from where props.children is rendered + /*#__PURE__*/ _react1.default.createElement("p", { + ref: ref + }, props.children)); + }) + } + }); + const para = view.dom.querySelector("p"); + view.dispatch(view.state.tr.insertText("a")); + expect(view.dom.querySelector("p")).toBe(para); + expect(para.textContent).toBe("afoo"); + }); + it("has its destroy method called", async ()=>{ + let destroyed = false; + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.br)())), + nodeViews: { + hard_break: /*#__PURE__*/ (0, _react1.forwardRef)(function BR(_props, ref) { + // React implements "destroy methods" with effect + // hooks + (0, _react1.useEffect)(()=>{ + return ()=>{ + destroyed = true; + }; + }, []); + return /*#__PURE__*/ _react1.default.createElement("br", { + ref: ref + }); + }) + } + }); + expect(destroyed).toBeFalsy(); + view.dispatch(view.state.tr.delete(3, 5)); + expect(destroyed).toBeTruthy(); + }); + it("can query its own position", async ()=>{ + let pos; + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.blockquote)((0, _prosemirrorTestBuilder.p)("abc"), (0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.br)()))), + nodeViews: { + hard_break: /*#__PURE__*/ (0, _react1.forwardRef)(function BR(param, ref) { + let { nodeProps , children , ...props } = param; + // trigger a re-render on every updated, otherwise we won't + // re-render when an updated doesn't directly affect us + (0, _useEditorStateJs.useEditorState)(); + pos = nodeProps.getPos(); + return /*#__PURE__*/ _react1.default.createElement("br", _extends({ + ref: ref + }, props)); + }) + } + }); + expect(pos).toBe(10); + view.dispatch(view.state.tr.insertText("a")); + expect(pos).toBe(11); + }); + it("has access to outer decorations", async ()=>{ + const plugin = new _prosemirrorState.Plugin({ + state: { + init () { + return null; + }, + apply (tr, prev) { + return tr.getMeta("setDeco") || prev; + } + }, + props: { + decorations (state) { + const deco = this.getState(state); + return deco && _prosemirrorView.DecorationSet.create(state.doc, [ + _prosemirrorView.Decoration.inline(0, state.doc.content.size, {}, { + name: deco + }) + ]); + } + } + }); + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", (0, _prosemirrorTestBuilder.br)())), + plugins: [ + plugin + ], + nodeViews: { + hard_break: /*#__PURE__*/ (0, _react1.forwardRef)(function Var(props, ref) { + return /*#__PURE__*/ _react1.default.createElement("var", { + ref: ref + }, props.nodeProps.decorations.length ? props.nodeProps.decorations[0].spec.name : "[]"); + }) + } + }); + expect(view.dom.querySelector("var").textContent).toBe("[]"); + view.dispatch(view.state.tr.setMeta("setDeco", "foo")); + expect(view.dom.querySelector("var").textContent).toBe("foo"); + view.dispatch(view.state.tr.setMeta("setDeco", "bar")); + expect(view.dom.querySelector("var").textContent).toBe("bar"); + }); + it("provides access to inner decorations in the constructor", async ()=>{ + (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ (0, _react1.forwardRef)(function Paragraph(props, ref) { + expect(props.nodeProps.innerDecorations.find().map((d)=>`${d.from}-${d.to}`).join()).toBe("1-2"); + return /*#__PURE__*/ _react1.default.createElement("p", { + ref: ref + }, props.children); + }) + }, + decorations (state) { + return _prosemirrorView.DecorationSet.create(state.doc, [ + _prosemirrorView.Decoration.inline(2, 3, { + someattr: "ok" + }), + _prosemirrorView.Decoration.node(0, 5, { + otherattr: "ok" + }) + ]); + } + }); + }); + it("provides access to inner decorations in the update method", async ()=>{ + let innerDecos = []; + const { rerender } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ (0, _react1.forwardRef)(function Paragraph(props, ref) { + innerDecos = props.nodeProps.innerDecorations.find().map((d)=>`${d.from}-${d.to}`); + return /*#__PURE__*/ _react1.default.createElement("p", { + ref: ref + }, props.children); + }) + } + }); + rerender({ + decorations (state) { + return _prosemirrorView.DecorationSet.create(state.doc, [ + _prosemirrorView.Decoration.inline(2, 3, { + someattr: "ok" + }), + _prosemirrorView.Decoration.node(0, 5, { + otherattr: "ok" + }) + ]); + } + }); + expect(innerDecos.join()).toBe("1-2"); + }); + it("can provide a stopEvent hook", async ()=>{ + (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("input value")), + nodeViews: { + paragraph: /*#__PURE__*/ (0, _react1.forwardRef)(function ParagraphInput(param, ref) { + let { nodeProps , children , ...props } = param; + (0, _useStopEventJs.useStopEvent)(()=>{ + return true; + }); + return /*#__PURE__*/ _react1.default.createElement("input", _extends({ + ref: ref, + type: "text", + defaultValue: nodeProps.node.textContent + }, props)); + }) + } + }); + const input = _react.screen.getByDisplayValue("input value"); + input.focus(); + await browser.keys("z"); + expect(await $(input).getValue()).toBe("input valuez"); + }); +}); diff --git a/dist/cjs/components/__tests__/ProseMirror.selection.test.js b/dist/cjs/components/__tests__/ProseMirror.selection.test.js new file mode 100644 index 00000000..e4637aa7 --- /dev/null +++ b/dist/cjs/components/__tests__/ProseMirror.selection.test.js @@ -0,0 +1,444 @@ +/* eslint-disable jest/no-disabled-tests */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _react = require("@testing-library/react"); +const _prosemirrorState = require("prosemirror-state"); +const _prosemirrorTestBuilder = require("prosemirror-test-builder"); +const _prosemirrorView = require("prosemirror-view"); +const _editorViewTestHelpersJs = require("../../testing/editorViewTestHelpers.js"); +const img = (0, _prosemirrorTestBuilder.img)({ + src: "data:image/gif;base64,R0lGODlhBQAFAIABAAAAAP///yH5BAEKAAEALAAAAAAFAAUAAAIEjI+pWAA7" +}); +async function findTextNode(_, text) { + const parent = await _react.screen.findByText(text); + return parent.firstChild; +} +function allPositions(doc) { + const 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) { + const range = document.createRange(); + range.setEnd(node, offset); + range.setStart(node, offset); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); +} +function getSel() { + const sel = window.getSelection(); + let node = sel.focusNode, offset = sel.focusOffset; + while(node && node.nodeType != 3){ + const after = offset < node.childNodes.length && node.childNodes[offset]; + const 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) { + const selection = typeof sel == "number" ? _prosemirrorState.Selection.near(view.state.doc.resolve(sel)) : sel; + (0, _react.act)(()=>{ + view.dispatch(view.state.tr.setSelection(selection)); + }); +} +function event(code) { + const event = document.createEvent("Event"); + event.initEvent("keydown", true, true); + event.keyCode = code; + return event; +} +const LEFT = 37, RIGHT = 39, UP = 38, DOWN = 40; +describe("ProseMirror", ()=>{ + it("can read the DOM selection", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one"), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.blockquote)((0, _prosemirrorTestBuilder.p)("two"))) + }); + function test(node, offset, expected) { + setDOMSel(node, offset); + view.dom.focus(); + (0, _react.act)(()=>{ + view.domObserver.flush(); + }); + const sel = view.state.selection; + expect(sel.head == null ? sel.from : sel.head).toBe(expected); + } + const one = await findTextNode(view.dom, "one"); + const two = await 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", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one"), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.blockquote)((0, _prosemirrorTestBuilder.p)("two"))) + }); + function test(pos, node, offset) { + setSel(view, pos); + const sel = getSel(); + expect(sel.node).toBe(node); + expect(sel.offset).toBe(offset); + } + const one = await findTextNode(view.dom, "one"); + const two = await 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", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one"), (0, _prosemirrorTestBuilder.p)("two")) + }); + const p00 = view.coordsAtPos(1); + const p01 = view.coordsAtPos(2); + const p03 = view.coordsAtPos(4); + const p10 = view.coordsAtPos(6); + const p13 = view.coordsAtPos(9); + expect(p00.bottom).toBeGreaterThan(p00.top); + expect(p13.bottom).toBeGreaterThan(p13.top); + expect(p00.top).toEqual(p01.top); + expect(p01.top).toEqual(p03.top); + expect(p00.bottom).toEqual(p03.bottom); + expect(p10.top).toEqual(p13.top); + expect(p01.left).toBeGreaterThan(p00.left); + expect(p03.left).toBeGreaterThan(p01.left); + expect(p10.top).toBeGreaterThan(p00.top); + expect(p13.left).toBeGreaterThan(p10.left); + }); + it("returns proper coordinates in code blocks", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.code_block)("a\nb\n")) + }), p = []; + for(let i = 1; i <= 5; i++)p.push(view.coordsAtPos(i)); + const [p0, p1, p2, p3, p4] = p; + expect(p0.top).toBe(p1.top); + expect(p0.left).toBeLessThan(p1.left); + expect(p2.top).toBeGreaterThan(p1.top); + expect(p2.top).toBe(p3.top); + expect(p2.left).toBeLessThan(p3.left); + expect(p2.left).toBe(p0.left); + expect(p4.top).toBeGreaterThan(p3.top); + // This one shows a small (0.01 pixel) difference in Firefox for + // some reason. + expect(Math.round(p4.left)).toBe(Math.round(p2.left)); + }); + // TODO: This test fails on the position between the img and the br (it returns + // the position before the img, instead of after). + it.skip("produces sensible screen coordinates in corner cases", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one", (0, _prosemirrorTestBuilder.em)("two", (0, _prosemirrorTestBuilder.strong)("three"), img), (0, _prosemirrorTestBuilder.br)(), (0, _prosemirrorTestBuilder.code)("foo")), (0, _prosemirrorTestBuilder.p)()) + }); + return new Promise((ok)=>{ + setTimeout(()=>{ + allPositions(view.state.doc).forEach((pos)=>{ + const coords = view.coordsAtPos(pos); + const found = view.posAtCoords({ + top: coords.top + 1, + left: coords.left + }).pos; + expect(found).toBe(pos); + setSel(view, pos); + }); + ok(null); + }, 20); + }); + }); + it("doesn't return zero-height rectangles after leaves", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)(img)) + }); + const coords = view.coordsAtPos(2, 1); + expect(coords.bottom - coords.top).toBeGreaterThan(5); + }); + it("produces horizontal rectangles for positions between blocks", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("ha"), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.blockquote)((0, _prosemirrorTestBuilder.p)("ba"))) + }); + const a = view.coordsAtPos(0); + expect(a.top).toBe(a.bottom); + expect(a.top).toBe(view.dom.firstChild.getBoundingClientRect().top); + expect(a.left).toBeLessThan(a.right); + const b = view.coordsAtPos(4); + expect(b.top).toBe(b.bottom); + expect(b.top).toBeGreaterThan(a.top); + expect(b.left).toBeLessThan(b.right); + const c = view.coordsAtPos(5); + expect(c.top).toBe(c.bottom); + expect(c.top).toBeGreaterThan(b.top); + const d = view.coordsAtPos(6); + expect(d.top).toBe(d.bottom); + expect(d.left).toBeLessThan(d.right); + expect(d.top).toBeLessThan(view.dom.getBoundingClientRect().bottom); + }); + it("produces sensible screen coordinates around line breaks", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one two three four five-six-seven-eight")) + }); + function afterSpace(pos) { + return pos > 0 && view.state.doc.textBetween(pos - 1, pos) == " "; + } + view.dom.style.width = "4em"; + let prevBefore; + let prevAfter; + allPositions(view.state.doc).forEach((pos)=>{ + const coords = view.coordsAtPos(pos, 1); + if (prevAfter) // eslint-disable-next-line jest/no-conditional-expect + expect(prevAfter.top < coords.top || prevAfter.top == coords.top && prevAfter.left < coords.left).toBeTruthy(); + prevAfter = coords; + const found = view.posAtCoords({ + top: coords.top + 1, + left: coords.left + }).pos; + expect(found).toBe(pos); + const coordsBefore = view.coordsAtPos(pos, -1); + if (prevBefore) // eslint-disable-next-line jest/no-conditional-expect + expect(prevBefore.top < coordsBefore.top || prevBefore.top == coordsBefore.top && (prevBefore.left < coordsBefore.left || afterSpace(pos) && prevBefore.left == coordsBefore.left)).toBeTruthy(); + prevBefore = coordsBefore; + }); + }); + it("can find coordinates on node boundaries", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one ", (0, _prosemirrorTestBuilder.em)("two"), " ", (0, _prosemirrorTestBuilder.em)((0, _prosemirrorTestBuilder.strong)("three")))) + }); + let prev; + allPositions(view.state.doc).forEach((pos)=>{ + const coords = view.coordsAtPos(pos, 1); + if (prev) // eslint-disable-next-line jest/no-conditional-expect + expect(prev.top < coords.top || Math.abs(prev.top - coords.top) < 4 && prev.left < coords.left).toBeTruthy(); + prev = coords; + }); + }); + it("finds proper coordinates in RTL text", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("مرآة نثرية")) + }); + view.dom.style.direction = "rtl"; + let prev; + allPositions(view.state.doc).forEach((pos)=>{ + const coords = view.coordsAtPos(pos, 1); + if (prev) // eslint-disable-next-line jest/no-conditional-expect + expect(prev.top < coords.top || Math.abs(prev.top - coords.top) < 4 && prev.left > coords.left).toBeTruthy(); + prev = coords; + }); + }); + it("can go back and forth between screen coordsa and document positions", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("one"), (0, _prosemirrorTestBuilder.blockquote)((0, _prosemirrorTestBuilder.p)("two"), (0, _prosemirrorTestBuilder.p)("three"))) + }); + [ + 1, + 2, + 4, + 7, + 14, + 15 + ].forEach((pos)=>{ + const coords = view.coordsAtPos(pos); + const found = view.posAtCoords({ + top: coords.top + 1, + left: coords.left + }).pos; + expect(found).toBe(pos); + }); + }); + it("returns correct screen coordinates for wrapped lines", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({}); + const top = view.coordsAtPos(1); + let pos = 1, end; + for(let i = 0; i < 100; i++){ + view.dispatch(view.state.tr.insertText("a bc de fg h")); + pos += 12; + end = view.coordsAtPos(pos); + if (end.bottom > top.bottom + 4) break; + } + expect(view.posAtCoords({ + left: end.left + 50, + top: end.top + 5 + }).pos).toBe(pos); + }); + it("makes arrow motion go through selectable inline nodes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", img, "bar")) + }); + (0, _react.act)(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.from).toBe(4); + (0, _react.act)(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.head).toBe(5); + expect(view.state.selection.anchor).toBe(5); + (0, _react.act)(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.from).toBe(4); + (0, _react.act)(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.head).toBe(4); + expect(view.state.selection.anchor).toBe(4); + }); + it("makes arrow motion go through selectable block nodes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("hello"), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.ul)((0, _prosemirrorTestBuilder.li)((0, _prosemirrorTestBuilder.p)("there")))) + }); + (0, _react.act)(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(7); + setSel(view, 11); + (0, _react.act)(()=>{ + view.dispatchEvent(event(UP)); + }); + expect(view.state.selection.from).toBe(7); + }); + it("supports arrow motion through adjacent blocks", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.blockquote)((0, _prosemirrorTestBuilder.p)("hello")), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.p)("there")) + }); + (0, _react.act)(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(9); + (0, _react.act)(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(10); + setSel(view, 14); + (0, _react.act)(()=>{ + view.dispatchEvent(event(UP)); + }); + expect(view.state.selection.from).toBe(10); + (0, _react.act)(()=>{ + view.dispatchEvent(event(UP)); + }); + expect(view.state.selection.from).toBe(9); + }); + it("support horizontal motion through blocks", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.p)("bar")) + }); + (0, _react.act)(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.from).toBe(5); + (0, _react.act)(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.from).toBe(6); + (0, _react.act)(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.head).toBe(8); + (0, _react.act)(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.from).toBe(6); + (0, _react.act)(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.from).toBe(5); + (0, _react.act)(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.head).toBe(4); + }); + it("allows moving directly from an inline node to a block node", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", img), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.p)(img, "bar")) + }); + setSel(view, _prosemirrorState.NodeSelection.create(view.state.doc, 4)); + (0, _react.act)(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(6); + setSel(view, _prosemirrorState.NodeSelection.create(view.state.doc, 8)); + (0, _react.act)(()=>{ + view.dispatchEvent(event(UP)); + }); + expect(view.state.selection.from).toBe(6); + }); + it("updates the selection even if the DOM parameters look unchanged", async ()=>{ + const { view , rerender } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foobar")) + }); + view.focus(); + const decos = _prosemirrorView.DecorationSet.create(view.state.doc, [ + _prosemirrorView.Decoration.inline(1, 4, { + color: "green" + }) + ]); + rerender({ + decorations () { + return decos; + } + }); + rerender({ + decorations: undefined + }); + rerender({ + decorations () { + return decos; + } + }); + const range = document.createRange(); + range.setEnd(document.getSelection().anchorNode, document.getSelection().anchorOffset); + range.setStart(view.dom, 0); + expect(range.toString()).toBe("foobar"); + }); + it("sets selection even if Selection.extend throws DOMException", async ()=>{ + const originalExtend = window.Selection.prototype.extend; + window.Selection.prototype.extend = ()=>{ + // declare global: DOMException + throw new DOMException("failed"); + }; + try { + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo", img), (0, _prosemirrorTestBuilder.hr)(), (0, _prosemirrorTestBuilder.p)(img, "bar")) + }); + setSel(view, _prosemirrorState.NodeSelection.create(view.state.doc, 4)); + (0, _react.act)(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(6); + } finally{ + window.Selection.prototype.extend = originalExtend; + } + }); + it("doesn't put the cursor after BR hack nodes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()) + }); + view.focus(); + expect(getSelection().focusOffset).toBe(0); + }); +}); diff --git a/dist/cjs/components/__tests__/ProseMirror.test.js b/dist/cjs/components/__tests__/ProseMirror.test.js new file mode 100644 index 00000000..3fa3f584 --- /dev/null +++ b/dist/cjs/components/__tests__/ProseMirror.test.js @@ -0,0 +1,382 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _react = require("@testing-library/react"); +const _prosemirrorModel = require("prosemirror-model"); +const _prosemirrorState = require("prosemirror-state"); +const _prosemirrorTestBuilder = require("prosemirror-test-builder"); +const _react1 = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _useEditorEffectJs = require("../../hooks/useEditorEffect.js"); +const _useStopEventJs = require("../../hooks/useStopEvent.js"); +const _reactKeysJs = require("../../plugins/reactKeys.js"); +const _editorViewTestHelpersJs = require("../../testing/editorViewTestHelpers.js"); +const _proseMirrorJs = require("../ProseMirror.js"); +const _proseMirrorDocJs = require("../ProseMirrorDoc.js"); +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +describe("ProseMirror", ()=>{ + it("renders a contenteditable", async ()=>{ + const schema = new _prosemirrorModel.Schema({ + nodes: { + text: {}, + doc: { + content: "text*" + } + } + }); + const editorState = _prosemirrorState.EditorState.create({ + schema + }); + function TestEditor() { + return /*#__PURE__*/ _react1.default.createElement(_proseMirrorJs.ProseMirror, { + defaultState: editorState + }, /*#__PURE__*/ _react1.default.createElement(_proseMirrorDocJs.ProseMirrorDoc, { + "data-testid": "editor" + })); + } + (0, _react.render)(/*#__PURE__*/ _react1.default.createElement(TestEditor, null)); + const editor = _react.screen.getByTestId("editor"); + editor.focus(); + await browser.keys("H"); + await browser.keys("e"); + await browser.keys("l"); + await browser.keys("l"); + await browser.keys("o"); + await browser.keys(","); + await browser.keys(" "); + await browser.keys("w"); + await browser.keys("o"); + await browser.keys("r"); + await browser.keys("l"); + await browser.keys("d"); + await browser.keys("!"); + expect(editor.textContent).toBe("Hello, world!"); + }); + it("supports lifted editor state", async ()=>{ + const schema = new _prosemirrorModel.Schema({ + nodes: { + text: {}, + doc: { + content: "text*" + } + } + }); + let outerEditorState = _prosemirrorState.EditorState.create({ + schema + }); + function TestEditor() { + const [editorState, setEditorState] = (0, _react1.useState)(outerEditorState); + (0, _react1.useEffect)(()=>{ + outerEditorState = editorState; + }, [ + editorState + ]); + return /*#__PURE__*/ _react1.default.createElement(_proseMirrorJs.ProseMirror, { + state: editorState, + dispatchTransaction: (tr)=>setEditorState(editorState.apply(tr)) + }, /*#__PURE__*/ _react1.default.createElement(_proseMirrorDocJs.ProseMirrorDoc, { + "data-testid": "editor" + })); + } + (0, _react.render)(/*#__PURE__*/ _react1.default.createElement(TestEditor, null)); + const editor = _react.screen.getByTestId("editor"); + editor.focus(); + await browser.keys("H"); + await browser.keys("e"); + await browser.keys("l"); + await browser.keys("l"); + await browser.keys("o"); + await browser.keys(","); + await browser.keys(" "); + await browser.keys("w"); + await browser.keys("o"); + await browser.keys("r"); + await browser.keys("l"); + await browser.keys("d"); + await browser.keys("!"); + expect(outerEditorState.doc.textContent).toBe("Hello, world!"); + }); + it("supports React NodeViews", async ()=>{ + const schema = new _prosemirrorModel.Schema({ + nodes: { + text: {}, + paragraph: { + content: "text*", + toDOM () { + return [ + "p", + 0 + ]; + } + }, + doc: { + content: "paragraph+" + } + } + }); + const editorState = _prosemirrorState.EditorState.create({ + schema + }); + const Paragraph = /*#__PURE__*/ (0, _react1.forwardRef)(function Paragraph(param, ref) { + let { children } = param; + return /*#__PURE__*/ _react1.default.createElement("p", { + ref: ref, + "data-testid": "paragraph" + }, children); + }); + const reactNodeViews = { + paragraph: Paragraph + }; + function TestEditor() { + return /*#__PURE__*/ _react1.default.createElement(_proseMirrorJs.ProseMirror, { + defaultState: editorState, + nodeViews: reactNodeViews + }, /*#__PURE__*/ _react1.default.createElement(_proseMirrorDocJs.ProseMirrorDoc, { + "data-testid": "editor" + })); + } + (0, _react.render)(/*#__PURE__*/ _react1.default.createElement(TestEditor, null)); + const editor = _react.screen.getByTestId("editor"); + editor.focus(); + await browser.keys("H"); + await browser.keys("e"); + await browser.keys("l"); + await browser.keys("l"); + await browser.keys("o"); + await browser.keys(","); + await browser.keys(" "); + await browser.keys("w"); + await browser.keys("o"); + await browser.keys("r"); + await browser.keys("l"); + await browser.keys("d"); + await browser.keys("!"); + expect(editor.textContent).toBe("Hello, world!"); + // Ensure that ProseMirror really rendered our Paragraph + // component, not just any old

tag + expect(_react.screen.getAllByTestId("paragraph").length).toBeGreaterThanOrEqual(1); + }); + it("reflects the current state in .props", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()) + }); + expect(view.state).toBe(view.props.state); + }); + it("calls handleScrollToSelection when appropriate", async ()=>{ + let scrolled = 0; + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()), + handleScrollToSelection: ()=>{ + scrolled++; + return false; + } + }); + view.dispatch(view.state.tr.scrollIntoView()); + expect(scrolled).toBe(1); + }); + it("can be queried for the DOM position at a doc position", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.ul)((0, _prosemirrorTestBuilder.li)((0, _prosemirrorTestBuilder.p)((0, _prosemirrorTestBuilder.strong)("foo"))))) + }); + const inText = view.domAtPos(4); + expect(inText.offset).toBe(1); + expect(inText.node.nodeValue).toBe("foo"); + const beforeLI = view.domAtPos(1); + expect(beforeLI.offset).toBe(0); + expect(beforeLI.node.nodeName).toBe("UL"); + const afterP = view.domAtPos(7); + expect(afterP.offset).toBe(1); + expect(afterP.node.nodeName).toBe("LI"); + }); + it("can bias DOM position queries to enter nodes", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)((0, _prosemirrorTestBuilder.em)((0, _prosemirrorTestBuilder.strong)("a"), "b"), "c")) + }); + function get(pos, bias) { + const r = view.domAtPos(pos, bias); + return (r.node.nodeType == 1 ? r.node.nodeName : r.node.nodeValue) + "@" + r.offset; + } + expect(get(1, 0)).toBe("P@0"); + expect(get(1, -1)).toBe("P@0"); + expect(get(1, 1)).toBe("a@0"); + expect(get(2, -1)).toBe("a@1"); + expect(get(2, 0)).toBe("EM@1"); + expect(get(2, 1)).toBe("b@0"); + expect(get(3, -1)).toBe("b@1"); + expect(get(3, 0)).toBe("P@1"); + expect(get(3, 1)).toBe("c@0"); + expect(get(4, -1)).toBe("c@1"); + expect(get(4, 0)).toBe("P@2"); + expect(get(4, 1)).toBe("P@2"); + }); + it("can be queried for a node's DOM representation", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.hr)()) + }); + expect(view.nodeDOM(0).nodeName).toBe("P"); + expect(view.nodeDOM(5).nodeName).toBe("HR"); + expect(view.nodeDOM(3)).toBeNull(); + }); + it("can map DOM positions to doc positions", async ()=>{ + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.hr)()) + }); + expect(view.posAtDOM(view.dom.firstChild.firstChild, 2)).toBe(3); + expect(view.posAtDOM(view.dom, 1)).toBe(5); + expect(view.posAtDOM(view.dom, 2)).toBe(6); + expect(view.posAtDOM(view.dom.lastChild, 0, -1)).toBe(5); + expect(view.posAtDOM(view.dom.lastChild, 0, 1)).toBe(6); + }); + it("binds this to itself in dispatchTransaction prop", async ()=>{ + let thisBinding; + const { view } = (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)("foo"), (0, _prosemirrorTestBuilder.hr)()), + dispatchTransaction () { + // eslint-disable-next-line @typescript-eslint/no-this-alias + thisBinding = this; + } + }); + view.dispatch(view.state.tr.insertText("x")); + expect(view).toBe(thisBinding); + }); + it("replaces the EditorView when ProseMirror would redraw", async ()=>{ + const viewPlugin = ()=>new _prosemirrorState.Plugin({ + props: { + nodeViews: { + horizontal_rule () { + const dom = document.createElement("hr"); + return { + dom + }; + } + } + } + }); + const startDoc = (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()); + const firstState = _prosemirrorState.EditorState.create({ + doc: startDoc, + schema: _prosemirrorTestBuilder.schema, + plugins: [ + viewPlugin(), + (0, _reactKeysJs.reactKeys)() + ] + }); + let firstView = null; + let secondView = null; + function Test() { + (0, _useEditorEffectJs.useEditorEffect)((v)=>{ + if (firstView === null) { + firstView = v; + } else { + secondView = v; + } + }); + return null; + } + const Paragraph = /*#__PURE__*/ (0, _react1.forwardRef)(function Paragraph(param, ref) { + let { nodeProps , children , ...props } = param; + return /*#__PURE__*/ _react1.default.createElement("p", _extends({ + ref: ref, + "data-testid": "node-view" + }, props), children); + }); + const { rerender } = (0, _react.render)(/*#__PURE__*/ _react1.default.createElement(_proseMirrorJs.ProseMirror, { + state: firstState, + nodeViews: { + paragraph: Paragraph + } + }, /*#__PURE__*/ _react1.default.createElement(Test, null), /*#__PURE__*/ _react1.default.createElement(_proseMirrorDocJs.ProseMirrorDoc, null))); + expect(()=>_react.screen.getByTestId("node-view")).not.toThrow(); + const secondState = _prosemirrorState.EditorState.create({ + doc: startDoc, + schema: _prosemirrorTestBuilder.schema, + plugins: [ + viewPlugin(), + (0, _reactKeysJs.reactKeys)() + ] + }); + rerender(/*#__PURE__*/ _react1.default.createElement(_proseMirrorJs.ProseMirror, { + state: secondState, + nodeViews: { + paragraph: Paragraph + } + }, /*#__PURE__*/ _react1.default.createElement(Test, null), /*#__PURE__*/ _react1.default.createElement(_proseMirrorDocJs.ProseMirrorDoc, null))); + expect(()=>_react.screen.getByTestId("node-view")).not.toThrow(); + expect(firstView).not.toBeNull(); + expect(secondView).not.toBeNull(); + expect(firstView === secondView).toBeFalsy(); + }); + it("supports focusing interactive controls", async ()=>{ + (0, _editorViewTestHelpersJs.tempEditor)({ + doc: (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.hr)()), + nodeViews: { + horizontal_rule: /*#__PURE__*/ (0, _react1.forwardRef)(function Button(param, ref) { + let { nodeProps , ...props } = param; + (0, _useStopEventJs.useStopEvent)(()=>{ + return true; + }); + return /*#__PURE__*/ _react1.default.createElement("button", _extends({ + id: "button", + ref: ref, + type: "button" + }, props), "Click me"); + }) + } + }); + const button = _react.screen.getByText("Click me"); + await $("#button").click(); + expect(document.activeElement === button).toBeTruthy(); + }); +}); diff --git a/dist/cjs/contexts/ChildDescriptorsContext.js b/dist/cjs/contexts/ChildDescriptorsContext.js new file mode 100644 index 00000000..0ef1f815 --- /dev/null +++ b/dist/cjs/contexts/ChildDescriptorsContext.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "ChildDescriptorsContext", { + enumerable: true, + get: ()=>ChildDescriptorsContext +}); +const _react = require("react"); +const ChildDescriptorsContext = (0, _react.createContext)({ + parentRef: { + current: undefined + }, + siblingsRef: { + current: [] + } +}); diff --git a/dist/cjs/contexts/EditorContext.js b/dist/cjs/contexts/EditorContext.js new file mode 100644 index 00000000..c8cc3019 --- /dev/null +++ b/dist/cjs/contexts/EditorContext.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "EditorContext", { + enumerable: true, + get: ()=>EditorContext +}); +const _react = require("react"); +const EditorContext = (0, _react.createContext)(null); diff --git a/dist/cjs/contexts/EditorStateContext.js b/dist/cjs/contexts/EditorStateContext.js new file mode 100644 index 00000000..6d239959 --- /dev/null +++ b/dist/cjs/contexts/EditorStateContext.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "EditorStateContext", { + enumerable: true, + get: ()=>EditorStateContext +}); +const _react = require("react"); +const EditorStateContext = (0, _react.createContext)(null); diff --git a/dist/cjs/contexts/LayoutGroupContext.js b/dist/cjs/contexts/LayoutGroupContext.js new file mode 100644 index 00000000..ec12c4a2 --- /dev/null +++ b/dist/cjs/contexts/LayoutGroupContext.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "LayoutGroupContext", { + enumerable: true, + get: ()=>LayoutGroupContext +}); +const _react = require("react"); +const LayoutGroupContext = /*#__PURE__*/ (0, _react.createContext)(null); diff --git a/dist/cjs/contexts/NodeViewContext.js b/dist/cjs/contexts/NodeViewContext.js new file mode 100644 index 00000000..c8a74d8c --- /dev/null +++ b/dist/cjs/contexts/NodeViewContext.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "NodeViewContext", { + enumerable: true, + get: ()=>NodeViewContext +}); +const _react = require("react"); +const NodeViewContext = /*#__PURE__*/ (0, _react.createContext)(null); diff --git a/dist/cjs/contexts/StopEventContext.js b/dist/cjs/contexts/StopEventContext.js new file mode 100644 index 00000000..257ff322 --- /dev/null +++ b/dist/cjs/contexts/StopEventContext.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "StopEventContext", { + enumerable: true, + get: ()=>StopEventContext +}); +const _react = require("react"); +const StopEventContext = (0, _react.createContext)(null); diff --git a/dist/cjs/contexts/__tests__/DeferredLayoutEffects.test.js b/dist/cjs/contexts/__tests__/DeferredLayoutEffects.test.js new file mode 100644 index 00000000..c9ffd5b8 --- /dev/null +++ b/dist/cjs/contexts/__tests__/DeferredLayoutEffects.test.js @@ -0,0 +1,141 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _react = require("@testing-library/react"); +const _react1 = /*#__PURE__*/ _interopRequireWildcard(require("react")); +const _layoutGroupJs = require("../../components/LayoutGroup.js"); +const _useLayoutGroupEffectJs = require("../../hooks/useLayoutGroupEffect.js"); +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || typeof obj !== "object" && typeof obj !== "function") { + return { + default: obj + }; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for(var key in obj){ + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} +describe("DeferredLayoutEffects", ()=>{ + jest.useFakeTimers("modern"); + it("registers multiple effects and runs them", ()=>{ + function Parent() { + return /*#__PURE__*/ _react1.default.createElement(_layoutGroupJs.LayoutGroup, null, /*#__PURE__*/ _react1.default.createElement(Child, null)); + } + function Child() { + const [double, setDouble] = (0, _react1.useState)(1); + (0, _react1.useLayoutEffect)(()=>{ + if (double === 2) { + setTimeout(()=>{ + setDouble((d)=>d * 2.5); + }, 500); + } + if (double === 20) { + setDouble((d)=>d * 2.5); + } + }, [ + double + ]); + (0, _useLayoutGroupEffectJs.useLayoutGroupEffect)(()=>{ + const timeout = setTimeout(()=>{ + setDouble((d)=>d * 2); + }, 1000); + return ()=>{ + clearTimeout(timeout); + }; + }, [ + double + ]); + return /*#__PURE__*/ _react1.default.createElement("div", null, /*#__PURE__*/ _react1.default.createElement("div", { + "data-testid": "double" + }, double)); + } + // The component mounts ... + // ... the initial value should be 1 + // ... there should be one timeout scheduled by the deferred effect + (0, _react.render)(/*#__PURE__*/ _react1.default.createElement(Parent, null)); + expect(_react.screen.getByTestId("double").innerHTML).toBe("1"); + // This block assert that deferred effects run. + // -------------------------------------------- + // 1000 milliseconds go by ... + // ... the timeout set by the deferred effect should run + // ... the timeout should double the new value to 2 + // ... the immediate effect should set a timeout + // ... the deferred effect should set a timeout + (0, _react.act)(()=>{ + jest.advanceTimersByTime(1000); + }); + expect(_react.screen.getByTestId("double").innerHTML).toBe("2"); + // The next three blocks assert that cleanup of deferred effects run. + // ------------------------------------------------------------------ + // 500 milliseconds go by ... + // ... the timeout set by the immediate effect should run + // ... the timeout should set the value to 5 + // ... the old deferred effect should cancel its timeout + // ... the new deferred effect should set a new timeout + (0, _react.act)(()=>{ + jest.advanceTimersByTime(500); + }); + expect(_react.screen.getByTestId("double").innerHTML).toBe("5"); + // ... 500 more milliseconds go by ... + // ... the canceled timeout should not run + // ... the rescheduled timoeut should not yet run + (0, _react.act)(()=>{ + jest.advanceTimersByTime(500); + }); + expect(_react.screen.getByTestId("double").innerHTML).toBe("5"); + // ... 500 more milliseconds go by ... + // ... the rescheduled timeout should run + // ... the timeout should double the value to 10 + // ... the deferred effect should set a new timeout + (0, _react.act)(()=>{ + jest.advanceTimersByTime(500); + }); + expect(_react.screen.getByTestId("double").innerHTML).toBe("10"); + // The next block asserts that cancelation of deferred effects works. + // ------------------------------------------------------------------ + // 1000 milliseconds go by ... + // ... the timeout set by the deferred effect should run + // ... the timeout should double the value to 20 + // ... the immediate effect should then set the value to 50 + // ... the deferred effect from the first render should not run + // ... the deferred effect from the second render should run + // ... the deferred effect that does run should set a new timeout + (0, _react.act)(()=>{ + jest.advanceTimersByTime(1000); + }); + // For this assertion, we need to clear a timer from the React scheduler. + jest.advanceTimersByTime(1); + expect(_react.screen.getByTestId("double").innerHTML).toBe("50"); + expect(jest.getTimerCount()).toBe(1); + }); +}); diff --git a/dist/cjs/decorations/ReactWidgetType.js b/dist/cjs/decorations/ReactWidgetType.js new file mode 100644 index 00000000..44d2846d --- /dev/null +++ b/dist/cjs/decorations/ReactWidgetType.js @@ -0,0 +1,51 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + ReactWidgetType: ()=>ReactWidgetType, + widget: ()=>widget +}); +const _prosemirrorView = require("prosemirror-view"); +function compareObjs(a, b) { + if (a == b) return true; + for(const p in a)if (a[p] !== b[p]) return false; + for(const p in b)if (!(p in a)) return false; + return true; +} +const noSpec = { + side: 0 +}; +let ReactWidgetType = class ReactWidgetType { + map(mapping, span, offset, oldOffset) { + const { pos , deleted } = mapping.mapResult(span.from + oldOffset, this.side < 0 ? -1 : 1); + // @ts-expect-error The Decoration constructor is private/internal, but + // we need to use it for our custom widget implementation here. + return deleted ? null : new _prosemirrorView.Decoration(pos - offset, pos - offset, this); + } + valid() { + return true; + } + eq(other) { + return this == other || other instanceof ReactWidgetType && (this.spec.key && this.spec.key == other.spec.key || this.Component == other.Component && compareObjs(this.spec, other.spec)); + } + destroy() { + // Can be implemented with React effect hooks + } + constructor(Component, spec){ + this.Component = Component; + this.spec = spec ?? noSpec; + this.side = this.spec.side ?? 0; + } +}; +function widget(pos, component, spec) { + // @ts-expect-error The Decoration constructor is private/internal, but + // we need to use it for our custom widget implementation here. + return new _prosemirrorView.Decoration(pos, pos, new ReactWidgetType(component, spec)); +} diff --git a/dist/cjs/decorations/computeDocDeco.js b/dist/cjs/decorations/computeDocDeco.js new file mode 100644 index 00000000..515c58ba --- /dev/null +++ b/dist/cjs/decorations/computeDocDeco.js @@ -0,0 +1,42 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "computeDocDeco", { + enumerable: true, + get: ()=>computeDocDeco +}); +const _prosemirrorView = require("prosemirror-view"); +const DocDecorationsCache = new WeakMap(); +function computeDocDeco(view) { + const attrs = Object.create(null); + attrs.class = "ProseMirror"; + attrs.contenteditable = String(view.editable); + view.someProp("attributes", (value)=>{ + if (typeof value == "function") value = value(view.state); + if (value) for(const attr in value){ + if (attr == "class") attrs.class += " " + value[attr]; + else if (attr == "style") attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr]; + else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName") attrs[attr] = String(value[attr]); + } + }); + if (!attrs.translate) attrs.translate = "no"; + const next = [ + _prosemirrorView.Decoration.node(0, view.state.doc.content.size, attrs) + ]; + const previous = DocDecorationsCache.get(view); + if (!previous) { + DocDecorationsCache.set(view, next); + return next; + } + if (previous[0].to !== view.state.doc.content.size) { + DocDecorationsCache.set(view, next); + return next; + } + // @ts-expect-error Internal property (Decoration.type) + if (!previous[0].type.eq(next[0].type)) { + DocDecorationsCache.set(view, next); + return next; + } + return previous; +} diff --git a/dist/cjs/decorations/internalTypes.js b/dist/cjs/decorations/internalTypes.js new file mode 100644 index 00000000..b62a6d55 --- /dev/null +++ b/dist/cjs/decorations/internalTypes.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); diff --git a/dist/cjs/decorations/iterDeco.js b/dist/cjs/decorations/iterDeco.js new file mode 100644 index 00000000..36e9a783 --- /dev/null +++ b/dist/cjs/decorations/iterDeco.js @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "iterDeco", { + enumerable: true, + get: ()=>iterDeco +}); +const _reactWidgetTypeJs = require("./ReactWidgetType.js"); +function compareSide(a, b) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return a.type.side - b.type.side; +} +function iterDeco(parent, deco, // Callbacks have been slightly modified to pass +// the offset, so that we can pass the position as +// a prop to components +onWidget, onNode) { + const locals = deco.locals(parent); + let offset = 0; + // Simple, cheap variant for when there are no local decorations + if (locals.length == 0) { + for(let i = 0; i < parent.childCount; i++){ + const child = parent.child(i); + onNode(child, locals, deco.forChild(offset, child), offset, i); + offset += child.nodeSize; + } + return; + } + let decoIndex = 0; + const active = []; + let restNode = null; + for(let parentIndex = 0;;){ + if (decoIndex < locals.length && locals[decoIndex].to == offset) { + const widget = locals[decoIndex++]; + let 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], // eslint-disable-next-line @typescript-eslint/no-explicit-any + !(widgets[i].type instanceof _reactWidgetTypeJs.ReactWidgetType), offset, parentIndex + i, !!restNode); + } else { + onWidget(widget, // eslint-disable-next-line @typescript-eslint/no-explicit-any + !(widget.type instanceof _reactWidgetTypeJs.ReactWidgetType), offset, 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; + } + } + const outerDeco = child.isInline && !child.isLeaf ? active.filter((d)=>!d.inline) : active.slice(); + onNode(child, outerDeco, deco.forChild(offset, child), offset, index); + offset = end; + } +} diff --git a/dist/cjs/decorations/viewDecorations.js b/dist/cjs/decorations/viewDecorations.js new file mode 100644 index 00000000..221dc02b --- /dev/null +++ b/dist/cjs/decorations/viewDecorations.js @@ -0,0 +1,160 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "viewDecorations", { + enumerable: true, + get: ()=>viewDecorations +}); +const _prosemirrorView = require("prosemirror-view"); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const none = [], noSpec = {}; +const empty = _prosemirrorView.DecorationSet.empty; +// 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. +let DecorationGroup = class DecorationGroup { + 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 _prosemirrorView.DecorationSet.empty; + let found = []; + for(let i = 0; i < this.members.length; i++){ + const 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++){ + const locals = this.members[i].localsInner(node); + if (!locals.length) continue; + if (!result) { + result = locals; + } else { + if (sorted) { + result = result.slice(); + sorted = false; + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + for(let j = 0; j < locals.length; j++)result.push(locals[j]); + } + } + return result ? removeOverlap(sorted ? result : result.sort(byPos)) : none; + } + // 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: + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return members[0]; + default: + return new DecorationGroup(members.every((m)=>m instanceof _prosemirrorView.DecorationSet) ? members : members.reduce((r, m)=>r.concat(m instanceof _prosemirrorView.DecorationSet ? m : m.members), [])); + } + } + forEachSet(f) { + for(let i = 0; i < this.members.length; i++)// eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.members[i].forEachSet(f); + } + constructor(members){ + this.members = members; + } +}; +// 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; +} +// 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++){ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const span = working[i]; + if (span.from != span.to) for(let j = i + 1; j < working.length; j++){ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const 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) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + while(i < array.length && byPos(deco, array[i]) > 0)i++; + array.splice(i, 0, deco); +} +const ViewDecorationsCache = new WeakMap(); +function viewDecorations(view, cursorWrapper) { + const found = []; + view.someProp("decorations", (f)=>{ + const result = f(view.state); + if (result && result != empty) found.push(result); + }); + // We don't have access to types for view.cursorWrapper here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (cursorWrapper) { + found.push(// eslint-disable-next-line @typescript-eslint/no-explicit-any + _prosemirrorView.DecorationSet.create(view.state.doc, [ + cursorWrapper + ])); + } + const previous = ViewDecorationsCache.get(view); + if (!previous) { + const result = DecorationGroup.from(found); + ViewDecorationsCache.set(view, result); + return result; + } + let numPrevious = 0; + let areSetsEqual = true; + previous.forEachSet((set)=>{ + const next = found[numPrevious++]; + if (next !== set) { + areSetsEqual = false; + } + }); + if (numPrevious !== found.length) { + areSetsEqual = false; + } + if (!areSetsEqual) { + const result = DecorationGroup.from(found); + ViewDecorationsCache.set(view, result); + return result; + } + return previous; +} diff --git a/dist/cjs/dom.js b/dist/cjs/dom.js new file mode 100644 index 00000000..50caf715 --- /dev/null +++ b/dist/cjs/dom.js @@ -0,0 +1,120 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + domIndex: ()=>domIndex, + parentNode: ()=>parentNode, + textRange: ()=>textRange, + isEquivalentPosition: ()=>isEquivalentPosition, + nodeSize: ()=>nodeSize, + isOnEdge: ()=>isOnEdge, + hasBlockDesc: ()=>hasBlockDesc, + selectionCollapsed: ()=>selectionCollapsed, + keyEvent: ()=>keyEvent, + deepActiveElement: ()=>deepActiveElement, + caretFromPoint: ()=>caretFromPoint +}); +const domIndex = function(node) { + for(let index = 0;; index++){ + node = node.previousSibling; + if (!node) return index; + } +}; +const parentNode = function(node) { + const parent = node.assignedSlot || node.parentNode; + return parent && parent.nodeType == 11 ? parent.host : parent; +}; +let reusedRange = null; +const textRange = function(node, from, to) { + const range = reusedRange || (reusedRange = document.createRange()); + range.setEnd(node, to == null ? node.nodeValue.length : to); + range.setStart(node, from || 0); + return range; +}; +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))) { + const 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; + } + } +} +function nodeSize(node) { + return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length; +} +function isOnEdge(node, offset, parent) { + for(let atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;){ + if (node == parent) return true; + const index = domIndex(node); + node = node.parentNode; + if (!node) return false; + atStart = atStart && index == 0; + atEnd = atEnd && index == nodeSize(node); + } + return false; +} +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); +} +const selectionCollapsed = function(domSel) { + return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset, domSel.anchorNode, domSel.anchorOffset); +}; +function keyEvent(keyCode, key) { + const event = document.createEvent("Event"); + event.initEvent("keydown", true, true); + event.keyCode = keyCode; + event.key = event.code = key; + return event; +} +function deepActiveElement(doc) { + let elt = doc.activeElement; + while(elt && elt.shadowRoot)elt = elt.shadowRoot.activeElement; + return elt; +} +function caretFromPoint(doc, x, y) { + if (doc.caretPositionFromPoint) { + try { + // Firefox throws for this call in hard-to-predict circumstances (#994) + const pos = doc.caretPositionFromPoint(x, y); + // Clip the offset, because Chrome will return a text offset + // into nodes, which can't be treated as a regular DOM + // offset + if (pos) return { + node: pos.offsetNode, + offset: Math.min(nodeSize(pos.offsetNode), pos.offset) + }; + } catch (_) { + // pass + } + } + if (doc.caretRangeFromPoint) { + const range = doc.caretRangeFromPoint(x, y); + if (range) return { + node: range.startContainer, + offset: Math.min(nodeSize(range.startContainer), range.startOffset) + }; + } + return; +} diff --git a/dist/cjs/hooks/__tests__/useEditorViewLayoutEffect.test.js b/dist/cjs/hooks/__tests__/useEditorViewLayoutEffect.test.js new file mode 100644 index 00000000..e8beebbe --- /dev/null +++ b/dist/cjs/hooks/__tests__/useEditorViewLayoutEffect.test.js @@ -0,0 +1,108 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _react = require("@testing-library/react"); +const _react1 = /*#__PURE__*/ _interopRequireDefault(require("react")); +const _layoutGroupJs = require("../../components/LayoutGroup.js"); +const _editorContextJs = require("../../contexts/EditorContext.js"); +const _editorStateContextJs = require("../../contexts/EditorStateContext.js"); +const _useEditorEffectJs = require("../useEditorEffect.js"); +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; +} +function TestComponent(param) { + let { effect , dependencies =[] } = param; + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, _useEditorEffectJs.useEditorEffect)(effect, dependencies); + return null; +} +describe("useEditorViewLayoutEffect", ()=>{ + it("should run the effect", ()=>{ + const effect = jest.fn(); + const editorView = {}; + const editorState = {}; + const registerEventListener = ()=>{}; + const unregisterEventListener = ()=>{}; + (0, _react.render)(/*#__PURE__*/ _react1.default.createElement(_layoutGroupJs.LayoutGroup, null, /*#__PURE__*/ _react1.default.createElement(_editorContextJs.EditorContext.Provider, { + value: { + view: editorView, + registerEventListener, + unregisterEventListener + } + }, /*#__PURE__*/ _react1.default.createElement(_editorStateContextJs.EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ _react1.default.createElement(TestComponent, { + effect: effect + }))))); + expect(effect).toHaveBeenCalled(); + expect(effect).toHaveBeenCalledWith(editorView); + }); + it("should not re-run the effect if no dependencies change", ()=>{ + const effect = jest.fn(); + const editorView = {}; + const editorState = {}; + const registerEventListener = ()=>{}; + const unregisterEventListener = ()=>{}; + const contextValue = { + view: editorView, + registerEventListener, + unregisterEventListener + }; + const { rerender } = (0, _react.render)(/*#__PURE__*/ _react1.default.createElement(_layoutGroupJs.LayoutGroup, null, /*#__PURE__*/ _react1.default.createElement(_editorContextJs.EditorContext.Provider, { + value: contextValue + }, /*#__PURE__*/ _react1.default.createElement(_editorStateContextJs.EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ _react1.default.createElement(TestComponent, { + effect: effect, + dependencies: [] + })), " "))); + rerender(/*#__PURE__*/ _react1.default.createElement(_layoutGroupJs.LayoutGroup, null, /*#__PURE__*/ _react1.default.createElement(_editorContextJs.EditorContext.Provider, { + value: contextValue + }, /*#__PURE__*/ _react1.default.createElement(_editorStateContextJs.EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ _react1.default.createElement(TestComponent, { + effect: effect, + dependencies: [] + }))))); + expect(effect).toHaveBeenCalledTimes(1); + }); + it("should re-run the effect if dependencies change", ()=>{ + const effect = jest.fn(); + const editorView = {}; + const editorState = {}; + const registerEventListener = ()=>{}; + const unregisterEventListener = ()=>{}; + const { rerender } = (0, _react.render)(/*#__PURE__*/ _react1.default.createElement(_layoutGroupJs.LayoutGroup, null, /*#__PURE__*/ _react1.default.createElement(_editorContextJs.EditorContext.Provider, { + value: { + view: editorView, + registerEventListener, + unregisterEventListener + } + }, /*#__PURE__*/ _react1.default.createElement(_editorStateContextJs.EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ _react1.default.createElement(TestComponent, { + effect: effect, + dependencies: [ + "one" + ] + }))))); + rerender(/*#__PURE__*/ _react1.default.createElement(_layoutGroupJs.LayoutGroup, null, /*#__PURE__*/ _react1.default.createElement(_editorContextJs.EditorContext.Provider, { + value: { + view: editorView, + registerEventListener, + unregisterEventListener + } + }, /*#__PURE__*/ _react1.default.createElement(_editorStateContextJs.EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ _react1.default.createElement(TestComponent, { + effect: effect, + dependencies: [ + "two" + ] + }))))); + expect(effect).toHaveBeenCalledTimes(2); + }); +}); diff --git a/dist/cjs/hooks/useComponentEventListeners.js b/dist/cjs/hooks/useComponentEventListeners.js new file mode 100644 index 00000000..904abf4c --- /dev/null +++ b/dist/cjs/hooks/useComponentEventListeners.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useComponentEventListeners", { + enumerable: true, + get: ()=>useComponentEventListeners +}); +const _react = require("react"); +const _componentEventListenersJs = require("../plugins/componentEventListeners.js"); +function useComponentEventListeners() { + const [registry, setRegistry] = (0, _react.useState)(new Map()); + const registerEventListener = (0, _react.useCallback)((eventType, handler)=>{ + const handlers = registry.get(eventType) ?? []; + handlers.unshift(handler); + if (!registry.has(eventType)) { + registry.set(eventType, handlers); + setRegistry(new Map(registry)); + } + }, [ + registry + ]); + const unregisterEventListener = (0, _react.useCallback)((eventType, handler)=>{ + const handlers = registry.get(eventType); + handlers?.splice(handlers.indexOf(handler), 1); + }, [ + registry + ]); + const componentEventListenersPlugin = (0, _react.useMemo)(()=>(0, _componentEventListenersJs.componentEventListeners)(registry), [ + registry + ]); + return { + registerEventListener, + unregisterEventListener, + componentEventListenersPlugin + }; +} diff --git a/dist/cjs/hooks/useEditor.js b/dist/cjs/hooks/useEditor.js new file mode 100644 index 00000000..affad8de --- /dev/null +++ b/dist/cjs/hooks/useEditor.js @@ -0,0 +1,279 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + ReactEditorView: ()=>ReactEditorView, + useEditor: ()=>useEditor +}); +const _prosemirrorModel = require("prosemirror-model"); +const _prosemirrorState = require("prosemirror-state"); +const _prosemirrorView = require("prosemirror-view"); +const _react = require("react"); +const _reactDom = require("react-dom"); +const _beforeInputPluginJs = require("../plugins/beforeInputPlugin.js"); +const _selectionDOMObserverJs = require("../selection/SelectionDOMObserver.js"); +const _viewdescJs = require("../viewdesc.js"); +const _useComponentEventListenersJs = require("./useComponentEventListeners.js"); +const _useForceUpdateJs = require("./useForceUpdate.js"); +function buildNodeViews(view) { + const result = Object.create(null); + function add(obj) { + for(const prop in obj)if (!Object.prototype.hasOwnProperty.call(result, prop)) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result[prop] = obj[prop]; + } + view.someProp("nodeViews", add); + view.someProp("markViews", add); + return result; +} +function changedNodeViews(a, b) { + let nA = 0, nB = 0; + for(const prop in a){ + if (a[prop] != b[prop]) return true; + nA++; + } + for(const _ in b)nB++; + return nA != nB; +} +function changedProps(a, b) { + for (const prop of Object.keys(a)){ + if (a[prop] !== b[prop]) return true; + } + return false; +} +function getEditable(view) { + return !view.someProp("editable", (value)=>value(view.state) === false); +} +let ReactEditorView = class ReactEditorView extends _prosemirrorView.EditorView { + /** + * Whether the EditorView's updateStateInner method thinks that the + * docView needs to be blown away and redrawn. + * + * @privateremarks + * + * When ProseMirror View detects that the EditorState has been reconfigured + * to provide new custom node views, it calls an internal function that + * we can't override in order to recreate the entire editor DOM. + * + * This property mimics that check, so that we can replace the EditorView + * with another of our own, preventing ProseMirror View from taking over + * DOM management responsibility. + */ get needsRedraw() { + if (this.oldProps.state.plugins === this._props.state.plugins && this._props.plugins === this.oldProps.plugins) { + return false; + } + const newNodeViews = buildNodeViews(this); + // @ts-expect-error Internal property + return changedNodeViews(this.nodeViews, newNodeViews); + } + /** + * Like setProps, but without executing any side effects. + * Safe to use in a component render method. + */ pureSetProps(props) { + // this.oldProps = this.props; + this._props = { + ...this._props, + ...props + }; + this.state = this._props.state; + this.editable = getEditable(this); + } + /** + * Triggers any side effects that have been queued by previous + * calls to pureSetProps. + */ runPendingEffects() { + if (changedProps(this.props, this.oldProps)) { + const newProps = this.props; + this._props = this.oldProps; + this.state = this._props.state; + this.update(newProps); + } + } + update(props) { + super.update(props); + // Ensure that side effects aren't re-triggered until + // pureSetProps is called again + this.oldProps = props; + } + updatePluginViews(prevState) { + if (this.shouldUpdatePluginViews) { + // @ts-expect-error We're making use of knowledge of internal methods here + super.updatePluginViews(prevState); + } + } + // We want to trigger the default EditorView cleanup, but without + // the actual view.dom cleanup (which React will have already handled). + // So we give the EditorView a dummy DOM element and ask it to clean up + destroy() { + // @ts-expect-error we're intentionally overwriting this property + // to prevent side effects + this.dom = document.createElement("div"); + super.destroy(); + } + constructor(place, props){ + // Call the superclass constructor with an empty + // document and limited props. We'll set everything + // else ourselves. + super(place, { + state: _prosemirrorState.EditorState.create({ + schema: props.state.schema, + plugins: props.state.plugins + }), + plugins: props.plugins + }); + this.shouldUpdatePluginViews = false; + this.shouldUpdatePluginViews = true; + this._props = props; + this.oldProps = { + state: props.state + }; + this.state = props.state; + // @ts-expect-error We're making use of knowledge of internal attributes here + this.domObserver.stop(); + // @ts-expect-error We're making use of knowledge of internal attributes here + this.domObserver = new _selectionDOMObserverJs.SelectionDOMObserver(this); + // @ts-expect-error We're making use of knowledge of internal attributes here + this.domObserver.start(); + this.editable = getEditable(this); + // Destroy the DOM created by the default + // ProseMirror ViewDesc implementation; we + // have a NodeViewDesc from React instead. + // @ts-expect-error We're making use of knowledge of internal attributes here + this.docView.dom.replaceChildren(); + // @ts-expect-error We're making use of knowledge of internal attributes here + this.docView = props.docView; + } +}; +const EMPTY_SCHEMA = new _prosemirrorModel.Schema({ + nodes: { + doc: { + content: "text*" + }, + text: { + inline: true + } + } +}); +const EMPTY_STATE = _prosemirrorState.EditorState.create({ + schema: EMPTY_SCHEMA +}); +let didWarnValueDefaultValue = false; +function useEditor(mount, options) { + if (process.env.NODE_ENV !== "production") { + if (options.defaultState !== undefined && options.state !== undefined && !didWarnValueDefaultValue) { + console.error("A component contains a ProseMirror editor with both value and defaultValue props. " + "ProseMirror editors must be either controlled or uncontrolled " + "(specify either the state prop, or the defaultState prop, but not both). " + "Decide between using a controlled or uncontrolled ProseMirror editor " + "and remove one of these props. More info: " + "https://reactjs.org/link/controlled-components"); + didWarnValueDefaultValue = true; + } + } + const [view, setView] = (0, _react.useState)(null); + const [cursorWrapper, _setCursorWrapper] = (0, _react.useState)(null); + const forceUpdate = (0, _useForceUpdateJs.useForceUpdate)(); + const defaultState = options.defaultState ?? EMPTY_STATE; + const [_state, setState] = (0, _react.useState)(defaultState); + const state = options.state ?? _state; + const { componentEventListenersPlugin , registerEventListener , unregisterEventListener } = (0, _useComponentEventListenersJs.useComponentEventListeners)(); + const setCursorWrapper = (0, _react.useCallback)((deco)=>{ + (0, _reactDom.flushSync)(()=>{ + _setCursorWrapper(deco); + }); + }, []); + const plugins = (0, _react.useMemo)(()=>[ + ...options.plugins ?? [], + componentEventListenersPlugin, + (0, _beforeInputPluginJs.beforeInputPlugin)(setCursorWrapper) + ], [ + options.plugins, + componentEventListenersPlugin, + setCursorWrapper + ]); + const dispatchTransaction = (0, _react.useCallback)(function dispatchTransaction(tr) { + (0, _reactDom.flushSync)(()=>{ + if (!options.state) { + setState((s)=>s.apply(tr)); + } + if (options.dispatchTransaction) { + options.dispatchTransaction.call(this, tr); + } + }); + }, [ + options.dispatchTransaction, + options.state + ]); + const tempDom = document.createElement("div"); + const docViewDescRef = (0, _react.useRef)(new _viewdescJs.NodeViewDesc(undefined, [], -1, state.doc, [], _prosemirrorView.DecorationSet.empty, tempDom, null, tempDom, ()=>false)); + const directEditorProps = { + ...options, + state, + plugins, + dispatchTransaction, + docView: docViewDescRef.current + }; + (0, _react.useLayoutEffect)(()=>{ + return ()=>{ + view?.destroy(); + }; + }, [ + view + ]); + // This rule is concerned about infinite updates due to the + // call to setView. These calls are deliberately conditional, + // so this is not a concern. + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, _react.useLayoutEffect)(()=>{ + if (view && view.dom !== mount) { + setView(null); + } + if (!mount) { + return; + } + if (!view) { + const newView = new ReactEditorView({ + mount + }, directEditorProps); + setView(newView); + newView.dom.addEventListener("compositionend", forceUpdate); + return; + } + }); + // This rule is concerned about infinite updates due to the + // call to setView. These calls are deliberately conditional, + // so this is not a concern. + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, _react.useLayoutEffect)(()=>{ + // If ProseMirror View is about to redraw the entire document's + // DOM, clear the EditorView and reconstruct another, instead. + // This only happens when a newly instantiated EditorState has + // been provided. + if (view?.needsRedraw) { + setView(null); + return; + } else { + // @ts-expect-error Internal property - domObserver + view?.domObserver.selectionToDOM(); + view?.runPendingEffects(); + } + }); + view?.pureSetProps(directEditorProps); + const editor = (0, _react.useMemo)(()=>({ + view: view, + registerEventListener, + unregisterEventListener, + cursorWrapper, + docViewDescRef + }), [ + view, + registerEventListener, + unregisterEventListener, + cursorWrapper + ]); + return { + editor, + state + }; +} diff --git a/dist/cjs/hooks/useEditorEffect.js b/dist/cjs/hooks/useEditorEffect.js new file mode 100644 index 00000000..8f41c2b0 --- /dev/null +++ b/dist/cjs/hooks/useEditorEffect.js @@ -0,0 +1,33 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useEditorEffect", { + enumerable: true, + get: ()=>useEditorEffect +}); +const _react = require("react"); +const _editorContextJs = require("../contexts/EditorContext.js"); +const _useLayoutGroupEffectJs = require("./useLayoutGroupEffect.js"); +function useEditorEffect(effect, dependencies) { + const { view } = (0, _react.useContext)(_editorContextJs.EditorContext); + // The rules of hooks want `effect` to be included in the + // dependency list, but dependency issues for `effect` will + // be caught by the linter at the call-site for + // `useEditorViewLayoutEffect`. + // Note: we specifically don't want to re-run the effect + // every time it changes, because it will most likely + // be defined inline and run on every re-render. + (0, _useLayoutGroupEffectJs.useLayoutGroupEffect)(()=>{ + if (view) { + return effect(view); + } + }, // The rules of hooks want to be able to statically + // verify the dependencies for the effect, but this will + // have already happened at the call-site. + // eslint-disable-next-line react-hooks/exhaustive-deps + dependencies && [ + view, + ...dependencies + ]); +} diff --git a/dist/cjs/hooks/useEditorEventCallback.js b/dist/cjs/hooks/useEditorEventCallback.js new file mode 100644 index 00000000..1601b3bf --- /dev/null +++ b/dist/cjs/hooks/useEditorEventCallback.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useEditorEventCallback", { + enumerable: true, + get: ()=>useEditorEventCallback +}); +const _react = require("react"); +const _editorContextJs = require("../contexts/EditorContext.js"); +const _useEditorEffectJs = require("./useEditorEffect.js"); +function useEditorEventCallback(callback) { + const ref = (0, _react.useRef)(callback); + const { view } = (0, _react.useContext)(_editorContextJs.EditorContext); + (0, _useEditorEffectJs.useEditorEffect)(()=>{ + ref.current = callback; + }, [ + callback + ]); + return (0, _react.useCallback)(function() { + for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ + args[_key] = arguments[_key]; + } + if (view) { + return ref.current(view, ...args); + } + return; + }, [ + view + ]); +} diff --git a/dist/cjs/hooks/useEditorEventListener.js b/dist/cjs/hooks/useEditorEventListener.js new file mode 100644 index 00000000..df914b9d --- /dev/null +++ b/dist/cjs/hooks/useEditorEventListener.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useEditorEventListener", { + enumerable: true, + get: ()=>useEditorEventListener +}); +const _react = require("react"); +const _editorContextJs = require("../contexts/EditorContext.js"); +const _useEditorEffectJs = require("./useEditorEffect.js"); +function useEditorEventListener(eventType, handler) { + const { registerEventListener , unregisterEventListener } = (0, _react.useContext)(_editorContextJs.EditorContext); + const ref = (0, _react.useRef)(handler); + (0, _useEditorEffectJs.useEditorEffect)(()=>{ + ref.current = handler; + }, [ + handler + ]); + const eventHandler = (0, _react.useCallback)(function(view, event) { + return ref.current.call(this, view, event); + }, []); + (0, _useEditorEffectJs.useEditorEffect)(()=>{ + registerEventListener(eventType, eventHandler); + return ()=>unregisterEventListener(eventType, eventHandler); + }, [ + eventHandler, + eventType, + registerEventListener, + unregisterEventListener + ]); +} diff --git a/dist/cjs/hooks/useEditorState.js b/dist/cjs/hooks/useEditorState.js new file mode 100644 index 00000000..7122ebbf --- /dev/null +++ b/dist/cjs/hooks/useEditorState.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useEditorState", { + enumerable: true, + get: ()=>useEditorState +}); +const _react = require("react"); +const _editorStateContextJs = require("../contexts/EditorStateContext.js"); +function useEditorState() { + const editorState = (0, _react.useContext)(_editorStateContextJs.EditorStateContext); + return editorState; +} diff --git a/dist/cjs/hooks/useForceUpdate.js b/dist/cjs/hooks/useForceUpdate.js new file mode 100644 index 00000000..0a3209d0 --- /dev/null +++ b/dist/cjs/hooks/useForceUpdate.js @@ -0,0 +1,13 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useForceUpdate", { + enumerable: true, + get: ()=>useForceUpdate +}); +const _react = require("react"); +function useForceUpdate() { + const [, forceUpdate] = (0, _react.useReducer)((x)=>x + 1, 0); + return forceUpdate; +} diff --git a/dist/cjs/hooks/useLayoutGroupEffect.js b/dist/cjs/hooks/useLayoutGroupEffect.js new file mode 100644 index 00000000..2fc67ff7 --- /dev/null +++ b/dist/cjs/hooks/useLayoutGroupEffect.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useLayoutGroupEffect", { + enumerable: true, + get: ()=>useLayoutGroupEffect +}); +const _react = require("react"); +const _layoutGroupContextJs = require("../contexts/LayoutGroupContext.js"); +function useLayoutGroupEffect(effect, deps) { + const register = (0, _react.useContext)(_layoutGroupContextJs.LayoutGroupContext); + // The rule for hooks wants to statically verify the deps, + // but the dependencies are up to the caller, not this implementation. + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, _react.useLayoutEffect)(()=>register(effect), deps); +} diff --git a/dist/cjs/hooks/useNodeViewDescriptor.js b/dist/cjs/hooks/useNodeViewDescriptor.js new file mode 100644 index 00000000..4b478b76 --- /dev/null +++ b/dist/cjs/hooks/useNodeViewDescriptor.js @@ -0,0 +1,96 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useNodeViewDescriptor", { + enumerable: true, + get: ()=>useNodeViewDescriptor +}); +const _react = require("react"); +const _childDescriptorsContextJs = require("../contexts/ChildDescriptorsContext.js"); +const _editorContextJs = require("../contexts/EditorContext.js"); +const _viewdescJs = require("../viewdesc.js"); +function useNodeViewDescriptor(node, getPos, domRef, nodeDomRef, innerDecorations, outerDecorations, viewDesc, contentDOMRef) { + const { view } = (0, _react.useContext)(_editorContextJs.EditorContext); + const [hasContentDOM, setHasContentDOM] = (0, _react.useState)(true); + const nodeViewDescRef = (0, _react.useRef)(viewDesc); + const stopEvent = (0, _react.useRef)(()=>false); + const setStopEvent = (0, _react.useCallback)((newStopEvent)=>{ + stopEvent.current = newStopEvent; + }, []); + const { siblingsRef , parentRef } = (0, _react.useContext)(_childDescriptorsContextJs.ChildDescriptorsContext); + const childDescriptors = (0, _react.useRef)([]); + (0, _react.useLayoutEffect)(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!nodeViewDescRef.current) return; + if (siblings.includes(nodeViewDescRef.current)) { + const index = siblings.indexOf(nodeViewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, _react.useLayoutEffect)(()=>{ + if (!node || !nodeDomRef.current) return; + const firstChildDesc = childDescriptors.current[0]; + if (!nodeViewDescRef.current) { + nodeViewDescRef.current = new _viewdescJs.NodeViewDesc(parentRef.current, childDescriptors.current, getPos(), node, outerDecorations, innerDecorations, domRef?.current ?? nodeDomRef.current, firstChildDesc?.dom.parentElement ?? null, nodeDomRef.current, (event)=>!!stopEvent.current(event)); + } else { + nodeViewDescRef.current.parent = parentRef.current; + nodeViewDescRef.current.children = childDescriptors.current; + nodeViewDescRef.current.node = node; + nodeViewDescRef.current.pos = getPos(); + nodeViewDescRef.current.outerDeco = outerDecorations; + nodeViewDescRef.current.innerDeco = innerDecorations; + nodeViewDescRef.current.dom = domRef?.current ?? nodeDomRef.current; + // @ts-expect-error We have our own ViewDesc implementations + nodeViewDescRef.current.dom.pmViewDesc = nodeViewDescRef.current; + nodeViewDescRef.current.contentDOM = // If there's already a contentDOM, we can just + // keep it; it won't have changed. This is especially + // important during compositions, where the + // firstChildDesc might not have a correct dom node set yet. + contentDOMRef?.current ?? nodeViewDescRef.current.contentDOM ?? firstChildDesc?.dom.parentElement ?? null; + nodeViewDescRef.current.nodeDOM = nodeDomRef.current; + } + setHasContentDOM(nodeViewDescRef.current.contentDOM !== null); + if (!siblingsRef.current.includes(nodeViewDescRef.current)) { + siblingsRef.current.push(nodeViewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + for (const childDesc of childDescriptors.current){ + childDesc.parent = nodeViewDescRef.current; + // Because TextNodeViews can't locate the DOM nodes + // for compositions, we need to override them here + if (childDesc instanceof _viewdescJs.CompositionViewDesc) { + const compositionTopDOM = nodeViewDescRef.current.contentDOM?.firstChild; + if (!compositionTopDOM) throw new Error(`Started a composition but couldn't find the text node it belongs to.`); + let textDOM = compositionTopDOM; + while(textDOM.firstChild){ + textDOM = textDOM.firstChild; + } + if (!textDOM || !(textDOM instanceof Text)) throw new Error(`Started a composition but couldn't find the text node it belongs to.`); + childDesc.dom = compositionTopDOM; + childDesc.textDOM = textDOM; + childDesc.text = textDOM.data; + // @ts-expect-error ??? + childDesc.textDOM.pmViewDesc = childDesc; + // @ts-expect-error ??? + view?.input.compositionNodes.push(childDesc); + } + } + return ()=>{ + if (nodeViewDescRef.current?.children[0] instanceof _viewdescJs.CompositionViewDesc && !view?.composing) { + nodeViewDescRef.current?.children[0].dom.parentNode?.removeChild(nodeViewDescRef.current?.children[0].dom); + } + }; + }); + return { + hasContentDOM, + childDescriptors, + nodeViewDescRef, + setStopEvent + }; +} diff --git a/dist/cjs/hooks/useReactKeys.js b/dist/cjs/hooks/useReactKeys.js new file mode 100644 index 00000000..d2edacd2 --- /dev/null +++ b/dist/cjs/hooks/useReactKeys.js @@ -0,0 +1,15 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useReactKeys", { + enumerable: true, + get: ()=>useReactKeys +}); +const _react = require("react"); +const _editorContextJs = require("../contexts/EditorContext.js"); +const _reactKeysJs = require("../plugins/reactKeys.js"); +function useReactKeys() { + const { view } = (0, _react.useContext)(_editorContextJs.EditorContext); + return view && _reactKeysJs.reactKeysPluginKey.getState(view.state); +} diff --git a/dist/cjs/hooks/useStopEvent.js b/dist/cjs/hooks/useStopEvent.js new file mode 100644 index 00000000..59d93800 --- /dev/null +++ b/dist/cjs/hooks/useStopEvent.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "useStopEvent", { + enumerable: true, + get: ()=>useStopEvent +}); +const _react = require("react"); +const _stopEventContextJs = require("../contexts/StopEventContext.js"); +const _useEditorEffectJs = require("./useEditorEffect.js"); +const _useEditorEventCallbackJs = require("./useEditorEventCallback.js"); +function useStopEvent(stopEvent) { + const register = (0, _react.useContext)(_stopEventContextJs.StopEventContext); + const stopEventMemo = (0, _useEditorEventCallbackJs.useEditorEventCallback)(stopEvent); + (0, _useEditorEffectJs.useEditorEffect)(()=>{ + register(stopEventMemo); + }, [ + register, + stopEventMemo + ]); +} diff --git a/dist/cjs/index.js b/dist/cjs/index.js new file mode 100644 index 00000000..005d001c --- /dev/null +++ b/dist/cjs/index.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + ProseMirror: ()=>_proseMirrorJs.ProseMirror, + ProseMirrorDoc: ()=>_proseMirrorDocJs.ProseMirrorDoc, + useEditorEffect: ()=>_useEditorEffectJs.useEditorEffect, + useEditorEventCallback: ()=>_useEditorEventCallbackJs.useEditorEventCallback, + useEditorEventListener: ()=>_useEditorEventListenerJs.useEditorEventListener, + useEditorState: ()=>_useEditorStateJs.useEditorState, + useStopEvent: ()=>_useStopEventJs.useStopEvent, + reactKeys: ()=>_reactKeysJs.reactKeys, + widget: ()=>_reactWidgetTypeJs.widget +}); +const _proseMirrorJs = require("./components/ProseMirror.js"); +const _proseMirrorDocJs = require("./components/ProseMirrorDoc.js"); +const _useEditorEffectJs = require("./hooks/useEditorEffect.js"); +const _useEditorEventCallbackJs = require("./hooks/useEditorEventCallback.js"); +const _useEditorEventListenerJs = require("./hooks/useEditorEventListener.js"); +const _useEditorStateJs = require("./hooks/useEditorState.js"); +const _useStopEventJs = require("./hooks/useStopEvent.js"); +const _reactKeysJs = require("./plugins/reactKeys.js"); +const _reactWidgetTypeJs = require("./decorations/ReactWidgetType.js"); +"use client"; diff --git a/dist/cjs/plugins/__tests__/reactKeys.test.js b/dist/cjs/plugins/__tests__/reactKeys.test.js new file mode 100644 index 00000000..4b225892 --- /dev/null +++ b/dist/cjs/plugins/__tests__/reactKeys.test.js @@ -0,0 +1,81 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _prosemirrorModel = require("prosemirror-model"); +const _prosemirrorState = require("prosemirror-state"); +const _reactKeysJs = require("../reactKeys.js"); +const schema = new _prosemirrorModel.Schema({ + nodes: { + doc: { + content: "block+" + }, + paragraph: { + group: "block", + content: "inline*" + }, + list: { + group: "block", + content: "list_item+" + }, + list_item: { + content: "inline*" + }, + text: { + group: "inline" + } + } +}); +describe("reactNodeViewPlugin", ()=>{ + it("should create a unique key for each node", ()=>{ + const editorState = _prosemirrorState.EditorState.create({ + doc: schema.topNodeType.create(null, [ + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create() + ]), + plugins: [ + (0, _reactKeysJs.reactKeys)() + ] + }); + const pluginState = _reactKeysJs.reactKeysPluginKey.getState(editorState); + expect(pluginState.posToKey.size).toBe(3); + }); + it("should maintain key stability when possible", ()=>{ + const initialEditorState = _prosemirrorState.EditorState.create({ + doc: schema.topNodeType.create(null, [ + schema.nodes.paragraph.create({}, schema.text("Hello")), + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create() + ]), + plugins: [ + (0, _reactKeysJs.reactKeys)() + ] + }); + const initialPluginState = _reactKeysJs.reactKeysPluginKey.getState(initialEditorState); + const nextEditorState = initialEditorState.apply(initialEditorState.tr.insertText(", world!", 6)); + const nextPluginState = _reactKeysJs.reactKeysPluginKey.getState(nextEditorState); + expect(Array.from(initialPluginState.keyToPos.keys())).toEqual(Array.from(nextPluginState.keyToPos.keys())); + }); + it("should create unique keys for new nodes", ()=>{ + const initialEditorState = _prosemirrorState.EditorState.create({ + doc: schema.topNodeType.create(null, [ + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create() + ]), + plugins: [ + (0, _reactKeysJs.reactKeys)() + ] + }); + const initialPluginState = _reactKeysJs.reactKeysPluginKey.getState(initialEditorState); + const nextEditorState = initialEditorState.apply(initialEditorState.tr.insert(0, schema.nodes.list.createAndFill())); + const nextPluginState = _reactKeysJs.reactKeysPluginKey.getState(nextEditorState); + // Adds new keys for new nodes + expect(nextPluginState.keyToPos.size).toBe(5); + // Maintains keys for previous nodes that are still there + Array.from(initialPluginState.keyToPos.keys()).forEach((key)=>{ + expect(Array.from(nextPluginState.keyToPos.keys())).toContain(key); + }); + }); +}); diff --git a/dist/cjs/plugins/beforeInputPlugin.js b/dist/cjs/plugins/beforeInputPlugin.js new file mode 100644 index 00000000..644f61d4 --- /dev/null +++ b/dist/cjs/plugins/beforeInputPlugin.js @@ -0,0 +1,141 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "beforeInputPlugin", { + enumerable: true, + get: ()=>beforeInputPlugin +}); +const _prosemirrorState = require("prosemirror-state"); +const _cursorWrapperJs = require("../components/CursorWrapper.js"); +const _reactWidgetTypeJs = require("../decorations/ReactWidgetType.js"); +const _reactKeysJs = require("./reactKeys.js"); +function insertText(view, eventData) { + let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}; + if (eventData === null) return false; + const from = options.from ?? view.state.selection.from; + const to = options.to ?? view.state.selection.to; + if (view.someProp("handleTextInput", (f)=>f(view, from, to, eventData))) { + return true; + } + const { tr } = view.state; + if (options.marks) tr.ensureMarks(options.marks); + tr.insertText(eventData, from, to); + if (options.bust) { + const $from = view.state.doc.resolve(from); + const sharedAncestorDepth = $from.sharedDepth(to); + const sharedAncestorPos = $from.start(sharedAncestorDepth); + const parentKey = _reactKeysJs.reactKeysPluginKey.getState(view.state)?.posToKey.get(sharedAncestorPos - 1); + tr.setMeta(_reactKeysJs.reactKeysPluginKey, { + type: "bustKey", + payload: { + key: parentKey + } + }); + } + view.dispatch(tr); + return true; +} +function beforeInputPlugin(setCursorWrapper) { + let compositionText = null; + let compositionMarks = null; + return new _prosemirrorState.Plugin({ + props: { + handleDOMEvents: { + compositionstart (view) { + const { state } = view; + view.dispatch(state.tr.deleteSelection()); + const $pos = state.selection.$from; + if (state.selection.empty && (state.storedMarks || !$pos.textOffset && $pos.parentOffset && $pos.nodeBefore?.marks.some((m)=>m.type.spec.inclusive === false))) { + setCursorWrapper((0, _reactWidgetTypeJs.widget)(state.selection.from, _cursorWrapperJs.CursorWrapper, { + key: "cursor-wrapper", + marks: state.storedMarks ?? $pos.marks() + })); + } + compositionMarks = state.storedMarks ?? $pos.marks(); + // @ts-expect-error Internal property - input + view.input.composing = true; + return true; + }, + compositionupdate () { + return true; + }, + compositionend (view) { + // @ts-expect-error Internal property - input + view.input.composing = false; + if (compositionText === null) return; + insertText(view, compositionText, { + // TODO: Rather than busting the reactKey cache here, + // which is pretty blunt and doesn't work for + // multi-node replacements, we should attempt to + // snapshot the selected DOM during compositionstart + // and restore it before we end the composition. + // This should allow React to successfully clean up + // and insert the newly composed text, without requiring + // any remounts + bust: true, + marks: compositionMarks + }); + compositionText = null; + compositionMarks = null; + setCursorWrapper(null); + return true; + }, + beforeinput (view, event) { + event.preventDefault(); + switch(event.inputType){ + case "insertCompositionText": + { + if (event.data === null) break; + compositionText = event.data; + break; + } + case "insertReplacementText": + { + const ranges = event.getTargetRanges(); + event.dataTransfer?.items[0]?.getAsString((data)=>{ + for (const range of ranges){ + const from = view.posAtDOM(range.startContainer, range.startOffset, 1); + const to = view.posAtDOM(range.endContainer, range.endOffset, 1); + insertText(view, data, { + from, + to + }); + } + }); + break; + } + case "insertText": + { + insertText(view, event.data); + break; + } + case "deleteWordBackward": + case "deleteContentBackward": + case "deleteWordForward": + case "deleteContentForward": + case "deleteContent": + { + const targetRanges = event.getTargetRanges(); + const { tr } = view.state; + for (const range of targetRanges){ + const start = view.posAtDOM(range.startContainer, range.startOffset); + const end = view.posAtDOM(range.endContainer, range.endOffset); + const { doc } = view.state; + const storedMarks = doc.resolve(start).marksAcross(doc.resolve(end)); + tr.delete(start, end).setStoredMarks(storedMarks); + } + view.dispatch(tr); + break; + } + default: + { + break; + } + } + return true; + } + } + } + }); +} diff --git a/dist/cjs/plugins/componentEventListeners.js b/dist/cjs/plugins/componentEventListeners.js new file mode 100644 index 00000000..810cc94b --- /dev/null +++ b/dist/cjs/plugins/componentEventListeners.js @@ -0,0 +1,33 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "componentEventListeners", { + enumerable: true, + get: ()=>componentEventListeners +}); +const _prosemirrorState = require("prosemirror-state"); +const _reactDom = require("react-dom"); +function componentEventListeners(eventHandlerRegistry) { + const domEventHandlers = {}; + for (const [eventType, handlers] of eventHandlerRegistry.entries()){ + function handleEvent(view, event) { + for (const handler of handlers){ + let handled = false; + (0, _reactDom.unstable_batchedUpdates)(()=>{ + handled = !!handler.call(this, view, event); + }); + if (handled || event.defaultPrevented) return true; + } + return false; + } + domEventHandlers[eventType] = handleEvent; + } + const plugin = new _prosemirrorState.Plugin({ + key: new _prosemirrorState.PluginKey("@nytimes/react-prosemirror/componentEventListeners"), + props: { + handleDOMEvents: domEventHandlers + } + }); + return plugin; +} diff --git a/dist/cjs/plugins/componentEventListenersPlugin.js b/dist/cjs/plugins/componentEventListenersPlugin.js new file mode 100644 index 00000000..183e45f5 --- /dev/null +++ b/dist/cjs/plugins/componentEventListenersPlugin.js @@ -0,0 +1,33 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "createComponentEventListenersPlugin", { + enumerable: true, + get: ()=>createComponentEventListenersPlugin +}); +const _prosemirrorState = require("prosemirror-state"); +const _reactDom = require("react-dom"); +function createComponentEventListenersPlugin(eventHandlerRegistry) { + const domEventHandlers = {}; + for (const [eventType, handlers] of eventHandlerRegistry.entries()){ + function handleEvent(view, event) { + for (const handler of handlers){ + let handled = false; + (0, _reactDom.unstable_batchedUpdates)(()=>{ + handled = !!handler.call(this, view, event); + }); + if (handled || event.defaultPrevented) return true; + } + return false; + } + domEventHandlers[eventType] = handleEvent; + } + const plugin = new _prosemirrorState.Plugin({ + key: new _prosemirrorState.PluginKey("componentEventListeners"), + props: { + handleDOMEvents: domEventHandlers + } + }); + return plugin; +} diff --git a/dist/cjs/plugins/reactKeys.js b/dist/cjs/plugins/reactKeys.js new file mode 100644 index 00000000..d37a5207 --- /dev/null +++ b/dist/cjs/plugins/reactKeys.js @@ -0,0 +1,90 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + createNodeKey: ()=>createNodeKey, + reactKeysPluginKey: ()=>reactKeysPluginKey, + reactKeys: ()=>reactKeys +}); +const _prosemirrorState = require("prosemirror-state"); +function createNodeKey() { + const key = Math.floor(Math.random() * 0xffffffffffff).toString(16); + return key; +} +const reactKeysPluginKey = new _prosemirrorState.PluginKey("@nytimes/react-prosemirror/reactKeys"); +function reactKeys() { + let composing = false; + return new _prosemirrorState.Plugin({ + key: reactKeysPluginKey, + state: { + init (_, state) { + const next = { + posToKey: new Map(), + keyToPos: new Map() + }; + state.doc.descendants((_, pos)=>{ + const key = createNodeKey(); + next.posToKey.set(pos, key); + next.keyToPos.set(key, pos); + return true; + }); + return next; + }, + /** + * Keeps node keys stable across transactions. + * + * To accomplish this, we map each node position forwards + * through the transaction to identify its current position, + * and assign its key to that new position, dropping it if the + * node was deleted. + */ apply (tr, value, _, newState) { + if (!tr.docChanged || composing) return value; + const meta = tr.getMeta(reactKeysPluginKey); + const keyToBust = meta?.type === "bustKey" && meta.payload.key; + const next = { + posToKey: new Map(), + keyToPos: new Map() + }; + const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{ + let [a] = param, [b] = param1; + return a - b; + }); + for (const [pos, key] of posToKeyEntries){ + const { pos: newPos , deleted } = tr.mapping.mapResult(pos); + if (deleted) continue; + let newKey = key; + if (keyToBust === key) { + newKey = createNodeKey(); + } + next.posToKey.set(newPos, newKey); + next.keyToPos.set(newKey, newPos); + } + newState.doc.descendants((_, pos)=>{ + if (next.posToKey.has(pos)) return true; + const key = createNodeKey(); + next.posToKey.set(pos, key); + next.keyToPos.set(key, pos); + return true; + }); + return next; + } + }, + props: { + handleDOMEvents: { + compositionstart: ()=>{ + composing = true; + }, + compositionend: ()=>{ + composing = false; + } + } + } + }); +} diff --git a/dist/cjs/props.js b/dist/cjs/props.js new file mode 100644 index 00000000..dd7e7d1a --- /dev/null +++ b/dist/cjs/props.js @@ -0,0 +1,261 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + kebabCaseToCamelCase: ()=>kebabCaseToCamelCase, + cssToStyles: ()=>cssToStyles, + mergeReactProps: ()=>mergeReactProps, + htmlAttrsToReactProps: ()=>htmlAttrsToReactProps +}); +const _classnames = /*#__PURE__*/ _interopRequireDefault(require("classnames")); +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; +} +function kebabCaseToCamelCase(str) { + return str.replaceAll(/-[a-z]/g, (g)=>g[1]?.toUpperCase() ?? ""); +} +function cssToStyles(css) { + const stylesheet = new CSSStyleSheet(); + stylesheet.insertRule(`* { ${css} }`); + const insertedRule = stylesheet.cssRules[0]; + const declaration = insertedRule.style; + const styles = {}; + for(let i = 0; i < declaration.length; i++){ + const property = declaration.item(i); + const value = declaration.getPropertyValue(property); + const camelCasePropertyName = property.startsWith("--") ? property : kebabCaseToCamelCase(property); + styles[camelCasePropertyName] = value; + } + return styles; +} +function mergeReactProps(a, b) { + return { + ...a, + ...b, + className: (0, _classnames.default)(a.className, b.className), + style: { + ...a.style, + ...b.style + } + }; +} +function htmlAttrsToReactProps(attrs) { + const props = {}; + for (const [attrName, attrValue] of Object.entries(attrs)){ + switch(attrName.toLowerCase()){ + case "class": + { + props.className = attrValue; + break; + } + case "style": + { + props.style = cssToStyles(attrValue); + break; + } + case "autocapitalize": + { + props.autoCapitalize = attrValue; + break; + } + case "contenteditable": + { + if (attrValue === "" || attrValue === "true") { + props.contentEditable = true; + break; + } + if (attrValue === "false") { + props.contentEditable = false; + break; + } + if (attrValue === "plaintext-only") { + props.contentEditable = "plaintext-only"; + break; + } + props.contentEditable = null; + break; + } + case "draggable": + { + props.draggable = attrValue != null; + break; + } + case "enterkeyhint": + { + props.enterKeyHint = attrValue; + break; + } + case "for": + { + props.htmlFor = attrValue; + break; + } + case "hidden": + { + props.hidden = attrValue; + break; + } + case "inputmode": + { + props.inputMode = attrValue; + break; + } + case "itemprop": + { + props.itemProp = attrValue; + break; + } + case "spellcheck": + { + if (attrValue === "" || attrValue === "true") { + props.spellCheck = true; + break; + } + if (attrValue === "false") { + props.spellCheck = false; + break; + } + props.spellCheck = null; + break; + } + case "tabindex": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.tabIndex = numValue; + } + break; + } + case "autocomplete": + { + props.autoComplete = attrValue; + break; + } + case "autofocus": + { + props.autoFocus = attrValue != null; + break; + } + case "formaction": + { + props.formAction = attrValue; + break; + } + case "formenctype": + { + props.formEnctype = attrValue; + break; + } + case "formmethod": + { + props.formMethod = attrValue; + break; + } + case "formnovalidate": + { + props.formNoValidate = attrValue; + break; + } + case "formtarget": + { + props.formTarget = attrValue; + break; + } + case "maxlength": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.maxLength = attrValue; + } + break; + } + case "minlength": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.minLength = attrValue; + } + break; + } + case "max": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.max = attrValue; + } + break; + } + case "min": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.min = attrValue; + } + break; + } + case "multiple": + { + props.multiple = attrValue != null; + break; + } + case "readonly": + { + props.readOnly = attrValue != null; + break; + } + case "required": + { + props.required = attrValue != null; + break; + } + case "size": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.size = attrValue; + } + break; + } + case "step": + { + if (attrValue === "any") { + props.step = attrValue; + break; + } + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue) && numValue > 0) { + props.step = attrValue; + } + break; + } + case "disabled": + { + props.disabled = attrValue != null; + break; + } + case "rows": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.rows = attrValue; + } + break; + } + default: + { + props[attrName] = attrValue; + break; + } + } + } + return props; +} diff --git a/dist/cjs/selection/SelectionDOMObserver.js b/dist/cjs/selection/SelectionDOMObserver.js new file mode 100644 index 00000000..f4a2906f --- /dev/null +++ b/dist/cjs/selection/SelectionDOMObserver.js @@ -0,0 +1,170 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "SelectionDOMObserver", { + enumerable: true, + get: ()=>SelectionDOMObserver +}); +const _prosemirrorState = require("prosemirror-state"); +const _browserJs = require("../browser.js"); +const _domJs = require("../dom.js"); +const _hasFocusAndSelectionJs = require("./hasFocusAndSelection.js"); +const _selectionFromDOMJs = require("./selectionFromDOM.js"); +const _selectionToDOMJs = require("./selectionToDOM.js"); +let SelectionState = class SelectionState { + set(sel) { + this.anchorNode = sel.anchorNode; + this.anchorOffset = sel.anchorOffset; + this.focusNode = sel.focusNode; + this.focusOffset = sel.focusOffset; + } + clear() { + this.anchorNode = this.focusNode = null; + } + eq(sel) { + return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset && sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset; + } + constructor(){ + this.anchorNode = null; + this.anchorOffset = 0; + this.focusNode = null; + this.focusOffset = 0; + } +}; +let SelectionDOMObserver = class SelectionDOMObserver { + connectSelection() { + this.view.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange); + } + disconnectSelection() { + this.view.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange); + } + stop() { + this.disconnectSelection(); + } + start() { + this.connectSelection(); + } + suppressSelectionUpdates() { + this.suppressingSelectionUpdates = true; + setTimeout(()=>this.suppressingSelectionUpdates = false, 50); + } + setCurSelection() { + // @ts-expect-error Internal method + this.currentSelection.set(this.view.domSelectionRange()); + } + ignoreSelectionChange(sel) { + if (!sel.focusNode) return true; + const ancestors = new Set(); + let container; + for(let scan = sel.focusNode; scan; scan = (0, _domJs.parentNode)(scan))ancestors.add(scan); + for(let scan = sel.anchorNode; scan; scan = (0, _domJs.parentNode)(scan))if (ancestors.has(scan)) { + container = scan; + break; + } + // @ts-expect-error Internal property (docView) + const desc = container && this.view.docView.nearestDesc(container); + if (desc && desc.ignoreMutation({ + type: "selection", + target: container?.nodeType == 3 ? container?.parentNode : container + })) { + this.setCurSelection(); + return true; + } + return; + } + registerMutation() { + // pass + } + flushSoon() { + if (this.flushingSoon < 0) this.flushingSoon = window.setTimeout(()=>{ + this.flushingSoon = -1; + this.flush(); + }, 20); + } + updateSelection() { + const { view } = this; + const compositionID = // @ts-expect-error Internal property (input) + view.input.compositionPendingChanges || // @ts-expect-error Internal property (input) + (view.composing ? view.input.compositionID : 0); + // @ts-expect-error Internal property (input) + view.input.compositionPendingChanges = 0; + const origin = // @ts-expect-error Internal property (input) + view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null; + const newSel = (0, _selectionFromDOMJs.selectionFromDOM)(view, origin); + if (newSel && !view.state.selection.eq(newSel)) { + const tr = view.state.tr.setSelection(newSel); + if (origin == "pointer") tr.setMeta("pointer", true); + else if (origin == "key") tr.scrollIntoView(); + if (compositionID) tr.setMeta("composition", compositionID); + view.dispatch(tr); + } + } + selectionToDOM() { + const { view } = this; + (0, _selectionToDOMJs.selectionToDOM)(view); + // @ts-expect-error Internal property (domSelectionRange) + const sel = view.domSelectionRange(); + this.currentSelection.set(sel); + } + flush() { + const { view } = this; + // @ts-expect-error Internal property (docView) + if (!view.docView || this.flushingSoon > -1) return; + // @ts-expect-error Internal property (domSelectionRange) + const sel = view.domSelectionRange(); + const newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && (0, _hasFocusAndSelectionJs.hasFocusAndSelection)(view) && !this.ignoreSelectionChange(sel); + let readSel = null; + // If it looks like the browser has reset the selection to the + // start of the document after focus, restore the selection from + // the state + if (newSel && // @ts-expect-error Internal property (input) + view.input.lastFocus > Date.now() - 200 && // @ts-expect-error Internal property (input) + Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 && (0, _domJs.selectionCollapsed)(sel) && (readSel = (0, _selectionFromDOMJs.selectionFromDOM)(view)) && readSel.eq(_prosemirrorState.Selection.near(view.state.doc.resolve(0), 1))) { + // @ts-expect-error Internal property (input) + view.input.lastFocus = 0; + (0, _selectionToDOMJs.selectionToDOM)(view); + this.currentSelection.set(sel); + // @ts-expect-error Internal property (scrollToSelection) + view.scrollToSelection(); + } else if (newSel) { + this.updateSelection(); + if (!this.currentSelection.eq(sel)) (0, _selectionToDOMJs.selectionToDOM)(view); + this.currentSelection.set(sel); + } + } + selectionChanged(sel) { + return !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && (0, _hasFocusAndSelectionJs.hasFocusAndSelection)(this.view) && !this.ignoreSelectionChange(sel); + } + forceFlush() { + if (this.flushingSoon > -1) { + window.clearTimeout(this.flushingSoon); + this.flushingSoon = -1; + this.flush(); + } + } + onSelectionChange() { + if (!(0, _hasFocusAndSelectionJs.hasFocusAndSelection)(this.view)) return; + if (this.view.composing) return; + if (this.suppressingSelectionUpdates) return (0, _selectionToDOMJs.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 (_browserJs.browser.ie && _browserJs.browser.ie_version <= 11 && !this.view.state.selection.empty) { + // @ts-expect-error Internal method + const sel = this.view.domSelectionRange(); + // Selection.isCollapsed isn't reliable on IE + if (sel.focusNode && (0, _selectionToDOMJs.isEquivalentPosition)(sel.focusNode, sel.focusOffset, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sel.anchorNode, sel.anchorOffset)) return this.flushSoon(); + } + this.flush(); + } + constructor(view){ + this.view = view; + this.flushingSoon = -1; + this.currentSelection = new SelectionState(); + this.suppressingSelectionUpdates = false; + this.view = view; + this.onSelectionChange = this.onSelectionChange.bind(this); + } +}; diff --git a/dist/cjs/selection/hasFocusAndSelection.js b/dist/cjs/selection/hasFocusAndSelection.js new file mode 100644 index 00000000..ccadbee1 --- /dev/null +++ b/dist/cjs/selection/hasFocusAndSelection.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + hasFocusAndSelection: ()=>hasFocusAndSelection, + hasSelection: ()=>hasSelection +}); +function hasFocusAndSelection(view) { + if (view.editable && !view.hasFocus()) return false; + return hasSelection(view); +} +function hasSelection(view) { + // @ts-expect-error Internal method + const sel = view.domSelectionRange(); + 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; + } +} diff --git a/dist/cjs/selection/selectionFromDOM.js b/dist/cjs/selection/selectionFromDOM.js new file mode 100644 index 00000000..21ce4baa --- /dev/null +++ b/dist/cjs/selection/selectionFromDOM.js @@ -0,0 +1,73 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + selectionBetween: ()=>selectionBetween, + selectionFromDOM: ()=>selectionFromDOM +}); +const _prosemirrorState = require("prosemirror-state"); +const _domJs = require("../dom.js"); +function selectionBetween(view, $anchor, $head, bias) { + return view.someProp("createSelectionBetween", (f)=>f(view, $anchor, $head)) || _prosemirrorState.TextSelection.between($anchor, $head, bias); +} +function selectionFromDOM(view) { + let origin = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : null; + // @ts-expect-error Internal method + const domSel = view.domSelectionRange(), doc = view.state.doc; + if (!domSel.focusNode) return null; + // @ts-expect-error Internal method + let nearestDesc = view.docView.nearestDesc(domSel.focusNode); + const inWidget = nearestDesc && nearestDesc.size == 0; + // @ts-expect-error Internal method + let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1); + if (head < 0) return null; + let $head = doc.resolve(head), anchor, selection; + if ((0, _domJs.selectionCollapsed)(domSel)) { + anchor = head; + while(nearestDesc && !nearestDesc.node)nearestDesc = nearestDesc.parent; + const nearestDescNode = nearestDesc.node; + if (nearestDesc && nearestDescNode.isAtom && _prosemirrorState.NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent && !(nearestDescNode.isInline && (0, _domJs.isOnEdge)(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) { + const pos = nearestDesc.posBefore; + selection = new _prosemirrorState.NodeSelection(head == pos ? $head : doc.resolve(pos)); + } + } else { + if (// eslint-disable-next-line @typescript-eslint/no-non-null-assertion + domSel instanceof view.dom.ownerDocument.defaultView.Selection && domSel.rangeCount > 1) { + let min = head, max = head; + for(let i = 0; i < domSel.rangeCount; i++){ + const range = domSel.getRangeAt(i); + min = Math.min(min, // @ts-expect-error Internal method + view.docView.posFromDOM(range.startContainer, range.startOffset, 1)); + max = Math.max(max, // @ts-expect-error Internal method + view.docView.posFromDOM(range.endContainer, range.endOffset, -1)); + } + if (min < 0) return null; + [anchor, head] = max == view.state.selection.anchor ? [ + max, + min + ] : [ + min, + max + ]; + $head = doc.resolve(head); + } else { + // @ts-expect-error Internal method + anchor = view.docView.posFromDOM(// eslint-disable-next-line @typescript-eslint/no-non-null-assertion + domSel.anchorNode, domSel.anchorOffset, 1); + } + if (anchor < 0) return null; + } + const $anchor = doc.resolve(anchor); + if (!selection) { + const bias = origin == "pointer" || view.state.selection.head < $head.pos && !inWidget ? 1 : -1; + selection = selectionBetween(view, $anchor, $head, bias); + } + return selection; +} diff --git a/dist/cjs/selection/selectionToDOM.js b/dist/cjs/selection/selectionToDOM.js new file mode 100644 index 00000000..bca8e778 --- /dev/null +++ b/dist/cjs/selection/selectionToDOM.js @@ -0,0 +1,212 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + isEquivalentPosition: ()=>isEquivalentPosition, + hasBlockDesc: ()=>hasBlockDesc, + domIndex: ()=>domIndex, + nodeSize: ()=>nodeSize, + syncNodeSelection: ()=>syncNodeSelection, + hasSelection: ()=>hasSelection, + selectionToDOM: ()=>selectionToDOM +}); +const _prosemirrorState = require("prosemirror-state"); +const _browserJs = require("../browser.js"); +const isEquivalentPosition = function(node, off, targetNode, targetOff) { + return targetNode && (scanFor(node, off, targetNode, targetOff, -1) || scanFor(node, off, targetNode, targetOff, 1)); +}; +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); +} +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))) { + const 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) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + node = node.childNodes[off + (dir < 0 ? -1 : 0)]; + if (node.contentEditable == "false") return false; + off = dir < 0 ? nodeSize(node) : 0; + } else { + return false; + } + } +} +const domIndex = function(node) { + let n = node; + for(let index = 0;; index++){ + n = n.previousSibling; + if (!n) return index; + } +}; +function nodeSize(node) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length; +} +function syncNodeSelection(view, sel) { + const v = view; + if (sel instanceof _prosemirrorState.NodeSelection) { + const desc = v.docView.descAt(sel.from); + if (desc != v.lastSelectedViewDesc) { + clearNodeSelection(v); + if (desc) desc.selectNode(); + v.lastSelectedViewDesc = desc; + } + } else { + clearNodeSelection(v); + } +} +// Clear all DOM statefulness of the last node selection. +function clearNodeSelection(view) { + const v = view; + if (v.lastSelectedViewDesc) { + if (v.lastSelectedViewDesc.parent) v.lastSelectedViewDesc.deselectNode(); + v.lastSelectedViewDesc = undefined; + } +} +function hasSelection(view) { + const v = view; + const sel = v.domSelectionRange(); + 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 v.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) && (v.editable || v.dom.contains(// eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sel.focusNode.nodeType == 3 ? sel.focusNode.parentNode : sel.focusNode)); + } catch (_) { + return false; + } +} +function editorOwnsSelection(view) { + return view.editable ? view.hasFocus() : hasSelection(view) && document.activeElement && document.activeElement.contains(view.dom); +} +function selectCursorWrapper(view) { + const v = view; + const domSel = v.domSelection(), range = document.createRange(); + if (!domSel) return; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const node = v.cursorWrapper.dom, img = node.nodeName == "IMG"; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (img) range.setStart(node.parentNode, domIndex(node) + 1); + else range.setStart(node, 0); + range.collapse(true); + 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 && !v.state.selection.visible && _browserJs.browser.ie && _browserJs.browser.ie_version <= 11) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + node.disabled = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + node.disabled = false; + } +} +function temporarilyEditableNear(view, pos) { + const v = view; + const { node , offset } = v.docView.domFromPos(pos, 0); + const after = offset < node.childNodes.length ? node.childNodes[offset] : null; + const before = offset ? node.childNodes[offset - 1] : null; + if (_browserJs.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); + } + return; +} +function setEditable(element) { + element.contentEditable = "true"; + if (_browserJs.browser.safari && element.draggable) { + element.draggable = false; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element.wasDraggable = true; + } + return element; +} +function resetEditable(element) { + element.contentEditable = "false"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (element.wasDraggable) { + element.draggable = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element.wasDraggable = null; + } +} +function removeClassOnSelectionChange(view) { + const v = view; + const doc = v.dom.ownerDocument; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + doc.removeEventListener("selectionchange", v.input.hideSelectionGuard); + const domSel = v.domSelectionRange(); + const node = domSel.anchorNode, offset = domSel.anchorOffset; + doc.addEventListener("selectionchange", v.input.hideSelectionGuard = ()=>{ + if (domSel.anchorNode != node || domSel.anchorOffset != offset) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + doc.removeEventListener("selectionchange", v.input.hideSelectionGuard); + setTimeout(()=>{ + if (!editorOwnsSelection(v) || v.state.selection.visible) v.dom.classList.remove("ProseMirror-hideselection"); + }, 20); + } + }); +} +const brokenSelectBetweenUneditable = _browserJs.browser.safari || _browserJs.browser.chrome && _browserJs.browser.chrome_version < 63; +function selectionToDOM(view) { + let force = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + const v = view; + const sel = v.state.selection; + syncNodeSelection(v, sel); + if (!editorOwnsSelection(v)) 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 && v.input.mouseDown && v.input.mouseDown.allowDefault && _browserJs.browser.chrome) { + const domSel = v.domSelectionRange(), curSel = v.domObserver.currentSelection; + if (domSel.anchorNode && curSel.anchorNode && isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset)) { + v.input.mouseDown.delayedSelectionSync = true; + v.domObserver.setCurSelection(); + return; + } + } + v.domObserver.disconnectSelection(); + if (v.cursorWrapper) { + selectCursorWrapper(v); + } else { + const { anchor , head } = sel; + let resetEditableFrom; + let resetEditableTo; + if (brokenSelectBetweenUneditable && !(sel instanceof _prosemirrorState.TextSelection)) { + if (!sel.$from.parent.inlineContent) resetEditableFrom = temporarilyEditableNear(v, sel.from); + if (!sel.empty && !sel.$from.parent.inlineContent) resetEditableTo = temporarilyEditableNear(v, sel.to); + } + v.docView.setSelection(anchor, head, v.root, force); + if (brokenSelectBetweenUneditable) { + if (resetEditableFrom) resetEditable(resetEditableFrom); + if (resetEditableTo) resetEditable(resetEditableTo); + } + if (sel.visible) { + v.dom.classList.remove("ProseMirror-hideselection"); + } else { + v.dom.classList.add("ProseMirror-hideselection"); + if ("onselectionchange" in document) removeClassOnSelectionChange(v); + } + } + v.domObserver.setCurSelection(); + v.domObserver.connectSelection(); +} diff --git a/dist/cjs/testing/editorViewTestHelpers.js b/dist/cjs/testing/editorViewTestHelpers.js new file mode 100644 index 00000000..dc6caee5 --- /dev/null +++ b/dist/cjs/testing/editorViewTestHelpers.js @@ -0,0 +1,117 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + tempEditor: ()=>tempEditor, + findTextNode: ()=>findTextNode +}); +const _react = require("@testing-library/react"); +const _expect = require("expect"); +const _prosemirrorModel = require("prosemirror-model"); +const _prosemirrorState = require("prosemirror-state"); +const _prosemirrorTestBuilder = require("prosemirror-test-builder"); +const _react1 = /*#__PURE__*/ _interopRequireDefault(require("react")); +const _proseMirrorJs = require("../components/ProseMirror.js"); +const _proseMirrorDocJs = require("../components/ProseMirrorDoc.js"); +const _useEditorEffectJs = require("../hooks/useEditorEffect.js"); +const _reactKeysJs = require("../plugins/reactKeys.js"); +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; +} +const toEqualNode = function(actual, expected) { + if (!(actual instanceof _prosemirrorModel.Node && expected instanceof _prosemirrorModel.Node)) { + throw new Error("Must be comparing nodes"); + } + const pass = (0, _prosemirrorTestBuilder.eq)(actual, expected); + return { + message: ()=>// `this` context will have correct typings + `expected ${this.utils.printReceived(actual)} ${pass ? "not " : ""}to equal ${this.utils.printExpected(expected)}`, + pass + }; +}; +_expect.expect.extend({ + toEqualNode +}); +function tempEditor(param) { + let { doc: startDoc , selection , controlled , plugins , ...props } = param; + startDoc = startDoc ?? (0, _prosemirrorTestBuilder.doc)((0, _prosemirrorTestBuilder.p)()); + const state = _prosemirrorState.EditorState.create({ + doc: startDoc, + schema: _prosemirrorTestBuilder.schema, + selection: selection ?? startDoc.tag?.a ? _prosemirrorState.TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined, + plugins: [ + ...plugins ?? [], + (0, _reactKeysJs.reactKeys)() + ] + }); + let view = null; + function Test() { + (0, _useEditorEffectJs.useEditorEffect)((v)=>{ + view = v; + }); + return null; + } + const { rerender , unmount } = (0, _react.render)(/*#__PURE__*/ _react1.default.createElement(_proseMirrorJs.ProseMirror, _extends({}, controlled ? { + state + } : { + defaultState: state + }, props), /*#__PURE__*/ _react1.default.createElement(Test, null), /*#__PURE__*/ _react1.default.createElement(_proseMirrorDocJs.ProseMirrorDoc, null))); + function rerenderEditor() { + let { ...newProps } = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; + rerender(/*#__PURE__*/ _react1.default.createElement(_proseMirrorJs.ProseMirror, _extends({}, controlled ? { + state + } : { + defaultState: state + }, { + ...props, + ...newProps + }), /*#__PURE__*/ _react1.default.createElement(Test, null), /*#__PURE__*/ _react1.default.createElement(_proseMirrorDocJs.ProseMirrorDoc, null))); + return view; + } + // We need two renders for the hasContentDOM state to settle + rerenderEditor(); + return { + view: view, + rerender: rerenderEditor, + unmount + }; +} +function findTextNodeInner(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){ + const found = findTextNodeInner(ch, text); + if (found) return found; + } + } + return undefined; +} +function findTextNode(node, text) { + const found = findTextNodeInner(node, text); + if (found) return found; + throw new Error("Unable to find matching text node"); +} diff --git a/dist/cjs/testing/setupProseMirrorView.js b/dist/cjs/testing/setupProseMirrorView.js new file mode 100644 index 00000000..2d645ef6 --- /dev/null +++ b/dist/cjs/testing/setupProseMirrorView.js @@ -0,0 +1,90 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + setupProseMirrorView: ()=>setupProseMirrorView, + teardownProseMirrorView: ()=>teardownProseMirrorView +}); +let oldElementFromPoint; +let oldGetClientRects; +let oldGetBoundingClientRect; +const mockElementFromPoint = ()=>globalThis.document.body; +const mockGetBoundingClientRect = ()=>{ + return { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0, + toJSON () { + return { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0 + }; + } + }; +}; +const mockGetClientRects = ()=>{ + const list = [ + { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0, + toJSON () { + return { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0 + }; + } + } + ]; + const domRectList = Object.assign(list, { + item (index) { + return list[index] ?? null; + } + }); + return domRectList; +}; +function setupProseMirrorView() { + oldElementFromPoint = Document.prototype.elementFromPoint; + Document.prototype.elementFromPoint = mockElementFromPoint; + oldGetClientRects = Range.prototype.getClientRects; + Range.prototype.getClientRects = mockGetClientRects; + oldGetBoundingClientRect = Range.prototype.getBoundingClientRect; + Range.prototype.getBoundingClientRect = mockGetBoundingClientRect; +} +function teardownProseMirrorView() { + // @ts-expect-error jsdom actually doesn't implement these, so they might be undefined + Document.prototype.elementFromPoint = oldElementFromPoint; + // @ts-expect-error jsdom actually doesn't implement these, so they might be undefined + Range.prototype.getClientRects = oldGetClientRects; + // @ts-expect-error jsdom actually doesn't implement these, so they might be undefined + Range.prototype.getBoundingClientRect = oldGetBoundingClientRect; +} diff --git a/dist/cjs/viewdesc.js b/dist/cjs/viewdesc.js new file mode 100644 index 00000000..e47afe55 --- /dev/null +++ b/dist/cjs/viewdesc.js @@ -0,0 +1,653 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +function _export(target, all) { + for(var name in all)Object.defineProperty(target, name, { + enumerable: true, + get: all[name] + }); +} +_export(exports, { + ViewDesc: ()=>ViewDesc, + WidgetViewDesc: ()=>WidgetViewDesc, + CompositionViewDesc: ()=>CompositionViewDesc, + MarkViewDesc: ()=>MarkViewDesc, + NodeViewDesc: ()=>NodeViewDesc, + TextViewDesc: ()=>TextViewDesc, + TrailingHackViewDesc: ()=>TrailingHackViewDesc +}); +const _prosemirrorModel = require("prosemirror-model"); +const _browserJs = require("./browser.js"); +const _selectionToDOMJs = require("./selection/selectionToDOM.js"); +// 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; +let ViewDesc = class ViewDesc { + // Used to check whether a given description corresponds to a + // widget/mark/node. + matchesWidget(_widget) { + return false; + } + matchesMark(_mark) { + return false; + } + matchesNode(_node, _outerDeco, _innerDeco) { + return false; + } + // @ts-expect-error ... + matchesHack(nodeName) { + return false; + } + // 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; + } + // Used by the editor's event handler to ignore events that come + // from certain descs. + // @ts-expect-error ... + stopEvent(event) { + 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++)// @ts-expect-error ... + 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() { + // pass + } + posBeforeChild(child) { + for(let i = 0, pos = this.posAtStart;; i++){ + const cur = this.children[i]; + if (cur == child) return pos; + // @ts-expect-error ... + 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; + } + 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 > (0, _selectionToDOMJs.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.previousSibling) break; + } + if (atEnd == null && offset == dom.childNodes.length) for(let search = dom;; search = search.parentNode){ + if (search == this.dom) { + atEnd = true; + break; + } + if (search.nextSibling) break; + } + } + return (atEnd == null ? bias > 0 : atEnd) ? this.posAtEnd : this.posAtStart; + } + nearestDesc(dom) { + let onlyNodes = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + for(let first = true, cur = dom; cur; cur = cur.parentNode){ + const desc = this.getDesc(cur); + let nodeDOM; + if (desc && (!onlyNodes || desc.node)) { + // If dom is outside of this desc's nodeDOM, don't count it. + if (first && (nodeDOM = desc.nodeDOM) && !(nodeDOM.nodeType == 1 ? nodeDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode) : nodeDOM == dom)) first = false; + else return desc; + } + } + return; + } + getDesc(dom) { + const desc = dom.pmViewDesc; + for(let cur = desc; cur; cur = cur.parent)if (cur == this) return desc; + return; + } + posFromDOM(dom, offset, bias) { + for(let scan = dom; scan; scan = scan.parentNode){ + const desc = this.getDesc(scan); + if (desc) return desc.localPosFromDOM(dom, offset, bias); + } + return -1; + } + // 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]; + const 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; + } + return; + } + domFromPos(pos, side) { + if (!this.contentDOM) return { + node: this.dom, + offset: 0, + atom: pos + 1 + }; + // First find the position in the child array + let i = 0, offset = 0; + for(let curPos = 0; i < this.children.length; i++){ + const 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.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 ? (0, _selectionToDOMJs.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 ? (0, _selectionToDOMJs.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) { + let base = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : 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++){ + const child = this.children[i], end = offset + child.size; + if (fromOffset == -1 && from <= end) { + const 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--){ + const prev = this.children[j - 1]; + if (prev.size && prev.dom.parentNode == this.contentDOM && !prev.emptyChildAt(1)) { + fromOffset = (0, _selectionToDOMJs.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++){ + const next = this.children[j]; + if (next.size && next.dom.parentNode == this.contentDOM && !next.emptyChildAt(-1)) { + toOffset = (0, _selectionToDOMJs.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; + const child = this.children[side < 0 ? 0 : this.children.length - 1]; + // @ts-expect-error ... + return child.size == 0 || child.emptyChildAt(side); + } + domAfterPos(pos) { + const { node , offset } = this.domFromPos(pos, 0); + if (node.nodeType != 1 || offset == node.childNodes.length) throw new RangeError("No node after pos " + pos); + // @ts-expect-error ... + return node.childNodes[offset]; + } + // 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) { + let force = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : false; + // If the selection falls entirely in a child, give it to that child + const from = Math.min(anchor, head), to = Math.max(anchor, head); + for(let i = 0, offset = 0; i < this.children.length; i++){ + const 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); + const 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 ((_browserJs.browser.gecko || _browserJs.browser.safari) && anchor == head) { + const { 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: (0, _selectionToDOMJs.domIndex)(after) + 1 + }; + break; + } + const desc = scan.pmViewDesc; + if (desc && desc.node && desc.node.isBlock) break; + } + } + } else { + const prev = node.childNodes[offset - 1]; + // @ts-expect-error ... + 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 (_browserJs.browser.gecko && domSel.focusNode && domSel.focusNode != headDOM.node && domSel.focusNode.nodeType == 1) { + const after = domSel.focusNode.childNodes[domSel.focusOffset]; + if (after && after.contentEditable == "false") force = true; + } + if (!(force || brKludge && _browserJs.browser.safari) && (0, _selectionToDOMJs.isEquivalentPosition)(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) && (0, _selectionToDOMJs.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 (_) { + // 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. + // Similarly, this could crash on Safari if the editor is hidden, and + // there was no selection. + } + } + if (!domSelExtended) { + if (anchor > head) { + const tmp = anchorDOM; + anchorDOM = headDOM; + headDOM = tmp; + } + const range = document.createRange(); + range.setEnd(headDOM.node, headDOM.offset); + range.setStart(anchorDOM.node, anchorDOM.offset); + domSel.removeAllRanges(); + domSel.addRange(range); + } + } + 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++){ + const child = this.children[i], end = offset + child.size; + if (offset == end ? from <= end && to >= offset : from < end && to > offset) { + const 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++){ + const dirty = level == 1 ? CONTENT_DIRTY : CHILD_DIRTY; + if (node.dirty < dirty) node.dirty = dirty; + } + } + get domAtom() { + return false; + } + get ignoreForCoords() { + return false; + } + constructor(parent, children, pos, dom, contentDOM){ + this.parent = parent; + this.children = children; + this.pos = pos; + this.dom = dom; + this.contentDOM = contentDOM; + this.dirty = NOT_DIRTY; + // An expando property on the DOM node provides a link back to its + // description. + // @ts-expect-error We're using custom view implementations here but + // we match the API so this is relatively safe. + dom.pmViewDesc = this; + } +}; +let WidgetViewDesc = class WidgetViewDesc extends ViewDesc { + matchesWidget(widget) { + return this.dirty == NOT_DIRTY && widget.type.eq(this.widget.type); + } + parseRule() { + return { + ignore: true + }; + } + stopEvent(event) { + const stop = this.widget.spec.stopEvent; + return stop ? stop(event) : false; + } + ignoreMutation(mutation) { + return mutation.type != "selection" || this.widget.spec.ignoreSelection; + } + get domAtom() { + return true; + } + get side() { + return this.widget.type.side; + } + constructor(parent, pos, widget, dom){ + super(parent, [], pos, dom, null); + this.widget = widget; + this.widget = widget; + } +}; +let CompositionViewDesc = class CompositionViewDesc extends ViewDesc { + 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; + } + constructor(parent, pos, dom, textDOM, text){ + super(parent, [], pos, dom, null); + this.textDOM = textDOM; + this.text = text; + } +}; +let MarkViewDesc = class MarkViewDesc extends ViewDesc { + 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; + } + } + constructor(parent, children, pos, mark, dom, contentDOM){ + super(parent, children, pos, dom, contentDOM); + this.mark = mark; + } +}; +let NodeViewDesc = class NodeViewDesc extends ViewDesc { + updateOuterDeco() { + // pass + } + 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 + const 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--){ + const child = this.children[i]; + // @ts-expect-error ... + if (this.dom.contains(child.dom.parentNode)) { + // @ts-expect-error ... + rule.contentElement = child.dom.parentNode; + break; + } + } + if (!rule.contentElement) rule.getContent = ()=>_prosemirrorModel.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; + } + // If this desc must be updated to match the given node decoration, + // do so and return true. + update(_node, _outerDeco, _innerDeco, _view) { + return true; + } + // Mark this node as being the selected node. + selectNode() { + if (this.nodeDOM.nodeType == 1) 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() { + if (this.nodeDOM.nodeType == 1) { + this.nodeDOM.classList.remove("ProseMirror-selectednode"); + if (this.contentDOM || !this.node.type.spec.draggable) this.dom.removeAttribute("draggable"); + } + } + get domAtom() { + return this.node.isAtom; + } + constructor(parent, children, pos, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, stopEvent){ + super(parent, children, pos, dom, contentDOM); + this.node = node; + this.outerDeco = outerDeco; + this.innerDeco = innerDeco; + this.nodeDOM = nodeDOM; + this.stopEvent = stopEvent; + } +}; +let TextViewDesc = class TextViewDesc extends NodeViewDesc { + parseRule() { + let skip = this.nodeDOM.parentNode; + while(skip && skip != this.dom && !skip.pmIsDeco)skip = skip.parentNode; + return { + skip: skip || true + }; + } + update(_node, _outerDeco, _innerDeco, _view) { + return true; + } + inParent() { + const 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"; + } + 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; + } + constructor(parent, children, pos, node, outerDeco, innerDeco, dom, nodeDOM){ + super(parent, children, pos, node, outerDeco, innerDeco, dom, null, nodeDOM, ()=>false); + } +}; +let TrailingHackViewDesc = 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"; + } +}; +function sameOuterDeco(a, b) { + if (a.length != b.length) return false; + // @ts-expect-error ... + for(let i = 0; i < a.length; i++)if (!a[i].type.eq(b[i].type)) return false; + return true; +} diff --git a/dist/esm/browser.js b/dist/esm/browser.js new file mode 100644 index 00000000..2bf09d4a --- /dev/null +++ b/dist/esm/browser.js @@ -0,0 +1,43 @@ +const nav = typeof navigator != "undefined" ? navigator : null; +const doc = typeof document != "undefined" ? document : null; +const agent = nav && nav.userAgent || ""; +const ie_edge = /Edge\/(\d+)/.exec(agent); +const ie_upto10 = /MSIE \d/.exec(agent); +const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent); +const ie = !!(ie_upto10 || ie_11up || ie_edge); +const ie_version = ie_upto10 ? document.documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0; +const gecko = !ie && /gecko\/(\d+)/i.test(agent); +const gecko_version = gecko && +(/Firefox\/(\d+)/.exec(agent) || [ + 0, + 0 +])[1]; +const _chrome = !ie && /Chrome\/(\d+)/.exec(agent); +const chrome = !!_chrome; +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +const chrome_version = _chrome ? +_chrome[1] : 0; +const safari = !ie && !!nav && /Apple Computer/.test(nav.vendor); +// Is true for both iOS and iPadOS for convenience +const ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2); +const mac = ios || (nav ? /Mac/.test(nav.platform) : false); +const windows = nav ? /Win/.test(nav.platform) : false; +const android = /Android \d/.test(agent); +const webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style; +const webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [ + 0, + 0 +])[1] : 0; +export const browser = { + ie, + ie_version, + gecko, + gecko_version, + chrome, + chrome_version, + safari, + ios, + mac, + windows, + android, + webkit, + webkit_version +}; diff --git a/dist/esm/components/ChildNodeViews.js b/dist/esm/components/ChildNodeViews.js new file mode 100644 index 00000000..f5fd6131 --- /dev/null +++ b/dist/esm/components/ChildNodeViews.js @@ -0,0 +1,292 @@ +import React, { cloneElement, createElement, memo, useContext, useRef } from "react"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { EditorContext } from "../contexts/EditorContext.js"; +import { iterDeco } from "../decorations/iterDeco.js"; +// import { useEditorState } from "../hooks/useEditorState.js"; +import { useReactKeys } from "../hooks/useReactKeys.js"; +import { htmlAttrsToReactProps, mergeReactProps } from "../props.js"; +import { MarkView } from "./MarkView.js"; +import { NativeWidgetView } from "./NativeWidgetView.js"; +import { NodeView } from "./NodeView.js"; +import { SeparatorHackView } from "./SeparatorHackView.js"; +import { TextNodeView } from "./TextNodeView.js"; +import { TrailingHackView } from "./TrailingHackView.js"; +import { WidgetView } from "./WidgetView.js"; +export function wrapInDeco(reactNode, deco) { + const { nodeName , ...attrs } = deco.type.attrs; + const props = htmlAttrsToReactProps(attrs); + // We auto-wrap text nodes in spans so that we can apply attributes + // and styles, but we want to avoid double-wrapping the same + // text node + if (nodeName || typeof reactNode === "string") { + return /*#__PURE__*/ createElement(nodeName ?? "span", props, reactNode); + } + return /*#__PURE__*/ cloneElement(reactNode, mergeReactProps(reactNode.props, props)); +} +const ChildView = /*#__PURE__*/ memo(function ChildView(param) { + let { child , getInnerPos } = param; + const { view } = useContext(EditorContext); + const getChildPos = useRef(()=>getInnerPos.current() + child.offset); + getChildPos.current = ()=>getInnerPos.current() + child.offset; + const reactKeys = useReactKeys(); + const key = createKey(getInnerPos.current(), child, reactKeys?.posToKey); + return child.type === "widget" ? /*#__PURE__*/ React.createElement(WidgetView, { + key: key, + widget: child.widget, + getPos: getChildPos + }) : child.type === "native-widget" ? /*#__PURE__*/ React.createElement(NativeWidgetView, { + key: key, + widget: child.widget, + getPos: getChildPos + }) : child.node.isText ? /*#__PURE__*/ React.createElement(ChildDescriptorsContext.Consumer, { + key: key + }, (param)=>/*#__PURE__*/ { + let { siblingsRef , parentRef } = param; + return React.createElement(TextNodeView, { + view: view, + node: child.node, + getPos: getChildPos, + siblingsRef: siblingsRef, + parentRef: parentRef, + decorations: child.outerDeco + }); + }) : /*#__PURE__*/ React.createElement(NodeView, { + key: key, + node: child.node, + getPos: getChildPos, + outerDeco: child.outerDeco, + innerDeco: child.innerDeco + }); +}); +const InlinePartition = /*#__PURE__*/ memo(function InlinePartition(param) { + let { childViews , getInnerPos } = param; + const reactKeys = useReactKeys(); + const firstChild = childViews[0]; + const getFirstChildPos = useRef(()=>getInnerPos.current() + firstChild.offset); + getFirstChildPos.current = ()=>getInnerPos.current() + firstChild.offset; + const firstMark = firstChild.marks[0]; + if (!firstMark) { + return /*#__PURE__*/ React.createElement(React.Fragment, null, childViews.map((child)=>{ + const key = createKey(getInnerPos.current(), child, reactKeys?.posToKey); + return /*#__PURE__*/ React.createElement(ChildView, { + key: key, + child: child, + getInnerPos: getInnerPos + }); + })); + } + return /*#__PURE__*/ React.createElement(MarkView, { + getPos: getFirstChildPos, + key: createKey(// editorState?.doc, + getInnerPos.current(), firstChild, reactKeys?.posToKey), + mark: firstMark + }, /*#__PURE__*/ React.createElement(InlineView, { + key: createKey(// editorState?.doc, + getInnerPos.current(), firstChild, reactKeys?.posToKey), + getInnerPos: getInnerPos, + childViews: childViews.map((child)=>({ + ...child, + marks: child.marks.slice(1) + })) + })); +}); +const InlineView = /*#__PURE__*/ memo(function InlineView(param) { + let { getInnerPos , childViews } = param; + // const editorState = useEditorState(); + const reactKeys = useReactKeys(); + const partitioned = childViews.reduce((acc, child)=>{ + const lastPartition = acc[acc.length - 1]; + if (!lastPartition) { + return [ + [ + child + ] + ]; + } + const lastChild = lastPartition[lastPartition.length - 1]; + if (!lastChild) { + return [ + ...acc.slice(0, acc.length), + [ + child + ] + ]; + } + if (!child.marks.length && !lastChild.marks.length || child.marks.length && lastChild.marks.length && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + child.marks[0]?.eq(lastChild.marks[0])) { + return [ + ...acc.slice(0, acc.length - 1), + [ + ...lastPartition.slice(0, lastPartition.length), + child + ] + ]; + } + return [ + ...acc, + [ + child + ] + ]; + }, []); + return /*#__PURE__*/ React.createElement(React.Fragment, null, partitioned.map((childViews)=>{ + const firstChild = childViews[0]; + if (!firstChild) return null; + const key = createKey(getInnerPos.current(), firstChild, reactKeys?.posToKey); + return /*#__PURE__*/ React.createElement(InlinePartition, { + key: key, + childViews: childViews, + getInnerPos: getInnerPos + }); + })); +}); +function createKey(// doc: Node | undefined, +innerPos, child, posToKey) { + const pos = innerPos + child.offset; + const key = posToKey?.get(pos); + if (child.type === "widget" || child.type === "native-widget") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (child.widget.type.spec.key) // eslint-disable-next-line @typescript-eslint/no-explicit-any + return child.widget.type.spec.key; + // eslint-disable-next-line no-console + console.warn(`Widget at position ${pos} doesn't have a key specified. This has negative performance implications.`); + return `${key}-${child.index}`; + } + if (key) return key; + // if (!doc) return pos; + // const parentPos = doc.resolve(pos).start() - 1; + // const parentKey = posToKey?.get(parentPos); + // if (parentKey) return `${parentKey}-${child.offset}`; + return pos; +} +function adjustWidgetMarksForward(children) { + const lastChild = children[children.length - 1]; + if (lastChild?.type !== "widget" && lastChild?.type !== "native-widget" || // Using internal Decoration property, "type" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lastChild.widget.type.side >= 0) return; + let lastNodeChild = null; + for(let i = children.length - 2; i >= 0; i--){ + const child = children[i]; + if (child?.type === "node") { + lastNodeChild = child; + break; + } + } + if (!lastNodeChild || !lastNodeChild.node.isInline) return; + const marksToSpread = lastNodeChild.marks; + lastChild.marks = lastChild.marks.reduce((acc, mark)=>mark.addToSet(acc), marksToSpread); +} +function adjustWidgetMarksBack(children) { + const lastChild = children[children.length - 1]; + if (lastChild?.type !== "node" || !lastChild.node.isInline) return; + const marksToSpread = lastChild.marks; + for(let i = children.length - 2; i >= 0; i--){ + const child = children[i]; + if (child?.type !== "widget" && child?.type !== "native-widget" || // Using internal Decoration property, "type" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + child.widget.type.side < 0) break; + child.marks = child.marks.reduce((acc, mark)=>mark.addToSet(acc), marksToSpread); + } +} +const ChildElement = /*#__PURE__*/ memo(function ChildElement(param) { + let { child , getInnerPos , posToKey } = param; + const getNodePos = useRef(()=>getInnerPos.current() + child.offset); + getNodePos.current = ()=>getInnerPos.current() + child.offset; + const key = createKey(getInnerPos.current(), child, posToKey); + if (child.type === "node") { + return /*#__PURE__*/ React.createElement(NodeView, { + key: key, + outerDeco: child.outerDeco, + node: child.node, + innerDeco: child.innerDeco, + getPos: getNodePos + }); + } else { + return /*#__PURE__*/ React.createElement(InlineView, { + key: key, + childViews: [ + child + ], + getInnerPos: getInnerPos + }); + } +}); +function createChildElements(children, getInnerPos, // doc: Node | undefined, +posToKey) { + if (!children.length) return []; + if (children.every((child)=>child.type !== "node" || child.node.isInline)) { + return [ + /*#__PURE__*/ React.createElement(InlineView, { + key: createKey(// doc, + getInnerPos.current(), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + children[0], posToKey), + childViews: children, + getInnerPos: getInnerPos + }) + ]; + } + return children.map((child)=>{ + const key = createKey(getInnerPos.current(), child, posToKey); + return /*#__PURE__*/ React.createElement(ChildElement, { + key: key, + child: child, + posToKey: posToKey, + getInnerPos: getInnerPos + }); + }); +} +export const ChildNodeViews = /*#__PURE__*/ memo(function ChildNodeViews(param) { + let { getPos , node , innerDecorations } = param; + // const editorState = useEditorState(); + const reactKeys = useReactKeys(); + const getInnerPos = useRef(()=>getPos.current() + 1); + if (!node) return null; + const children = []; + iterDeco(node, innerDecorations, (widget, isNative, offset, index)=>{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const widgetMarks = widget.type.spec.marks ?? []; + if (isNative) { + children.push({ + type: "native-widget", + widget: widget, + marks: widgetMarks, + offset, + index + }); + } else { + children.push({ + type: "widget", + widget: widget, + marks: widgetMarks, + offset, + index + }); + } + adjustWidgetMarksForward(children); + }, (childNode, outerDeco, innerDeco, offset)=>{ + children.push({ + type: "node", + node: childNode, + marks: childNode.marks, + innerDeco, + outerDeco, + offset + }); + adjustWidgetMarksBack(children); + }); + const childElements = createChildElements(children, getInnerPos, // editorState.doc, + reactKeys?.posToKey); + const lastChild = children[children.length - 1]; + if (!lastChild || lastChild.type !== "node" || lastChild.node.isInline && !lastChild.node.isText || // RegExp.test actually handles undefined just fine + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + /\n$/.test(lastChild.node.text)) { + childElements.push(/*#__PURE__*/ React.createElement(SeparatorHackView, { + getPos: getInnerPos, + key: "trailing-hack-img" + }), /*#__PURE__*/ React.createElement(TrailingHackView, { + getPos: getInnerPos, + key: "trailing-hack-br" + })); + } + return /*#__PURE__*/ React.createElement(React.Fragment, null, childElements); +}); diff --git a/dist/esm/components/CursorWrapper.js b/dist/esm/components/CursorWrapper.js new file mode 100644 index 00000000..334b5b6b --- /dev/null +++ b/dist/esm/components/CursorWrapper.js @@ -0,0 +1,53 @@ +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import React, { forwardRef, useImperativeHandle, useRef, useState } from "react"; +import { domIndex } from "../dom.js"; +import { useEditorEffect } from "../hooks/useEditorEffect.js"; +export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(param, ref) { + let { widget , getPos , ...props } = param; + const [shouldRender, setShouldRender] = useState(true); + const innerRef = useRef(null); + useImperativeHandle(ref, ()=>{ + return innerRef.current; + }, []); + useEditorEffect((view)=>{ + if (!view || !innerRef.current) return; + // @ts-expect-error Internal property - domObserver + view.domObserver.disconnectSelection(); + // @ts-expect-error Internal property - domSelection + const domSel = view.domSelection(); + const range = document.createRange(); + const node = innerRef.current; + const img = node.nodeName == "IMG"; + if (img && node.parentNode) { + range.setEnd(node.parentNode, domIndex(node) + 1); + } else { + range.setEnd(node, 0); + } + range.collapse(false); + domSel.removeAllRanges(); + domSel.addRange(range); + setShouldRender(false); + // @ts-expect-error Internal property - domObserver + view.domObserver.connectSelection(); + }, []); + return shouldRender ? /*#__PURE__*/ React.createElement("img", _extends({ + ref: innerRef, + className: "ProseMirror-separator", + // eslint-disable-next-line react/no-unknown-property + "mark-placeholder": "true", + alt: "" + }, props)) : null; +}); diff --git a/dist/esm/components/DocNodeView.js b/dist/esm/components/DocNodeView.js new file mode 100644 index 00000000..c69dd974 --- /dev/null +++ b/dist/esm/components/DocNodeView.js @@ -0,0 +1,53 @@ +// TODO: I must be missing something, but I do not know why +// this linting rule is only broken in this file +/* eslint-disable react/prop-types */ import React, { cloneElement, createElement, forwardRef, memo, useImperativeHandle, useMemo, useRef } from "react"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { useNodeViewDescriptor } from "../hooks/useNodeViewDescriptor.js"; +import { ChildNodeViews, wrapInDeco } from "./ChildNodeViews.js"; +const getPos = { + current () { + return -1; + } +}; +export const DocNodeView = /*#__PURE__*/ memo(/*#__PURE__*/ forwardRef(function DocNodeView(param, ref) { + let { className , node , innerDeco , outerDeco , as , viewDesc , ...elementProps } = param; + const innerRef = useRef(null); + useImperativeHandle(ref, ()=>{ + return innerRef.current; + }, []); + const { childDescriptors , nodeViewDescRef } = useNodeViewDescriptor(node, ()=>getPos.current(), innerRef, innerRef, innerDeco, outerDeco, viewDesc); + const childContextValue = useMemo(()=>({ + parentRef: nodeViewDescRef, + siblingsRef: childDescriptors + }), [ + childDescriptors, + nodeViewDescRef + ]); + const props = { + ...elementProps, + ref: innerRef, + className, + suppressContentEditableWarning: true + }; + const element = as ? /*#__PURE__*/ cloneElement(as, props, /*#__PURE__*/ React.createElement(ChildDescriptorsContext.Provider, { + value: childContextValue + }, /*#__PURE__*/ React.createElement(ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + }))) : /*#__PURE__*/ createElement("div", props, /*#__PURE__*/ React.createElement(ChildDescriptorsContext.Provider, { + value: childContextValue + }, /*#__PURE__*/ React.createElement(ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + }))); + if (!node) return element; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeDecorations = outerDeco.filter((deco)=>!deco.inline); + if (!nodeDecorations.length) { + return element; + } + const wrapped = nodeDecorations.reduce(wrapInDeco, element); + return wrapped; +})); diff --git a/dist/esm/components/LayoutGroup.js b/dist/esm/components/LayoutGroup.js new file mode 100644 index 00000000..ef7307a1 --- /dev/null +++ b/dist/esm/components/LayoutGroup.js @@ -0,0 +1,66 @@ +import React, { useCallback, useLayoutEffect, useRef } from "react"; +import { LayoutGroupContext } from "../contexts/LayoutGroupContext.js"; +import { useForceUpdate } from "../hooks/useForceUpdate.js"; +/** + * Provides a boundary for grouping layout effects. + * + * Descendant components can invoke the `useLayoutGroupEffect` hook to register + * effects that run after all descendants within the group have processed their + * regular layout effects. + */ export function LayoutGroup(param) { + let { children } = param; + const createQueue = useRef(new Set()).current; + const destroyQueue = useRef(new Set()).current; + const isMounted = useRef(false); + const forceUpdate = useForceUpdate(); + const isUpdatePending = useRef(true); + const ensureFlush = useCallback(()=>{ + if (!isUpdatePending.current) { + forceUpdate(); + isUpdatePending.current = true; + } + }, [ + forceUpdate + ]); + const register = useCallback((effect)=>{ + let destroy; + const create = ()=>{ + destroy = effect(); + }; + createQueue.add(create); + ensureFlush(); + return ()=>{ + createQueue.delete(create); + if (destroy) { + if (isMounted.current) { + destroyQueue.add(destroy); + ensureFlush(); + } else { + destroy(); + } + } + }; + }, [ + createQueue, + destroyQueue, + ensureFlush + ]); + useLayoutEffect(()=>{ + isUpdatePending.current = false; + createQueue.forEach((create)=>create()); + createQueue.clear(); + return ()=>{ + destroyQueue.forEach((destroy)=>destroy()); + destroyQueue.clear(); + }; + }); + useLayoutEffect(()=>{ + isMounted.current = true; + return ()=>{ + isMounted.current = false; + }; + }, []); + return /*#__PURE__*/ React.createElement(LayoutGroupContext.Provider, { + value: register + }, children); +} diff --git a/dist/esm/components/MarkView.js b/dist/esm/components/MarkView.js new file mode 100644 index 00000000..f516c2d4 --- /dev/null +++ b/dist/esm/components/MarkView.js @@ -0,0 +1,63 @@ +import React, { forwardRef, memo, useContext, useImperativeHandle, useLayoutEffect, useMemo, useRef } from "react"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { MarkViewDesc } from "../viewdesc.js"; +import { OutputSpec } from "./OutputSpec.js"; +export const MarkView = /*#__PURE__*/ memo(/*#__PURE__*/ forwardRef(function MarkView(param, ref) { + let { mark , getPos , children } = param; + const { siblingsRef , parentRef } = useContext(ChildDescriptorsContext); + const viewDescRef = useRef(undefined); + const childDescriptors = useRef([]); + const domRef = useRef(null); + useImperativeHandle(ref, ()=>{ + return domRef.current; + }, []); + const outputSpec = useMemo(()=>mark.type.spec.toDOM?.(mark, true), [ + mark + ]); + if (!outputSpec) throw new Error(`Mark spec for ${mark.type.name} is missing toDOM`); + useLayoutEffect(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + useLayoutEffect(()=>{ + if (!domRef.current) return; + const firstChildDesc = childDescriptors.current[0]; + if (!viewDescRef.current) { + viewDescRef.current = new MarkViewDesc(parentRef.current, childDescriptors.current, getPos.current(), mark, domRef.current, firstChildDesc?.dom.parentElement ?? domRef.current); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.dom = domRef.current; + viewDescRef.current.contentDOM = firstChildDesc?.dom.parentElement ?? domRef.current; + viewDescRef.current.mark = mark; + viewDescRef.current.pos = getPos.current(); + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + for (const childDesc of childDescriptors.current){ + childDesc.parent = viewDescRef.current; + } + }); + const childContextValue = useMemo(()=>({ + parentRef: viewDescRef, + siblingsRef: childDescriptors + }), [ + childDescriptors, + viewDescRef + ]); + return /*#__PURE__*/ React.createElement(OutputSpec, { + ref: domRef, + outputSpec: outputSpec + }, /*#__PURE__*/ React.createElement(ChildDescriptorsContext.Provider, { + value: childContextValue + }, children)); +})); diff --git a/dist/esm/components/NativeWidgetView.js b/dist/esm/components/NativeWidgetView.js new file mode 100644 index 00000000..785f9d4a --- /dev/null +++ b/dist/esm/components/NativeWidgetView.js @@ -0,0 +1,58 @@ +import React, { useContext, useLayoutEffect, useRef } from "react"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { useEditorEffect } from "../hooks/useEditorEffect.js"; +import { WidgetViewDesc } from "../viewdesc.js"; +export function NativeWidgetView(param) { + let { widget , getPos } = param; + const { siblingsRef , parentRef } = useContext(ChildDescriptorsContext); + const viewDescRef = useRef(null); + const rootDomRef = useRef(null); + useLayoutEffect(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + useEditorEffect((view)=>{ + if (!rootDomRef.current) return; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const toDOM = widget.type.toDOM; + let dom = typeof toDOM === "function" ? toDOM(view, ()=>getPos.current()) : toDOM; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!widget.type.spec.raw) { + if (dom.nodeType != 1) { + const wrap = document.createElement("span"); + wrap.appendChild(dom); + dom = wrap; + } + dom.contentEditable = "false"; + dom.classList.add("ProseMirror-widget"); + } + if (rootDomRef.current.firstElementChild === dom) return; + rootDomRef.current.replaceChildren(dom); + }); + useLayoutEffect(()=>{ + if (!rootDomRef.current) return; + if (!viewDescRef.current) { + viewDescRef.current = new WidgetViewDesc(parentRef.current, getPos.current(), widget, rootDomRef.current); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.widget = widget; + viewDescRef.current.pos = getPos.current(); + viewDescRef.current.dom = rootDomRef.current; + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + }); + return /*#__PURE__*/ React.createElement("span", { + ref: rootDomRef + }); +} diff --git a/dist/esm/components/NodeView.js b/dist/esm/components/NodeView.js new file mode 100644 index 00000000..6320bbc7 --- /dev/null +++ b/dist/esm/components/NodeView.js @@ -0,0 +1,172 @@ +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import React, { cloneElement, createElement, memo, useContext, useLayoutEffect, useMemo, useRef } from "react"; +import { createPortal } from "react-dom"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { EditorContext } from "../contexts/EditorContext.js"; +import { NodeViewContext } from "../contexts/NodeViewContext.js"; +import { StopEventContext } from "../contexts/StopEventContext.js"; +import { useNodeViewDescriptor } from "../hooks/useNodeViewDescriptor.js"; +import { ChildNodeViews, wrapInDeco } from "./ChildNodeViews.js"; +import { MarkView } from "./MarkView.js"; +import { OutputSpec } from "./OutputSpec.js"; +export const NodeView = /*#__PURE__*/ memo(function NodeView(param) { + let { outerDeco , getPos , node , innerDeco , ...props } = param; + const domRef = useRef(null); + const nodeDomRef = useRef(null); + const contentDomRef = useRef(null); + const getPosFunc = useRef(()=>getPos.current()).current; + // this is ill-conceived; should revisit + const initialNode = useRef(node); + const initialOuterDeco = useRef(outerDeco); + const initialInnerDeco = useRef(innerDeco); + const customNodeViewRootRef = useRef(null); + const customNodeViewRef = useRef(null); + // const state = useEditorState(); + const { nodeViews } = useContext(NodeViewContext); + const { view } = useContext(EditorContext); + let element = null; + const Component = nodeViews[node.type.name]; + const outputSpec = useMemo(()=>node.type.spec.toDOM?.(node), [ + node + ]); + // TODO: Would be great to pull all of the custom node view stuff into + // a hook + const customNodeView = view?.someProp("nodeViews", (nodeViews)=>nodeViews?.[node.type.name]); + useLayoutEffect(()=>{ + if (!customNodeViewRef.current || !customNodeViewRootRef.current) return; + const { dom } = customNodeViewRef.current; + nodeDomRef.current = customNodeViewRootRef.current; + customNodeViewRootRef.current.appendChild(dom); + return ()=>{ + customNodeViewRef.current?.destroy?.(); + }; + }, []); + useLayoutEffect(()=>{ + if (!customNodeView || !customNodeViewRef.current) return; + const { destroy , update } = customNodeViewRef.current; + const updated = update?.call(customNodeViewRef.current, node, outerDeco, innerDeco) ?? true; + if (updated) return; + destroy?.call(customNodeViewRef.current); + if (!customNodeViewRootRef.current) return; + initialNode.current = node; + initialOuterDeco.current = outerDeco; + initialInnerDeco.current = innerDeco; + customNodeViewRef.current = customNodeView(initialNode.current, // customNodeView will only be set if view is set, and we can only reach + // this line if customNodeView is set + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + view, ()=>getPos.current(), initialOuterDeco.current, initialInnerDeco.current); + const { dom } = customNodeViewRef.current; + nodeDomRef.current = customNodeViewRootRef.current; + customNodeViewRootRef.current.appendChild(dom); + }, [ + customNodeView, + view, + innerDeco, + node, + outerDeco, + getPos + ]); + const { hasContentDOM , childDescriptors , setStopEvent , nodeViewDescRef } = useNodeViewDescriptor(node, ()=>getPos.current(), domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef); + const finalProps = { + ...props, + ...!hasContentDOM && { + contentEditable: false + } + }; + const nodeProps = useMemo(()=>({ + node: node, + getPos: getPosFunc, + decorations: outerDeco, + innerDecorations: innerDeco, + isSelected: false + }), [ + getPosFunc, + innerDeco, + node, + outerDeco + ]); + if (Component) { + element = /*#__PURE__*/ React.createElement(Component, _extends({}, finalProps, { + ref: nodeDomRef, + nodeProps: nodeProps + }), /*#__PURE__*/ React.createElement(ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + })); + } else if (customNodeView) { + if (!customNodeViewRef.current) { + customNodeViewRef.current = customNodeView(initialNode.current, // customNodeView will only be set if view is set, and we can only reach + // this line if customNodeView is set + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + view, ()=>getPos.current(), initialOuterDeco.current, initialInnerDeco.current); + } + const { contentDOM } = customNodeViewRef.current; + contentDomRef.current = contentDOM ?? null; + element = /*#__PURE__*/ createElement(node.isInline ? "span" : "div", { + ref: customNodeViewRootRef, + contentEditable: !!contentDOM, + suppressContentEditableWarning: true + }, contentDOM && /*#__PURE__*/ createPortal(/*#__PURE__*/ React.createElement(ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + }), contentDOM)); + } else { + if (outputSpec) { + element = /*#__PURE__*/ React.createElement(OutputSpec, _extends({}, finalProps, { + ref: nodeDomRef, + outputSpec: outputSpec + }), /*#__PURE__*/ React.createElement(ChildNodeViews, { + getPos: getPos, + node: node, + innerDecorations: innerDeco + })); + } + } + if (!element) { + throw new Error(`Node spec for ${node.type.name} is missing toDOM`); + } + const decoratedElement = /*#__PURE__*/ cloneElement(outerDeco.reduce(wrapInDeco, element), // eslint-disable-next-line @typescript-eslint/no-explicit-any + outerDeco.some((d)=>d.type.attrs.nodeName) ? { + ref: domRef + } : // we've already passed the domRef to the NodeView component + // as a prop + undefined); + // TODO: Should we only be wrapping non-inline elements? Inline elements have + // already been wrapped in ChildNodeViews/InlineView? + const markedElement = node.marks.reduce((element, mark)=>/*#__PURE__*/ React.createElement(MarkView, { + getPos: getPos, + mark: mark + }, element), decoratedElement); + const childContextValue = useMemo(()=>({ + parentRef: nodeViewDescRef, + siblingsRef: childDescriptors + }), [ + childDescriptors, + nodeViewDescRef + ]); + return /*#__PURE__*/ React.createElement(StopEventContext.Provider, { + value: setStopEvent + }, /*#__PURE__*/ React.createElement(ChildDescriptorsContext.Provider, { + value: childContextValue + }, /*#__PURE__*/ cloneElement(markedElement, node.marks.length || // eslint-disable-next-line @typescript-eslint/no-explicit-any + outerDeco.some((d)=>d.type.attrs.nodeName) ? { + ref: domRef + } : // we've already passed the domRef to the NodeView component + // as a prop + undefined))); +}); diff --git a/dist/esm/components/NodeViewComponentProps.js b/dist/esm/components/NodeViewComponentProps.js new file mode 100644 index 00000000..2234b9ca --- /dev/null +++ b/dist/esm/components/NodeViewComponentProps.js @@ -0,0 +1 @@ +export { }; diff --git a/dist/esm/components/OutputSpec.js b/dist/esm/components/OutputSpec.js new file mode 100644 index 00000000..bcf1fb38 --- /dev/null +++ b/dist/esm/components/OutputSpec.js @@ -0,0 +1,38 @@ +import React, { createElement, forwardRef, memo } from "react"; +import { htmlAttrsToReactProps, mergeReactProps } from "../props.js"; +const ForwardedOutputSpec = /*#__PURE__*/ memo(/*#__PURE__*/ forwardRef(function OutputSpec(param, ref) { + let { outputSpec , children , ...propOverrides } = param; + if (typeof outputSpec === "string") { + return /*#__PURE__*/ React.createElement(React.Fragment, null, outputSpec); + } + if (!Array.isArray(outputSpec)) { + throw new Error("@nytimes/react-prosemirror only supports strings and arrays in toDOM"); + } + const tagSpec = outputSpec[0]; + const tagName = tagSpec.replace(" ", ":"); + const attrs = outputSpec[1]; + let props = { + ref, + ...propOverrides + }; + let start = 1; + if (attrs && typeof attrs === "object" && attrs.nodeType == null && !Array.isArray(attrs)) { + start = 2; + props = mergeReactProps(htmlAttrsToReactProps(attrs), props); + } + const content = []; + for(let i = start; i < outputSpec.length; i++){ + const child = outputSpec[i]; + if (child === 0) { + if (i < outputSpec.length - 1 || i > start) { + throw new RangeError("Content hole must be the only child of its parent node"); + } + return /*#__PURE__*/ createElement(tagName, props, children); + } + content.push(/*#__PURE__*/ React.createElement(ForwardedOutputSpec, { + outputSpec: child + }, children)); + } + return /*#__PURE__*/ createElement(tagName, props, ...content); +})); +export { ForwardedOutputSpec as OutputSpec }; diff --git a/dist/esm/components/ProseMirror.js b/dist/esm/components/ProseMirror.js new file mode 100644 index 00000000..bbee8a4b --- /dev/null +++ b/dist/esm/components/ProseMirror.js @@ -0,0 +1,66 @@ +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import { DecorationSet } from "prosemirror-view"; +import React, { useMemo, useState } from "react"; +import { EditorContext } from "../contexts/EditorContext.js"; +import { EditorStateContext } from "../contexts/EditorStateContext.js"; +import { NodeViewContext } from "../contexts/NodeViewContext.js"; +import { computeDocDeco } from "../decorations/computeDocDeco.js"; +import { viewDecorations } from "../decorations/viewDecorations.js"; +import { useEditor } from "../hooks/useEditor.js"; +import { LayoutGroup } from "./LayoutGroup.js"; +import { DocNodeViewContext } from "./ProseMirrorDoc.js"; +const EMPTY_OUTER_DECOS = []; +function ProseMirrorInner(param) { + let { className , children , nodeViews ={} , customNodeViews , ...props } = param; + const [mount, setMount] = useState(null); + const { editor , state } = useEditor(mount, { + ...props, + nodeViews: customNodeViews + }); + const innerDecos = editor.view ? viewDecorations(editor.view, editor.cursorWrapper) : DecorationSet.empty; + const outerDecos = editor.view ? computeDocDeco(editor.view) : EMPTY_OUTER_DECOS; + const nodeViewContextValue = useMemo(()=>({ + nodeViews + }), [ + nodeViews + ]); + const docNodeViewContextValue = useMemo(()=>({ + className: className, + setMount: setMount, + node: editor.view?.state.doc, + innerDeco: innerDecos, + outerDeco: outerDecos, + viewDesc: editor.docViewDescRef.current + }), [ + className, + editor.docViewDescRef, + editor.view?.state.doc, + innerDecos, + outerDecos + ]); + return /*#__PURE__*/ React.createElement(EditorContext.Provider, { + value: editor + }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, { + value: state + }, /*#__PURE__*/ React.createElement(NodeViewContext.Provider, { + value: nodeViewContextValue + }, /*#__PURE__*/ React.createElement(DocNodeViewContext.Provider, { + value: docNodeViewContextValue + }, children)))); +} +export function ProseMirror(props) { + return /*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(ProseMirrorInner, _extends({}, props))); +} diff --git a/dist/esm/components/ProseMirrorDoc.js b/dist/esm/components/ProseMirrorDoc.js new file mode 100644 index 00000000..7d194d60 --- /dev/null +++ b/dist/esm/components/ProseMirrorDoc.js @@ -0,0 +1,47 @@ +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import React, { createContext, forwardRef, useContext, useImperativeHandle, useMemo, useRef } from "react"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { DocNodeView } from "./DocNodeView.js"; +export const DocNodeViewContext = /*#__PURE__*/ createContext(null); +function ProseMirrorDoc(param, ref) { + let { as , ...props } = param; + const childDescriptors = useRef([]); + const innerRef = useRef(null); + const { setMount , ...docProps } = useContext(DocNodeViewContext); + const viewDescRef = useRef(undefined); + useImperativeHandle(ref, ()=>{ + return innerRef.current; + }, []); + const childContextValue = useMemo(()=>({ + parentRef: viewDescRef, + siblingsRef: childDescriptors + }), [ + childDescriptors, + viewDescRef + ]); + return /*#__PURE__*/ React.createElement(ChildDescriptorsContext.Provider, { + value: childContextValue + }, /*#__PURE__*/ React.createElement(DocNodeView, _extends({ + ref: (el)=>{ + innerRef.current = el; + setMount(el); + } + }, props, docProps, { + as: as + }))); +} +const ForwardedProseMirrorDoc = /*#__PURE__*/ forwardRef(ProseMirrorDoc); +export { ForwardedProseMirrorDoc as ProseMirrorDoc }; diff --git a/dist/esm/components/SeparatorHackView.js b/dist/esm/components/SeparatorHackView.js new file mode 100644 index 00000000..51cf1c57 --- /dev/null +++ b/dist/esm/components/SeparatorHackView.js @@ -0,0 +1,48 @@ +import React, { useContext, useLayoutEffect, useRef, useState } from "react"; +import { browser } from "../browser.js"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { TrailingHackViewDesc } from "../viewdesc.js"; +export function SeparatorHackView(param) { + let { getPos } = param; + const { siblingsRef , parentRef } = useContext(ChildDescriptorsContext); + const viewDescRef = useRef(null); + const ref = useRef(null); + const [shouldRender, setShouldRender] = useState(false); + useLayoutEffect(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + // There's no risk of an infinite loop here, because + // we call setShouldRender conditionally + // eslint-disable-next-line react-hooks/exhaustive-deps + useLayoutEffect(()=>{ + const lastSibling = siblingsRef.current[siblingsRef.current.length - 1]; + if ((browser.safari || browser.chrome) && (lastSibling?.dom)?.contentEditable == "false") { + setShouldRender(true); + return; + } + if (!ref.current) return; + if (!viewDescRef.current) { + viewDescRef.current = new TrailingHackViewDesc(parentRef.current, [], getPos.current(), ref.current, null); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.dom = ref.current; + viewDescRef.current.pos = getPos.current(); + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + }); + return shouldRender ? /*#__PURE__*/ React.createElement("img", { + ref: ref, + className: "ProseMirror-separator" + }) : null; +} diff --git a/dist/esm/components/TextNodeView.js b/dist/esm/components/TextNodeView.js new file mode 100644 index 00000000..a0bbb4d9 --- /dev/null +++ b/dist/esm/components/TextNodeView.js @@ -0,0 +1,105 @@ +import { DecorationSet } from "prosemirror-view"; +import { Component } from "react"; +import { findDOMNode } from "react-dom"; +import { CompositionViewDesc, TextViewDesc } from "../viewdesc.js"; +import { wrapInDeco } from "./ChildNodeViews.js"; +function shallowEqual(objA, objB) { + if (objA === objB) { + return true; + } + if (!objA || !objB) { + return false; + } + const aKeys = Object.keys(objA); + const bKeys = Object.keys(objB); + const len = aKeys.length; + if (bKeys.length !== len) { + return false; + } + for(let i = 0; i < len; i++){ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const key = aKeys[i]; + if (objA[key] !== objB[key] || !Object.prototype.hasOwnProperty.call(objB, key)) { + return false; + } + } + return true; +} +export class TextNodeView extends Component { + updateEffect() { + const { view , decorations , siblingsRef , parentRef , getPos , node } = this.props; + // There simply is no other way to ref a text node + // eslint-disable-next-line react/no-find-dom-node + const dom = findDOMNode(this); + // We only need to explicitly create a CompositionViewDesc + // when a composition was started that produces a new text node. + // Otherwise we just rely on re-rendering the renderRef + if (!dom) { + if (!view?.composing) return; + this.viewDescRef = new CompositionViewDesc(parentRef.current, getPos.current(), // These are just placeholders/dummies. We can't + // actually find the correct DOM nodes from here, + // so we let our parent do it. + // Passing a valid element here just so that the + // ViewDesc constructor doesn't blow up. + document.createElement("div"), document.createTextNode(node.text ?? ""), node.text ?? ""); + return; + } + let textNode = dom; + while(textNode.firstChild){ + textNode = textNode.firstChild; + } + if (!this.viewDescRef || this.viewDescRef instanceof CompositionViewDesc) { + this.viewDescRef = new TextViewDesc(undefined, [], getPos.current(), node, decorations, DecorationSet.empty, dom, textNode); + } else { + this.viewDescRef.parent = parentRef.current; + this.viewDescRef.children = []; + this.viewDescRef.node = node; + this.viewDescRef.pos = getPos.current(); + this.viewDescRef.outerDeco = decorations; + this.viewDescRef.innerDeco = DecorationSet.empty; + this.viewDescRef.dom = dom; + // @ts-expect-error We have our own ViewDesc implementations + this.viewDescRef.dom.pmViewDesc = this.viewDescRef; + this.viewDescRef.nodeDOM = textNode; + } + if (!siblingsRef.current.includes(this.viewDescRef)) { + siblingsRef.current.push(this.viewDescRef); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + } + shouldComponentUpdate(nextProps) { + return !shallowEqual(this.props, nextProps); + } + componentDidMount() { + this.updateEffect(); + } + componentDidUpdate() { + this.updateEffect(); + } + componentWillUnmount() { + const { siblingsRef } = this.props; + if (!this.viewDescRef) return; + if (siblingsRef.current.includes(this.viewDescRef)) { + const index = siblingsRef.current.indexOf(this.viewDescRef); + siblingsRef.current.splice(index, 1); + } + } + render() { + const { view , getPos , node , decorations } = this.props; + // During a composition, it's crucial that we don't try to + // update the DOM that the user is working in. If there's + // an active composition and the selection is in this node, + // we freeze the DOM of this element so that it doesn't + // interrupt the composition + if (view?.composing && view.state.selection.from >= getPos.current() && view.state.selection.from <= getPos.current() + node.nodeSize) { + return this.renderRef; + } + this.renderRef = decorations.reduce(wrapInDeco, node.text); + return this.renderRef; + } + constructor(...args){ + super(...args); + this.viewDescRef = null; + this.renderRef = null; + } +} diff --git a/dist/esm/components/TrailingHackView.js b/dist/esm/components/TrailingHackView.js new file mode 100644 index 00000000..d4a046f4 --- /dev/null +++ b/dist/esm/components/TrailingHackView.js @@ -0,0 +1,39 @@ +import React, { useContext, useLayoutEffect, useRef } from "react"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { TrailingHackViewDesc } from "../viewdesc.js"; +export function TrailingHackView(param) { + let { getPos } = param; + const { siblingsRef , parentRef } = useContext(ChildDescriptorsContext); + const viewDescRef = useRef(null); + const ref = useRef(null); + useLayoutEffect(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + useLayoutEffect(()=>{ + if (!ref.current) return; + if (!viewDescRef.current) { + viewDescRef.current = new TrailingHackViewDesc(parentRef.current, [], getPos.current(), ref.current, null); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.dom = ref.current; + viewDescRef.current.pos = getPos.current(); + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + }); + return /*#__PURE__*/ React.createElement("br", { + ref: ref, + className: "ProseMirror-trailingBreak" + }); +} diff --git a/dist/esm/components/WidgetView.js b/dist/esm/components/WidgetView.js new file mode 100644 index 00000000..54f70115 --- /dev/null +++ b/dist/esm/components/WidgetView.js @@ -0,0 +1,44 @@ +import React, { useContext, useLayoutEffect, useRef } from "react"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { WidgetViewDesc } from "../viewdesc.js"; +export function WidgetView(param) { + let { widget , getPos } = param; + const { siblingsRef , parentRef } = useContext(ChildDescriptorsContext); + const viewDescRef = useRef(null); + const getPosFunc = useRef(()=>getPos.current()).current; + const domRef = useRef(null); + useLayoutEffect(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!viewDescRef.current) return; + if (siblings.includes(viewDescRef.current)) { + const index = siblings.indexOf(viewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + useLayoutEffect(()=>{ + if (!domRef.current) return; + if (!viewDescRef.current) { + viewDescRef.current = new WidgetViewDesc(parentRef.current, getPos.current(), widget, domRef.current); + } else { + viewDescRef.current.parent = parentRef.current; + viewDescRef.current.widget = widget; + viewDescRef.current.pos = getPos.current(); + viewDescRef.current.dom = domRef.current; + } + if (!siblingsRef.current.includes(viewDescRef.current)) { + siblingsRef.current.push(viewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + }); + const { Component } = widget.type; + return Component && /*#__PURE__*/ React.createElement(Component, { + ref: domRef, + widget: widget, + getPos: getPosFunc, + contentEditable: false + }); +} diff --git a/dist/esm/components/WidgetViewComponentProps.js b/dist/esm/components/WidgetViewComponentProps.js new file mode 100644 index 00000000..2234b9ca --- /dev/null +++ b/dist/esm/components/WidgetViewComponentProps.js @@ -0,0 +1 @@ +export { }; diff --git a/dist/esm/components/__tests__/ProseMirror.composition.test.js b/dist/esm/components/__tests__/ProseMirror.composition.test.js new file mode 100644 index 00000000..219fcde4 --- /dev/null +++ b/dist/esm/components/__tests__/ProseMirror.composition.test.js @@ -0,0 +1,395 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { doc, em, p, strong } from "prosemirror-test-builder"; +// @ts-expect-error This is an internal export +import { __endComposition } from "prosemirror-view"; +import { findTextNode, tempEditor } from "../../testing/editorViewTestHelpers.js"; +function endComposition(view, forceUpdate) { + __endComposition(view, forceUpdate); +} +function event(pm, type) { + pm.dom.dispatchEvent(new CompositionEvent(type)); +} +function edit(node) { + let text = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : "", from = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : node.nodeValue.length, to = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : from; + const 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) { + let options = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : {}; + event(pm, "compositionstart"); + expect(pm.composing).toBeTruthy(); + let node; + const sel = document.getSelection(); + for(let i = -1; i < update.length; i++){ + if (i < 0) node = start(); + else update[i](node); + const { focusNode , focusOffset } = sel; + // @ts-expect-error Internal property + pm.domObserver.flush(); + if (options.cancel && i == update.length - 1) { + expect(hasCompositionNode(pm)).toBeFalsy(); + } else { + expect(node.parentNode && pm.dom.contains(node.parentNode)).toBeTruthy(); + expect(sel.focusNode === focusNode).toBeTruthy(); + expect(sel.focusOffset === focusOffset).toBeTruthy(); + if (options.node) expect(hasCompositionNode(pm)).toBeTruthy(); + } + } + event(pm, "compositionend"); + if (options.end) { + options.end(node); + // @ts-expect-error Internal property + pm.domObserver.flush(); + } + endComposition(pm); + expect(pm.composing).toBeFalsy(); + expect(hasCompositionNode(pm)).toBeFalsy(); +} +// function wordDeco(state: EditorState) { +// const re = /\w+/g, +// deco: Decoration[] = []; +// 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 }, +// }); +// const Widget = forwardRef(function Widget( +// { widget, pos, ...props }: WidgetViewComponentProps, +// ref: Ref +// ) { +// return ( +// +// × +// +// ); +// }); +// function widgets(positions: number[], sides: number[]) { +// return new Plugin({ +// state: { +// init(state) { +// const deco = positions.map((p, i) => +// widget(p, Widget, { side: sides[i] }) +// ); +// return DecorationSet.create(state.doc!, deco); +// }, +// apply(tr, deco) { +// return deco.map(tr.mapping, tr.doc); +// }, +// }, +// props: { +// decorations(this: Plugin, state) { +// return this.getState(state); +// }, +// }, +// }); +// } +// These unfortunately aren't working at the moment, though +// composition seems to be working generally. +describe.skip("EditorView composition", ()=>{ + it("supports composition in an empty block", ()=>{ + const { view: pm } = tempEditor({ + doc: doc(p("")) + }); + compose(pm, ()=>edit(pm.dom.firstChild.appendChild(document.createTextNode("a"))), [ + (n)=>edit(n, "b"), + (n)=>edit(n, "c") + ], { + node: true + }); + expect(pm.state.doc).toEqualNode(doc(p("abc"))); + }); + it("supports composition at end of block", ()=>{ + const { view: pm } = tempEditor({ + doc: doc(p("foo")) + }); + compose(pm, ()=>edit(findTextNode(pm.dom, "foo")), [ + (n)=>edit(n, "!"), + (n)=>edit(n, "?") + ]); + expect(pm.state.doc).toEqualNode(doc(p("foo!?"))); + }); + it("supports composition at end of block in a new node", ()=>{ + const { view: pm } = tempEditor({ + doc: doc(p("foo")) + }); + compose(pm, ()=>edit(pm.dom.firstChild.appendChild(document.createTextNode("!"))), [ + (n)=>edit(n, "?") + ], // $$FORK: We don't use composition view descriptors except for in initially empty nodes + { + node: false + }); + expect(pm.state.doc).toEqualNode(doc(p("foo!?"))); + }); + it("supports composition at start of block in a new node", ()=>{ + const { view: pm } = tempEditor({ + doc: doc(p("foo")) + }); + compose(pm, ()=>{ + const p = pm.dom.firstChild; + return edit(p.insertBefore(document.createTextNode("!"), p.firstChild)); + }, [ + (n)=>edit(n, "?") + ], // $$FORK: We don't use composition view descriptors except for in initially empty nodes + { + node: false + }); + expect(pm.state.doc).toEqualNode(doc(p("!?foo"))); + }); + it("supports composition inside existing text", ()=>{ + const { view: pm } = 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) + ]); + expect(pm.state.doc).toEqualNode(doc(p("fxyzoo"))); + }); + // TODO: Offset out of bound + // eslint-disable-next-line jest/no-disabled-tests + it.skip("can deal with Android-style newline-after-composition", ()=>{ + const { view: pm } = tempEditor({ + doc: doc(p("abcdef")) + }); + compose(pm, ()=>edit(findTextNode(pm.dom, "abcdef")), [ + (n)=>edit(n, "x", 3), + (n)=>edit(n, "y", 4) + ], { + end: (n)=>{ + const line = pm.dom.appendChild(document.createElement("div")); + line.textContent = "def"; + n.nodeValue = "abcxy"; + document.getSelection().collapse(line, 0); + } + }); + expect(pm.state.doc).toEqualNode(doc(p("abcxy"), p("def"))); + }); + it("handles replacement of existing words", ()=>{ + const { view: pm } = 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) + ]); + expect(pm.state.doc).toEqualNode(doc(p("one zero three"))); + }); + it("handles composition inside marks", ()=>{ + const { view: pm } = tempEditor({ + doc: doc(p("one ", em("two"))) + }); + compose(pm, ()=>edit(findTextNode(pm.dom, "two"), "o"), [ + (n)=>edit(n, "o"), + (n)=>edit(n, "w") + ]); + expect(pm.state.doc).toEqualNode(doc(p("one ", em("twooow")))); + }); + it.skip("handles composition in a mark that has multiple children", ()=>{ + const { view: pm } = 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") + ]); + expect(pm.state.doc).toEqualNode(doc(p("one ", em("twooow", strong(" three"))))); + }); +// it("supports composition in a cursor wrapper", () => { +// const { view: pm } = 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", () => { +// const { view: pm } = requireFocus( +// tempEditor({ doc: doc(p("one ", em("two", strong(" three")))) }) +// ); +// pm.dispatch(pm.state.tr.addStoredMark(schema.marks.code.create())); +// const 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", () => { +// const { view: 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", () => { +// const { view: 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", () => { +// const { view: 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: Text) => { +// 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", () => { +// const { view: pm } = requireFocus( +// tempEditor({ doc: doc(p("")), plugins: [widgets([1, 1], [-1, 1])] }) +// ); +// compose( +// pm, +// () => { +// const 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", () => { +// const { view: 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", () => { +// const { view: 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", () => { +// const { view: 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", () => { +// const { view: 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", () => { +// const { view: pm } = tempEditor({ doc: doc(p("one"), p("two")) }); +// event(pm, "compositionstart"); +// const one = findTextNode(pm.dom, "one")!; +// edit(one, "!"); +// pm.domObserver.flush(); +// event(pm, "compositionend"); +// one.nodeValue = "one!!"; +// const L2 = pm.dom.lastChild; +// event(pm, "compositionstart"); +// const two = findTextNode(pm.dom, "two")!; +// ist(pm.dom.lastChild, L2); +// edit(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 = false) { +// const { view: 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!); +// const target = pm.dom.firstChild!.firstChild as Text; +// 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)); +}); diff --git a/dist/esm/components/__tests__/ProseMirror.domchange.test.js b/dist/esm/components/__tests__/ProseMirror.domchange.test.js new file mode 100644 index 00000000..ccecfdac --- /dev/null +++ b/dist/esm/components/__tests__/ProseMirror.domchange.test.js @@ -0,0 +1,266 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { screen } from "@testing-library/react"; +import { EditorState, TextSelection } from "prosemirror-state"; +import { a, blockquote, doc, em, p, pre, strong } from "prosemirror-test-builder"; +import { Key } from "webdriverio"; +import { tempEditor } from "../../testing/editorViewTestHelpers.js"; +describe("DOM change", ()=>{ + it("notices when text is added", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("hello")) + }); + view.focus(); + await browser.keys([ + Key.Shift, + "l" + ]); + expect(view.state.doc).toEqualNode(doc(p("heLllo"))); + }); + it("notices when text is removed", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("hello")) + }); + view.focus(); + await browser.keys(Key.Backspace); + await browser.keys(Key.Backspace); + expect(view.state.doc).toEqualNode(doc(p("heo"))); + }); + it("respects stored marks", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("hello")) + }); + view.dispatch(view.state.tr.addStoredMark(view.state.schema.marks.em.create())); + view.focus(); + await browser.keys("o"); + expect(view.state.doc).toEqualNode(doc(p("hello", em("o")))); + }); + it("support inserting repeated text", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("hello")) + }); + view.focus(); + await browser.keys("h"); + await browser.keys("e"); + await browser.keys("l"); + expect(view.state.doc).toEqualNode(doc(p("helhello"))); + }); + it("detects an enter press", async ()=>{ + let enterPressed = false; + const { view } = tempEditor({ + doc: doc(blockquote(p("foo"), p(""))), + handleKeyDown: (_, event)=>{ + if (event.key === "Enter") return enterPressed = true; + return false; + } + }); + view.focus(); + await browser.keys(Key.Enter); + expect(enterPressed).toBeTruthy(); + }); + it("detects a simple backspace press", async ()=>{ + let backspacePressed = false; + const { view } = tempEditor({ + doc: doc(blockquote(p("foo"), p(""))), + handleKeyDown: (_, event)=>{ + if (event.key === "Backspace") return backspacePressed = true; + return false; + } + }); + view.focus(); + await browser.keys(Key.Backspace); + expect(backspacePressed).toBeTruthy(); + }); + it("correctly adjusts the selection", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("abc")) + }); + view.focus(); + await browser.keys("d"); + expect(view.state.doc).toEqualNode(doc(p("abcd"))); + expect(view.state.selection.anchor).toBe(5); + expect(view.state.selection.head).toBe(5); + }); + // todoit("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); + // }); + // $$FORK: We _do_ repaint text nodes when they're typed into. + // Unlike prosemirror-view, we prevent user inputs from modifying + // the dom until after we've turned them into transactions. + // This test instead ensures that we only modify the character data, + // rather than replacing entire nodes. + it("does not replace a text node when it's typed into", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")) + }); + let mutated = false; + const observer = new MutationObserver(()=>mutated = true); + observer.observe(view.dom, { + subtree: true, + characterData: false, + childList: true + }); + view.focus(); + await browser.keys("j"); + expect(view.state.doc).toEqualNode(doc(p("fojo"))); + expect(mutated).toBeFalsy(); + observer.disconnect(); + }); + it("understands text typed into an empty paragraph", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("")) + }); + view.focus(); + await browser.keys("i"); + expect(view.state.doc).toEqualNode(doc(p("i"))); + }); + it("fixes text changes when input is ignored", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")), + controlled: true, + dispatchTransaction () { + // intentionally do nothing + } + }); + view.focus(); + await browser.keys("i"); + expect(view.dom.textContent).toBe("foo"); + }); + it("aborts when an incompatible state is set", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("abcde")) + }); + view.dispatchEvent({ + type: "input" + }); + view.focus(); + await browser.keys("x"); + view.updateState(EditorState.create({ + doc: doc(p("uvw")) + })); + expect(view.state.doc).toEqualNode(doc(p("uvw"))); + }); + it("preserves marks on deletion", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one", em("x"))) + }); + view.focus(); + await browser.keys(Key.Backspace); + view.dispatchEvent({ + type: "input" + }); + view.dispatch(view.state.tr.insertText("y")); + expect(view.state.doc).toEqualNode(doc(p("one", em("y")))); + }); + it("works when a node's contentDOM is deleted", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one"), pre("two")) + }); + view.focus(); + await browser.keys(Key.Backspace); + await browser.keys(Key.Backspace); + await browser.keys(Key.Backspace); + view.dispatchEvent({ + type: "input" + }); + expect(view.state.doc).toEqualNode(doc(p("one"), pre())); + expect(view.state.selection.head).toBe(6); + }); + it("doesn't redraw content with marks when typing in front", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo", em("bar"), strong("baz"))) + }); + const bar = await screen.findByText("bar"); + const foo = await screen.findByText("foo"); + view.focus(); + await browser.keys("r"); + expect(view.state.doc).toEqualNode(doc(p("froo", em("bar"), strong("baz")))); + expect(bar.parentNode).toBeTruthy(); + expect(view.dom.contains(bar.parentNode)).toBeTruthy(); + expect(foo.parentNode).toBeTruthy(); + expect(view.dom.contains(foo.parentNode)).toBeTruthy(); + }); + it("doesn't redraw content with marks when typing inside mark", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo", em("bar"), strong("baz"))) + }); + const bar = await screen.findByText("bar"); + const foo = await screen.findByText("foo"); + view.focus(); + await browser.keys("a"); + expect(view.state.doc).toEqualNode(doc(p("foo", em("baar"), strong("baz")))); + expect(bar.parentNode).toBeTruthy(); + expect(view.dom.contains(bar.parentNode)).toBeTruthy(); + expect(foo.parentNode).toBeTruthy(); + expect(view.dom.contains(foo.parentNode)).toBeTruthy(); + }); + it("maps input to coordsAtPos through pending changes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")) + }); + view.dispatchEvent({ + type: "input" + }); + view.dispatch(view.state.tr.insertText("more text")); + expect(view.coordsAtPos(13)).toBeTruthy(); + }); + it("notices text added to a cursor wrapper at the start of a mark", async ()=>{ + const { view } = tempEditor({ + doc: doc(p(strong(a("foo"), "bar"))) + }); + view.focus(); + await browser.keys("xy"); + expect(view.state.doc).toEqualNode(doc(p(strong(a("foo"), "xybar")))); + }); + it("removes cursor wrapper text when the wrapper otherwise remains valid", async ()=>{ + const { view } = tempEditor({ + doc: doc(p(a(strong("foo"), "bar"))) + }); + view.focus(); + await browser.keys("q"); + expect(view.state.doc).toEqualNode(doc(p(a(strong("fooq"), "bar")))); + const found = screen.queryByText("\uFEFFq"); + expect(found).toBeNull(); + }); + it("creates a correct step for an ambiguous selection-deletion", async ()=>{ + let steps; + const { view } = tempEditor({ + doc: doc(p("lalala")), + dispatchTransaction (tr) { + steps = tr.steps; + view.updateState(view.state.apply(tr)); + } + }); + view.input.lastKeyCode = 8; + view.input.lastKeyCodeTime = Date.now(); + view.focus(); + await browser.keys(Key.Backspace); + expect(steps).toHaveLength(1); + expect(steps[0].from).toBe(3); + expect(steps[0].to).toBe(5); + }); + it("creates a step that covers the entire selection for partially-matching replacement", async ()=>{ + let steps; + const { view } = tempEditor({ + doc: doc(p("one two three")), + dispatchTransaction (tr) { + steps = tr.steps; + view.updateState(view.state.apply(tr)); + } + }); + view.focus(); + await browser.keys("t"); + expect(steps).toHaveLength(1); + expect(steps[0].from).toBe(5); + expect(steps[0].to).toBe(8); + expect(steps[0].slice.content.toString()).toBe('<"t">'); + view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, 7, 12))); + view.focus(); + await browser.keys("e"); + expect(steps).toHaveLength(1); + expect(steps[0].from).toBe(7); + expect(steps[0].to).toBe(12); + expect(steps[0].slice.content.toString()).toBe('<"e">'); + }); +}); diff --git a/dist/esm/components/__tests__/ProseMirror.draw-decoration.test.js b/dist/esm/components/__tests__/ProseMirror.draw-decoration.test.js new file mode 100644 index 00000000..9662e01f --- /dev/null +++ b/dist/esm/components/__tests__/ProseMirror.draw-decoration.test.js @@ -0,0 +1,958 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ // TODO: figure out whether it's possible to support native +// widgets. Right now, I'm not sure how we'd do it without +// wrapping them in another element, which would re-introduce +// all of the issues we had before with node views. +// +// For now, we've updated the factory in this file to use +// our React widgets. +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import { Schema } from "prosemirror-model"; +import { Plugin, TextSelection } from "prosemirror-state"; +import { blockquote, doc, em, h1, hr, img, p, schema, strong } from "prosemirror-test-builder"; +import { Decoration, DecorationSet } from "prosemirror-view"; +import React, { forwardRef, useEffect } from "react"; +import { widget } from "../../decorations/ReactWidgetType.js"; +import { useEditorEffect } from "../../hooks/useEditorEffect.js"; +import { tempEditor } from "../../testing/editorViewTestHelpers.js"; +const Widget = /*#__PURE__*/ forwardRef(function Widget(param, ref) { + let { widget , getPos , ...props } = param; + return /*#__PURE__*/ React.createElement("button", _extends({ + ref: ref + }, props), "ω"); +}); +function make(str) { + if (typeof str != "string") return str; + const match = /^(\d+)(?:-(\d+))?-(.+)$/.exec(str); + if (match[3] == "widget") { + return widget(+match[1], Widget, { + key: str + }); + } + return Decoration.inline(+match[1], +match[2], { + class: match[3] + }); +} +function decoPlugin(decos) { + return new Plugin({ + state: { + init (config) { + return config.doc ? DecorationSet.create(config.doc, decos.map(make)) : DecorationSet.empty; + }, + apply (tr, set, state) { + if (tr.docChanged) set = set.map(tr.mapping, tr.doc); + const 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", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foobar")), + plugins: [ + decoPlugin([ + "2-5-foo" + ]) + ] + }); + const found = view.dom.querySelector(".foo"); + expect(found).not.toBeNull(); + expect(found.textContent).toBe("oob"); + }); + it("draws wrapping decorations", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")), + plugins: [ + decoPlugin([ + Decoration.inline(1, 5, { + nodeName: "i" + }) + ]) + ] + }); + const found = view.dom.querySelector("i"); + expect(found && found.innerHTML).toBe("foo"); + }); + it("draws node decorations", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), p("bar")), + plugins: [ + decoPlugin([ + Decoration.node(5, 10, { + class: "cls" + }) + ]) + ] + }); + const found = view.dom.querySelectorAll(".cls"); + expect(found).toHaveLength(1); + expect(found[0].nodeName).toBe("P"); + expect(found[0].previousSibling.nodeName).toBe("P"); + }); + it("can update multi-level wrapping decorations", async ()=>{ + const d2 = Decoration.inline(1, 5, { + nodeName: "i", + class: "b" + }); + const { view } = tempEditor({ + doc: doc(p("hello")), + plugins: [ + decoPlugin([ + Decoration.inline(1, 5, { + nodeName: "i", + class: "a" + }), + d2 + ]) + ] + }); + expect(view.dom.querySelectorAll("i")).toHaveLength(2); + updateDeco(view, [ + Decoration.inline(1, 5, { + nodeName: "i", + class: "c" + }) + ], [ + d2 + ]); + const iNodes = view.dom.querySelectorAll("i"); + expect(iNodes).toHaveLength(2); + expect(Array.prototype.map.call(iNodes, (n)=>n.className).sort().join()).toBe("a,c"); + }); + it("draws overlapping inline decorations", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("abcdef")), + plugins: [ + decoPlugin([ + "3-5-foo", + "4-6-bar", + "1-7-baz" + ]) + ] + }); + const baz = view.dom.querySelectorAll(".baz"); + expect(baz).toHaveLength(5); + expect(Array.prototype.map.call(baz, (x)=>x.textContent).join("-")).toBe("ab-c-d-e-f"); + function classes(n) { + return n.className.split(" ").sort().join(" "); + } + expect(classes(baz[1])).toBe("baz foo"); + expect(classes(baz[2])).toBe("bar baz foo"); + expect(classes(baz[3])).toBe("bar baz"); + }); + it("draws multiple widgets", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foobar")), + plugins: [ + decoPlugin([ + "1-widget", + "4-widget", + "7-widget" + ]) + ] + }); + const found = view.dom.querySelectorAll("button"); + expect(found).toHaveLength(3); + expect(found[0].nextSibling.textContent).toBe("foo"); + expect(found[1].nextSibling.textContent).toBe("bar"); + expect(found[2].previousSibling.textContent).toBe("bar"); + }); + it("orders widgets by their side option", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foobar")), + plugins: [ + decoPlugin([ + widget(4, /*#__PURE__*/ forwardRef(function B(props, ref) { + return /*#__PURE__*/ React.createElement("span", _extends({ + ref: ref + }, props), "B"); + }), { + key: "widget-b" + }), + widget(4, /*#__PURE__*/ forwardRef(function A(props, ref) { + return /*#__PURE__*/ React.createElement("span", _extends({ + ref: ref + }, props), "A"); + }), { + side: -100, + key: "widget-a" + }), + widget(4, /*#__PURE__*/ forwardRef(function C(props, ref) { + return /*#__PURE__*/ React.createElement("span", _extends({ + ref: ref + }, props), "C"); + }), { + side: 2, + key: "widget-c" + }) + ]) + ] + }); + expect(view.dom.textContent).toBe("fooABCbar"); + }); + it("draws a widget in an empty node", async ()=>{ + const { view } = tempEditor({ + doc: doc(p()), + plugins: [ + decoPlugin([ + "1-widget" + ]) + ] + }); + expect(view.dom.querySelectorAll("button")).toHaveLength(1); + }); + it("draws widgets on node boundaries", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo", em("bar"))), + plugins: [ + decoPlugin([ + "4-widget" + ]) + ] + }); + expect(view.dom.querySelectorAll("button")).toHaveLength(1); + }); + it("draws decorations from multiple plugins", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo", em("bar"))), + plugins: [ + decoPlugin([ + "2-widget" + ]), + decoPlugin([ + "6-widget" + ]) + ] + }); + expect(view.dom.querySelectorAll("button")).toHaveLength(2); + }); + it("calls widget destroy methods", async ()=>{ + let destroyed = false; + const DestroyableWidget = /*#__PURE__*/ forwardRef(function DestroyableWidget(props, ref) { + useEffect(()=>{ + destroyed = true; + }); + return /*#__PURE__*/ React.createElement("button", _extends({ + ref: ref + }, props)); + }); + const { view } = tempEditor({ + doc: doc(p("abc")), + plugins: [ + decoPlugin([ + widget(2, DestroyableWidget, { + key: "destroyable-widget" + }) + ]) + ] + }); + view.dispatch(view.state.tr.delete(1, 4)); + expect(destroyed).toBeTruthy(); + }); + it("draws inline decorations spanning multiple parents", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("long first ", em("p"), "aragraph"), p("two")), + plugins: [ + decoPlugin([ + "7-25-foo" + ]) + ] + }); + const foos = view.dom.querySelectorAll(".foo"); + expect(foos).toHaveLength(4); + expect(foos[0].textContent).toBe("irst "); + expect(foos[1].textContent).toBe("p"); + expect(foos[2].textContent).toBe("aragraph"); + expect(foos[3].textContent).toBe("tw"); + }); + it("draws inline decorations across empty paragraphs", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("first"), p(), p("second")), + plugins: [ + decoPlugin([ + "3-12-foo" + ]) + ] + }); + const foos = view.dom.querySelectorAll(".foo"); + expect(foos).toHaveLength(2); + expect(foos[0].textContent).toBe("rst"); + expect(foos[1].textContent).toBe("se"); + }); + it("can handle inline decorations ending at the start or end of a node", async ()=>{ + const { view } = tempEditor({ + doc: doc(p(), p()), + plugins: [ + decoPlugin([ + "1-3-foo" + ]) + ] + }); + expect(view.dom.querySelector(".foo")).toBeNull(); + }); + it("can draw decorations with multiple classes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")), + plugins: [ + decoPlugin([ + "1-4-foo bar" + ]) + ] + }); + expect(view.dom.querySelectorAll(".foo")).toHaveLength(1); + expect(view.dom.querySelectorAll(".bar")).toHaveLength(1); + }); + it("supports overlapping inline decorations", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foobar")), + plugins: [ + decoPlugin([ + "1-3-foo", + "2-5-bar" + ]) + ] + }); + const foos = view.dom.querySelectorAll(".foo"); + const bars = view.dom.querySelectorAll(".bar"); + expect(foos).toHaveLength(2); + expect(bars).toHaveLength(2); + expect(foos[0].textContent).toBe("f"); + expect(foos[1].textContent).toBe("o"); + expect(bars[0].textContent).toBe("o"); + expect(bars[1].textContent).toBe("ob"); + }); + it("doesn't redraw when irrelevant decorations change", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), p("baz")), + plugins: [ + decoPlugin([ + "7-8-foo" + ]) + ] + }); + const para2 = view.dom.lastChild; + updateDeco(view, [ + make("2-3-bar") + ]); + expect(view.dom.lastChild).toBe(para2); + expect(view.dom.querySelector(".bar")).not.toBeNull(); + }); + it("doesn't redraw when irrelevant content changes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), p("baz")), + plugins: [ + decoPlugin([ + "7-8-foo" + ]) + ] + }); + const para2 = view.dom.lastChild; + view.dispatch(view.state.tr.delete(2, 3)); + view.dispatch(view.state.tr.delete(2, 3)); + expect(view.dom.lastChild).toBe(para2); + }); + it("can add a widget on a node boundary", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo", em("bar"))), + plugins: [ + decoPlugin([]) + ] + }); + updateDeco(view, [ + make("4-widget") + ]); + expect(view.dom.querySelectorAll("button")).toHaveLength(1); + }); + it("can remove a widget on a node boundary", async ()=>{ + const dec = make("4-widget"); + const { view } = tempEditor({ + doc: doc(p("foo", em("bar"))), + plugins: [ + decoPlugin([ + dec + ]) + ] + }); + updateDeco(view, null, [ + dec + ]); + expect(view.dom.querySelector("button")).toBeNull(); + }); + it("can remove the class from a text node", async ()=>{ + const dec = make("1-4-foo"); + const { view } = tempEditor({ + doc: doc(p("abc")), + plugins: [ + decoPlugin([ + dec + ]) + ] + }); + expect(view.dom.querySelector(".foo")).not.toBeNull(); + updateDeco(view, null, [ + dec + ]); + expect(view.dom.querySelector(".foo")).toBeNull(); + }); + it("can remove the class from part of a text node", async ()=>{ + const dec = make("2-4-foo"); + const { view } = tempEditor({ + doc: doc(p("abcd")), + plugins: [ + decoPlugin([ + dec + ]) + ] + }); + expect(view.dom.querySelector(".foo")).not.toBeNull(); + updateDeco(view, null, [ + dec + ]); + expect(view.dom.querySelector(".foo")).toBeNull(); + expect(view.dom.firstChild.innerHTML).toBe("abcd"); + }); + it("can change the class for part of a text node", async ()=>{ + const dec = make("2-4-foo"); + const { view } = tempEditor({ + doc: doc(p("abcd")), + plugins: [ + decoPlugin([ + dec + ]) + ] + }); + expect(view.dom.querySelector(".foo")).not.toBeNull(); + updateDeco(view, [ + make("2-4-bar") + ], [ + dec + ]); + expect(view.dom.querySelector(".foo")).toBeNull(); + expect(view.dom.querySelector(".bar")).not.toBeNull(); + }); + it("draws a widget added in the middle of a text node", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")), + plugins: [ + decoPlugin([]) + ] + }); + updateDeco(view, [ + make("3-widget") + ]); + expect(view.dom.firstChild.textContent).toBe("foωo"); + }); + it("can update a text node around a widget", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("bar")), + plugins: [ + decoPlugin([ + "3-widget" + ]) + ] + }); + view.dispatch(view.state.tr.delete(1, 2)); + expect(view.dom.querySelectorAll("button")).toHaveLength(1); + expect(view.dom.firstChild.textContent).toBe("aωr"); + }); + it("can update a text node with an inline decoration", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("bar")), + plugins: [ + decoPlugin([ + "1-3-foo" + ]) + ] + }); + view.dispatch(view.state.tr.delete(1, 2)); + const foo = view.dom.querySelector(".foo"); + expect(foo).not.toBeNull(); + expect(foo.textContent).toBe("a"); + expect(foo.nextSibling.textContent).toBe("r"); + }); + it("correctly redraws a partially decorated node when a widget is added", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one", em("two"))), + plugins: [ + decoPlugin([ + "1-6-foo" + ]) + ] + }); + updateDeco(view, [ + make("6-widget") + ]); + const foos = view.dom.querySelectorAll(".foo"); + expect(foos).toHaveLength(2); + expect(foos[0].textContent).toBe("one"); + expect(foos[1].textContent).toBe("tw"); + }); + it("correctly redraws when skipping split text node", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")), + plugins: [ + decoPlugin([ + "3-widget", + "3-4-foo" + ]) + ] + }); + updateDeco(view, [ + make("4-widget") + ]); + expect(view.dom.querySelectorAll("button")).toHaveLength(2); + }); + it("drops removed node decorations from the view", async ()=>{ + const deco = Decoration.node(1, 6, { + class: "cls" + }); + const { view } = tempEditor({ + doc: doc(blockquote(p("foo"), p("bar"))), + plugins: [ + decoPlugin([ + deco + ]) + ] + }); + updateDeco(view, null, [ + deco + ]); + expect(view.dom.querySelector(".cls")).toBeNull(); + }); + it("can update a node's attributes without replacing the node", async ()=>{ + const deco = Decoration.node(0, 5, { + title: "title", + class: "foo" + }); + const { view } = tempEditor({ + doc: doc(p("foo")), + plugins: [ + decoPlugin([ + deco + ]) + ] + }); + const para = view.dom.querySelector("p"); + updateDeco(view, [ + Decoration.node(0, 5, { + class: "foo bar" + }) + ], [ + deco + ]); + expect(view.dom.querySelector("p")).toBe(para); + expect(para.className).toBe("foo bar"); + expect(para.title).toBeFalsy(); + }); + it("can add and remove CSS custom properties from a node", async ()=>{ + const deco = Decoration.node(0, 5, { + style: "--my-custom-property:36px" + }); + const { view } = tempEditor({ + doc: doc(p("foo")), + plugins: [ + decoPlugin([ + deco + ]) + ] + }); + expect(view.dom.querySelector("p").style.getPropertyValue("--my-custom-property")).toBe("36px"); + updateDeco(view, null, [ + deco + ]); + expect(view.dom.querySelector("p").style.getPropertyValue("--my-custom-property")).toBe(""); + }); + it("updates decorated nodes even if a widget is added before them", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("a"), p("b")), + plugins: [ + decoPlugin([]) + ] + }); + const lastP = view.dom.querySelectorAll("p")[1]; + updateDeco(view, [ + make("3-widget"), + Decoration.node(3, 6, { + style: "color: red" + }) + ]); + expect(lastP.style.color).toBe("red"); + }); + it("doesn't redraw nodes when a widget before them is replaced", async ()=>{ + const w0 = make("3-widget"); + const { view } = tempEditor({ + doc: doc(h1("a"), p("b")), + plugins: [ + decoPlugin([ + w0 + ]) + ] + }); + const initialP = view.dom.querySelector("p"); + view.dispatch(view.state.tr.setMeta("updateDecorations", { + add: [ + make("3-widget") + ], + remove: [ + w0 + ] + }).insertText("c", 5)); + expect(view.dom.querySelector("p")).toBe(initialP); + }); + it("can add and remove inline style", async ()=>{ + const deco = Decoration.inline(1, 6, { + style: "color: rgba(0,10,200,.4); text-decoration: underline" + }); + const { view } = tempEditor({ + doc: doc(p("al", img(), "lo")), + plugins: [ + decoPlugin([ + deco + ]) + ] + }); + expect(view.dom.querySelector("img").style.color).toMatch(/rgba/); + expect(view.dom.querySelector("img").previousSibling.style.textDecoration).toBe("underline"); + updateDeco(view, null, [ + deco + ]); + expect(view.dom.querySelector("img").style.color).toBe(""); + expect(view.dom.querySelector("img").style.textDecoration).toBe(""); + }); + it("passes decorations to a node view", async ()=>{ + let current = ""; + const { view } = tempEditor({ + doc: doc(p("foo"), hr()), + plugins: [ + decoPlugin([]) + ], + nodeViews: { + horizontal_rule: /*#__PURE__*/ forwardRef(function HR(param, ref) { + let { nodeProps , children , ...props } = param; + current = nodeProps.decorations.map((d)=>d.spec.name).join(); + return /*#__PURE__*/ React.createElement("hr", _extends({ + ref: ref + }, props)); + }) + } + }); + const a = Decoration.node(5, 6, {}, { + name: "a" + }); + updateDeco(view, [ + a + ], []); + expect(current).toBe("a"); + updateDeco(view, [ + Decoration.node(5, 6, {}, { + name: "b" + }), + Decoration.node(5, 6, {}, { + name: "c" + }) + ], [ + a + ]); + expect(current).toBe("b,c"); + }); + it("draws the specified marks around a widget", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foobar")), + plugins: [ + decoPlugin([ + widget(4, /*#__PURE__*/ forwardRef(function Img(props, ref) { + return /*#__PURE__*/ React.createElement("img", _extends({}, props, { + ref: ref + })); + }), { + marks: [ + schema.mark("em") + ], + key: "img-widget" + }) + ]) + ] + }); + expect(view.dom.querySelector("em img")).not.toBeNull(); + }); + it("draws widgets inside the marks for their side", async ()=>{ + const { view } = tempEditor({ + doc: doc(p(em("foo"), strong("bar"))), + plugins: [ + decoPlugin([ + widget(4, /*#__PURE__*/ forwardRef(function Img(props, ref) { + return /*#__PURE__*/ React.createElement("img", _extends({}, props, { + ref: ref + })); + }), { + side: -1, + key: "img-widget" + }) + ]), + decoPlugin([ + widget(4, /*#__PURE__*/ forwardRef(function BR(props, ref) { + return /*#__PURE__*/ React.createElement("br", _extends({}, props, { + ref: ref + })); + }), { + key: "br-widget" + }) + ]), + decoPlugin([ + widget(7, /*#__PURE__*/ forwardRef(function Span(param, ref) { + let { widget , getPos , ...props } = param; + return /*#__PURE__*/ React.createElement("span", _extends({}, props, { + ref: ref + })); + }), { + side: 1, + key: "span-widget" + }) + ]) + ] + }); + expect(view.dom.querySelector("em img")).not.toBeNull(); + expect(view.dom.querySelector("strong img")).toBeNull(); + expect(view.dom.querySelector("strong br")).not.toBeNull(); + expect(view.dom.querySelector("em br")).toBeNull(); + expect(view.dom.querySelector("strong span")).toBeNull(); + }); + it("draws decorations inside node views", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ forwardRef(function Paragraph(param, ref) { + let { nodeProps , children , ...props } = param; + return /*#__PURE__*/ React.createElement("p", _extends({ + ref: ref + }, props), children); + }) + }, + plugins: [ + decoPlugin([ + widget(2, /*#__PURE__*/ forwardRef(function Img(props, ref) { + return /*#__PURE__*/ React.createElement("img", _extends({}, props, { + ref: ref + })); + }), { + key: "img-widget" + }) + ]) + ] + }); + expect(view.dom.querySelector("img")).not.toBeNull(); + }); + it("can delay widget drawing to render time", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("hi")), + decorations (state) { + return DecorationSet.create(state.doc, [ + widget(3, /*#__PURE__*/ forwardRef(function Span(props, ref) { + useEditorEffect((view)=>{ + expect(view.state).toBe(state); + }); + return /*#__PURE__*/ React.createElement("span", _extends({}, props, { + ref: ref + }), "!"); + }), { + key: "span-widget" + }) + ]); + } + }); + expect(view.dom.textContent).toBe("hi!"); + }); + it("supports widgets querying their own position", async ()=>{ + tempEditor({ + doc: doc(p("hi")), + decorations (state) { + return DecorationSet.create(state.doc, [ + widget(3, /*#__PURE__*/ forwardRef(function Widget(param, ref) { + let { getPos , ...props } = param; + expect(getPos()).toBe(3); + return /*#__PURE__*/ React.createElement("button", _extends({ + ref: ref + }, props), "ω"); + }), { + key: "button-widget" + }) + ]); + } + }); + }); + it("doesn't redraw widgets with matching keys", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("hi")), + decorations (state) { + return DecorationSet.create(state.doc, [ + widget(2, Widget, { + key: "myButton" + }) + ]); + } + }); + const widgetDOM = view.dom.querySelector("button"); + view.dispatch(view.state.tr.insertText("!", 2, 2)); + expect(view.dom.querySelector("button")).toBe(widgetDOM); + }); + it("doesn't redraw widgets with identical specs", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("hi")), + decorations (state) { + return DecorationSet.create(state.doc, [ + widget(2, Widget, { + side: 1, + key: "widget" + }) + ]); + } + }); + const widgetDOM = view.dom.querySelector("button"); + view.dispatch(view.state.tr.insertText("!", 2, 2)); + expect(view.dom.querySelector("button")).toBe(widgetDOM); + }); + it("doesn't get confused by split text nodes", async ()=>{ + const { 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))); + expect(view.dom.textContent).toBe("abab"); + }); + it("only draws inline decorations on the innermost level", async ()=>{ + const s = new Schema({ + nodes: { + doc: { + content: "(text | thing)*" + }, + text: {}, + thing: { + inline: true, + content: "text*", + toDOM: ()=>[ + "strong", + 0 + ], + parseDOM: [ + { + tag: "strong" + } + ] + } + } + }); + const { 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" + }) + ]) + }); + const styled = view.dom.querySelectorAll(".dec"); + expect(styled).toHaveLength(3); + expect(Array.prototype.map.call(styled, (n)=>n.textContent).join()).toBe("bc,def,gh"); + expect(styled[1].parentNode.nodeName).toBe("STRONG"); + }); + it("can handle nodeName decoration overlapping with classes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one two three")), + plugins: [ + decoPlugin([ + Decoration.inline(2, 13, { + class: "foo" + }), + Decoration.inline(5, 8, { + nodeName: "em" + }) + ]) + ] + }); + expect(view.dom.firstChild.innerHTML).toBe('one two three'); + }); + it("can handle combining decorations from parent editors in child editors", async ()=>{ + let decosFromFirstEditor; + let { view } = tempEditor({ + doc: doc(p("one two three")), + plugins: [ + decoPlugin([ + Decoration.inline(2, 13, { + class: "foo" + }) + ]), + decoPlugin([ + Decoration.inline(2, 13, { + class: "bar" + }) + ]) + ], + nodeViews: { + paragraph: /*#__PURE__*/ forwardRef(function Paragraph(param, ref) { + let { nodeProps , children , ...props } = param; + decosFromFirstEditor = nodeProps.innerDecorations; + return /*#__PURE__*/ React.createElement("p", _extends({ + ref: ref + }, props), children); + }) + } + }); + ({ view } = tempEditor({ + doc: doc(p("one two three")), + plugins: [ + decoPlugin([ + Decoration.inline(1, 12, { + class: "baz" + }) + ]) + ], + decorations: ()=>decosFromFirstEditor + })); + expect(view.dom.querySelectorAll(".foo")).toHaveLength(1); + expect(view.dom.querySelectorAll(".bar")).toHaveLength(1); + expect(view.dom.querySelectorAll(".baz")).toHaveLength(1); + }); +}); diff --git a/dist/esm/components/__tests__/ProseMirror.draw.test.js b/dist/esm/components/__tests__/ProseMirror.draw.test.js new file mode 100644 index 00000000..000cd9d1 --- /dev/null +++ b/dist/esm/components/__tests__/ProseMirror.draw.test.js @@ -0,0 +1,283 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import { Schema } from "prosemirror-model"; +import { Plugin } from "prosemirror-state"; +import { doc, h1, hr, p, pre, schema, strong } from "prosemirror-test-builder"; +import React, { forwardRef } from "react"; +import { tempEditor } from "../../testing/editorViewTestHelpers.js"; +describe("EditorView draw", ()=>{ + it("updates the DOM", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")) + }); + view.dispatch(view.state.tr.insertText("bar")); + expect(view.dom.textContent).toBe("barfoo"); + }); + it("doesn't redraw nodes after changes", async ()=>{ + const { view } = tempEditor({ + doc: doc(h1("foo"), p("bar")) + }); + const oldP = view.dom.querySelector("p"); + view.dispatch(view.state.tr.insertText("!")); + expect(view.dom.querySelector("p")).toBe(oldP); + }); + it("doesn't redraw nodes before changes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), h1("bar")) + }); + const oldP = view.dom.querySelector("p"); + view.dispatch(view.state.tr.insertText("!", 2)); + expect(view.dom.querySelector("p")).toBe(oldP); + }); + it("doesn't redraw nodes between changes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), h1("bar"), pre("baz")) + }); + const oldP = view.dom.querySelector("p"); + const oldPre = view.dom.querySelector("pre"); + view.dispatch(view.state.tr.insertText("!", 2)); + expect(view.dom.querySelector("p")).toBe(oldP); + expect(view.dom.querySelector("pre")).toBe(oldPre); + }); + it("doesn't redraw siblings of a split node", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), h1("bar"), pre("baz")) + }); + const oldP = view.dom.querySelector("p"); + const oldPre = view.dom.querySelector("pre"); + view.dispatch(view.state.tr.split(8)); + expect(view.dom.querySelector("p")).toBe(oldP); + expect(view.dom.querySelector("pre")).toBe(oldPre); + }); + it("doesn't redraw siblings of a joined node", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), h1("bar"), h1("x"), pre("baz")) + }); + const oldP = view.dom.querySelector("p"); + const oldPre = view.dom.querySelector("pre"); + view.dispatch(view.state.tr.join(10)); + expect(view.dom.querySelector("p")).toBe(oldP); + expect(view.dom.querySelector("pre")).toBe(oldPre); + }); + it("doesn't redraw after a big deletion", async ()=>{ + const { view } = tempEditor({ + doc: doc(p(), p(), p(), p(), p(), p(), p(), p(), h1("!"), p(), p()) + }); + const oldH = view.dom.querySelector("h1"); + view.dispatch(view.state.tr.delete(2, 14)); + expect(view.dom.querySelector("h1")).toBe(oldH); + }); + it("adds classes from the attributes prop", async ()=>{ + const { view , rerender } = tempEditor({ + doc: doc(p()), + attributes: { + class: "foo bar" + } + }); + expect(view.dom.classList.contains("foo")).toBeTruthy(); + expect(view.dom.classList.contains("bar")).toBeTruthy(); + expect(view.dom.classList.contains("ProseMirror")).toBeTruthy(); + rerender({ + attributes: { + class: "baz" + } + }); + expect(!view.dom.classList.contains("foo")).toBeTruthy(); + expect(view.dom.classList.contains("baz")).toBeTruthy(); + }); + it("adds style from the attributes prop", async ()=>{ + const { 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;" + } + } + }) + ] + }); + expect(view.dom.style.border).toBe("1px solid red"); + expect(view.dom.style.backgroundColor).toBe("red"); + expect(view.dom.style.color).toBe("red"); + }); + it("can set other attributes", async ()=>{ + const { view , rerender } = tempEditor({ + doc: doc(p()), + attributes: { + spellcheck: "false", + "aria-label": "hello" + } + }); + expect(view.dom.spellcheck).toBe(false); + expect(view.dom.getAttribute("aria-label")).toBe("hello"); + rerender({ + attributes: { + style: "background-color: yellow" + } + }); + expect(view.dom.hasAttribute("aria-label")).toBe(false); + expect(view.dom.style.backgroundColor).toBe("yellow"); + }); + it("can't set the contenteditable attribute", async ()=>{ + const { view } = tempEditor({ + doc: doc(p()), + attributes: { + contenteditable: "false" + } + }); + expect(view.dom.contentEditable).toBe("true"); + }); + it("understands the editable prop", async ()=>{ + const { view , rerender } = tempEditor({ + doc: doc(p()), + editable: ()=>false + }); + expect(view.dom.contentEditable).toBe("false"); + rerender({ + editable: ()=>true + }); + expect(view.dom.contentEditable).toBe("true"); + }); + it("doesn't redraw following paragraphs when a paragraph is split", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("abcde"), p("fg")) + }); + const lastPara = view.dom.lastChild; + view.dispatch(view.state.tr.split(3)); + expect(view.dom.lastChild).toBe(lastPara); + }); + it("doesn't greedily match nodes that have another match", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("a"), p("b"), p()) + }); + const secondPara = view.dom.querySelectorAll("p")[1]; + view.dispatch(view.state.tr.split(2)); + expect(view.dom.querySelectorAll("p")[2]).toBe(secondPara); + }); + it("creates and destroys plugin views", async ()=>{ + const events = []; + let PluginView = class PluginView { + update() { + events.push("update"); + } + destroy() { + events.push("destroy"); + } + }; + const plugin = new Plugin({ + view () { + events.push("create"); + return new PluginView(); + } + }); + const { view , unmount } = tempEditor({ + plugins: [ + plugin + ] + }); + view.dispatch(view.state.tr.insertText("u")); + unmount(); + expect(events.join(" ")).toBe("create update destroy"); + }); + it("redraws changed node views", async ()=>{ + const { view , rerender } = tempEditor({ + doc: doc(p("foo"), hr()) + }); + expect(view.dom.querySelector("hr")).toBeTruthy(); + rerender({ + nodeViews: { + horizontal_rule: /*#__PURE__*/ forwardRef(function HorizontalRule(param, ref) { + let { nodeProps , ...props } = param; + return /*#__PURE__*/ React.createElement("var", _extends({ + ref: ref + }, props)); + }) + } + }); + expect(!view.dom.querySelector("hr")).toBeTruthy(); + expect(view.dom.querySelector("var")).toBeTruthy(); + }); + it("doesn't get confused by merged nodes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p(strong("one"), " two ", strong("three"))) + }); + view.dispatch(view.state.tr.removeMark(1, 4, schema.marks.strong)); + expect(view.dom.querySelectorAll("strong")).toHaveLength(1); + }); + it("doesn't redraw too much when marks are present", async ()=>{ + const 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" + } + ] + } + } + }); + const paragraphs = []; + for(let i = 1; i <= 10; i++)paragraphs.push(s.node("paragraph", null, [ + s.text("para " + i) + ], [ + s.mark("m") + ])); + const { view } = tempEditor({ + // @ts-expect-error this is fine + doc: s.node("doc", null, paragraphs) + }); + const initialChildren = Array.from(view.dom.querySelectorAll("p")); + const 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)); + const currentChildren = Array.from(view.dom.querySelectorAll("p")); + let sameAtEnd = 0; + while(sameAtEnd < currentChildren.length && sameAtEnd < initialChildren.length && currentChildren[currentChildren.length - sameAtEnd - 1] == initialChildren[initialChildren.length - sameAtEnd - 1])sameAtEnd++; + expect(sameAtEnd).toBe(9); + }); +}); diff --git a/dist/esm/components/__tests__/ProseMirror.node-view.test.js b/dist/esm/components/__tests__/ProseMirror.node-view.test.js new file mode 100644 index 00000000..c0eccc92 --- /dev/null +++ b/dist/esm/components/__tests__/ProseMirror.node-view.test.js @@ -0,0 +1,272 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import { screen } from "@testing-library/react"; +import { Plugin } from "prosemirror-state"; +import { blockquote, br, doc, p } from "prosemirror-test-builder"; +import { Decoration, DecorationSet } from "prosemirror-view"; +import React, { forwardRef, useEffect } from "react"; +import { useEditorState } from "../../hooks/useEditorState.js"; +import { useStopEvent } from "../../hooks/useStopEvent.js"; +import { tempEditor } from "../../testing/editorViewTestHelpers.js"; +describe("nodeViews prop", ()=>{ + it("can replace a node's representation", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo", br())), + nodeViews: { + hard_break: /*#__PURE__*/ forwardRef(function Var(props, ref) { + return /*#__PURE__*/ React.createElement("var", { + ref: ref + }, props.children); + }) + } + }); + expect(view.dom.querySelector("var")).not.toBeNull(); + }); + it("can override drawing of a node's content", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ forwardRef(function Paragraph(props, ref) { + return /*#__PURE__*/ React.createElement("p", { + ref: ref + }, props.nodeProps.node.textContent.toUpperCase()); + }) + } + }); + expect(view.dom.querySelector("p").textContent).toBe("FOO"); + view.dispatch(view.state.tr.insertText("a")); + expect(view.dom.querySelector("p").textContent).toBe("AFOO"); + }); + // React makes this more or less trivial; the render + // method of the component _is_ the update (and create) + // method + // eslint-disable-next-line jest/no-disabled-tests + it.skip("can register its own update method", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ forwardRef(function Paragraph(props, ref) { + return /*#__PURE__*/ React.createElement("p", { + ref: ref + }, props.nodeProps.node.textContent.toUpperCase()); + }) + } + }); + const para = view.dom.querySelector("p"); + view.dispatch(view.state.tr.insertText("a")); + expect(view.dom.querySelector("p")).toBe(para); + expect(para.textContent).toBe("AFOO"); + }); + it("allows decoration updates for node views with an update method", async ()=>{ + const { view , rerender } = tempEditor({ + doc: doc(p("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ forwardRef(function Paragraph(param, ref) { + let { children , nodeProps , ...props } = param; + return /*#__PURE__*/ React.createElement("p", _extends({ + ref: ref + }, props), children); + }) + } + }); + rerender({ + decorations (state) { + return DecorationSet.create(state.doc, [ + Decoration.inline(2, 3, { + someattr: "ok" + }), + Decoration.node(0, 5, { + otherattr: "ok" + }) + ]); + } + }); + expect(view.dom.querySelector("[someattr]")).not.toBeNull(); + expect(view.dom.querySelector("[otherattr]")).not.toBeNull(); + }); + it("can provide a contentDOM property", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ forwardRef(function Paragraph(props, ref) { + return(// ContentDOM is inferred from where props.children is rendered + /*#__PURE__*/ React.createElement("p", { + ref: ref + }, props.children)); + }) + } + }); + const para = view.dom.querySelector("p"); + view.dispatch(view.state.tr.insertText("a")); + expect(view.dom.querySelector("p")).toBe(para); + expect(para.textContent).toBe("afoo"); + }); + it("has its destroy method called", async ()=>{ + let destroyed = false; + const { view } = tempEditor({ + doc: doc(p("foo", br())), + nodeViews: { + hard_break: /*#__PURE__*/ forwardRef(function BR(_props, ref) { + // React implements "destroy methods" with effect + // hooks + useEffect(()=>{ + return ()=>{ + destroyed = true; + }; + }, []); + return /*#__PURE__*/ React.createElement("br", { + ref: ref + }); + }) + } + }); + expect(destroyed).toBeFalsy(); + view.dispatch(view.state.tr.delete(3, 5)); + expect(destroyed).toBeTruthy(); + }); + it("can query its own position", async ()=>{ + let pos; + const { view } = tempEditor({ + doc: doc(blockquote(p("abc"), p("foo", br()))), + nodeViews: { + hard_break: /*#__PURE__*/ forwardRef(function BR(param, ref) { + let { nodeProps , children , ...props } = param; + // trigger a re-render on every updated, otherwise we won't + // re-render when an updated doesn't directly affect us + useEditorState(); + pos = nodeProps.getPos(); + return /*#__PURE__*/ React.createElement("br", _extends({ + ref: ref + }, props)); + }) + } + }); + expect(pos).toBe(10); + view.dispatch(view.state.tr.insertText("a")); + expect(pos).toBe(11); + }); + it("has access to outer decorations", async ()=>{ + const plugin = new Plugin({ + state: { + init () { + return null; + }, + apply (tr, prev) { + return tr.getMeta("setDeco") || prev; + } + }, + props: { + decorations (state) { + const deco = this.getState(state); + return deco && DecorationSet.create(state.doc, [ + Decoration.inline(0, state.doc.content.size, {}, { + name: deco + }) + ]); + } + } + }); + const { view } = tempEditor({ + doc: doc(p("foo", br())), + plugins: [ + plugin + ], + nodeViews: { + hard_break: /*#__PURE__*/ forwardRef(function Var(props, ref) { + return /*#__PURE__*/ React.createElement("var", { + ref: ref + }, props.nodeProps.decorations.length ? props.nodeProps.decorations[0].spec.name : "[]"); + }) + } + }); + expect(view.dom.querySelector("var").textContent).toBe("[]"); + view.dispatch(view.state.tr.setMeta("setDeco", "foo")); + expect(view.dom.querySelector("var").textContent).toBe("foo"); + view.dispatch(view.state.tr.setMeta("setDeco", "bar")); + expect(view.dom.querySelector("var").textContent).toBe("bar"); + }); + it("provides access to inner decorations in the constructor", async ()=>{ + tempEditor({ + doc: doc(p("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ forwardRef(function Paragraph(props, ref) { + expect(props.nodeProps.innerDecorations.find().map((d)=>`${d.from}-${d.to}`).join()).toBe("1-2"); + return /*#__PURE__*/ React.createElement("p", { + ref: ref + }, props.children); + }) + }, + 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", async ()=>{ + let innerDecos = []; + const { rerender } = tempEditor({ + doc: doc(p("foo")), + nodeViews: { + paragraph: /*#__PURE__*/ forwardRef(function Paragraph(props, ref) { + innerDecos = props.nodeProps.innerDecorations.find().map((d)=>`${d.from}-${d.to}`); + return /*#__PURE__*/ React.createElement("p", { + ref: ref + }, props.children); + }) + } + }); + rerender({ + decorations (state) { + return DecorationSet.create(state.doc, [ + Decoration.inline(2, 3, { + someattr: "ok" + }), + Decoration.node(0, 5, { + otherattr: "ok" + }) + ]); + } + }); + expect(innerDecos.join()).toBe("1-2"); + }); + it("can provide a stopEvent hook", async ()=>{ + tempEditor({ + doc: doc(p("input value")), + nodeViews: { + paragraph: /*#__PURE__*/ forwardRef(function ParagraphInput(param, ref) { + let { nodeProps , children , ...props } = param; + useStopEvent(()=>{ + return true; + }); + return /*#__PURE__*/ React.createElement("input", _extends({ + ref: ref, + type: "text", + defaultValue: nodeProps.node.textContent + }, props)); + }) + } + }); + const input = screen.getByDisplayValue("input value"); + input.focus(); + await browser.keys("z"); + expect(await $(input).getValue()).toBe("input valuez"); + }); +}); diff --git a/dist/esm/components/__tests__/ProseMirror.selection.test.js b/dist/esm/components/__tests__/ProseMirror.selection.test.js new file mode 100644 index 00000000..7fa99f67 --- /dev/null +++ b/dist/esm/components/__tests__/ProseMirror.selection.test.js @@ -0,0 +1,440 @@ +/* eslint-disable jest/no-disabled-tests */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { act, screen } from "@testing-library/react"; +import { NodeSelection, Selection } from "prosemirror-state"; +import { blockquote, br, code, code_block, doc, em, hr, img as img_, li, p, strong, ul } from "prosemirror-test-builder"; +import { Decoration, DecorationSet } from "prosemirror-view"; +import { tempEditor } from "../../testing/editorViewTestHelpers.js"; +const img = img_({ + src: "data:image/gif;base64,R0lGODlhBQAFAIABAAAAAP///yH5BAEKAAEALAAAAAAFAAUAAAIEjI+pWAA7" +}); +async function findTextNode(_, text) { + const parent = await screen.findByText(text); + return parent.firstChild; +} +function allPositions(doc) { + const 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) { + const range = document.createRange(); + range.setEnd(node, offset); + range.setStart(node, offset); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); +} +function getSel() { + const sel = window.getSelection(); + let node = sel.focusNode, offset = sel.focusOffset; + while(node && node.nodeType != 3){ + const after = offset < node.childNodes.length && node.childNodes[offset]; + const 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) { + const selection = typeof sel == "number" ? Selection.near(view.state.doc.resolve(sel)) : sel; + act(()=>{ + view.dispatch(view.state.tr.setSelection(selection)); + }); +} +function event(code) { + const event = document.createEvent("Event"); + event.initEvent("keydown", true, true); + event.keyCode = code; + return event; +} +const LEFT = 37, RIGHT = 39, UP = 38, DOWN = 40; +describe("ProseMirror", ()=>{ + it("can read the DOM selection", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one"), hr(), blockquote(p("two"))) + }); + function test(node, offset, expected) { + setDOMSel(node, offset); + view.dom.focus(); + act(()=>{ + view.domObserver.flush(); + }); + const sel = view.state.selection; + expect(sel.head == null ? sel.from : sel.head).toBe(expected); + } + const one = await findTextNode(view.dom, "one"); + const two = await 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", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one"), hr(), blockquote(p("two"))) + }); + function test(pos, node, offset) { + setSel(view, pos); + const sel = getSel(); + expect(sel.node).toBe(node); + expect(sel.offset).toBe(offset); + } + const one = await findTextNode(view.dom, "one"); + const two = await 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", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one"), p("two")) + }); + const p00 = view.coordsAtPos(1); + const p01 = view.coordsAtPos(2); + const p03 = view.coordsAtPos(4); + const p10 = view.coordsAtPos(6); + const p13 = view.coordsAtPos(9); + expect(p00.bottom).toBeGreaterThan(p00.top); + expect(p13.bottom).toBeGreaterThan(p13.top); + expect(p00.top).toEqual(p01.top); + expect(p01.top).toEqual(p03.top); + expect(p00.bottom).toEqual(p03.bottom); + expect(p10.top).toEqual(p13.top); + expect(p01.left).toBeGreaterThan(p00.left); + expect(p03.left).toBeGreaterThan(p01.left); + expect(p10.top).toBeGreaterThan(p00.top); + expect(p13.left).toBeGreaterThan(p10.left); + }); + it("returns proper coordinates in code blocks", async ()=>{ + const { view } = tempEditor({ + doc: doc(code_block("a\nb\n")) + }), p = []; + for(let i = 1; i <= 5; i++)p.push(view.coordsAtPos(i)); + const [p0, p1, p2, p3, p4] = p; + expect(p0.top).toBe(p1.top); + expect(p0.left).toBeLessThan(p1.left); + expect(p2.top).toBeGreaterThan(p1.top); + expect(p2.top).toBe(p3.top); + expect(p2.left).toBeLessThan(p3.left); + expect(p2.left).toBe(p0.left); + expect(p4.top).toBeGreaterThan(p3.top); + // This one shows a small (0.01 pixel) difference in Firefox for + // some reason. + expect(Math.round(p4.left)).toBe(Math.round(p2.left)); + }); + // TODO: This test fails on the position between the img and the br (it returns + // the position before the img, instead of after). + it.skip("produces sensible screen coordinates in corner cases", async ()=>{ + const { 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)=>{ + const coords = view.coordsAtPos(pos); + const found = view.posAtCoords({ + top: coords.top + 1, + left: coords.left + }).pos; + expect(found).toBe(pos); + setSel(view, pos); + }); + ok(null); + }, 20); + }); + }); + it("doesn't return zero-height rectangles after leaves", async ()=>{ + const { view } = tempEditor({ + doc: doc(p(img)) + }); + const coords = view.coordsAtPos(2, 1); + expect(coords.bottom - coords.top).toBeGreaterThan(5); + }); + it("produces horizontal rectangles for positions between blocks", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("ha"), hr(), blockquote(p("ba"))) + }); + const a = view.coordsAtPos(0); + expect(a.top).toBe(a.bottom); + expect(a.top).toBe(view.dom.firstChild.getBoundingClientRect().top); + expect(a.left).toBeLessThan(a.right); + const b = view.coordsAtPos(4); + expect(b.top).toBe(b.bottom); + expect(b.top).toBeGreaterThan(a.top); + expect(b.left).toBeLessThan(b.right); + const c = view.coordsAtPos(5); + expect(c.top).toBe(c.bottom); + expect(c.top).toBeGreaterThan(b.top); + const d = view.coordsAtPos(6); + expect(d.top).toBe(d.bottom); + expect(d.left).toBeLessThan(d.right); + expect(d.top).toBeLessThan(view.dom.getBoundingClientRect().bottom); + }); + it("produces sensible screen coordinates around line breaks", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one two three four five-six-seven-eight")) + }); + function afterSpace(pos) { + return pos > 0 && view.state.doc.textBetween(pos - 1, pos) == " "; + } + view.dom.style.width = "4em"; + let prevBefore; + let prevAfter; + allPositions(view.state.doc).forEach((pos)=>{ + const coords = view.coordsAtPos(pos, 1); + if (prevAfter) // eslint-disable-next-line jest/no-conditional-expect + expect(prevAfter.top < coords.top || prevAfter.top == coords.top && prevAfter.left < coords.left).toBeTruthy(); + prevAfter = coords; + const found = view.posAtCoords({ + top: coords.top + 1, + left: coords.left + }).pos; + expect(found).toBe(pos); + const coordsBefore = view.coordsAtPos(pos, -1); + if (prevBefore) // eslint-disable-next-line jest/no-conditional-expect + expect(prevBefore.top < coordsBefore.top || prevBefore.top == coordsBefore.top && (prevBefore.left < coordsBefore.left || afterSpace(pos) && prevBefore.left == coordsBefore.left)).toBeTruthy(); + prevBefore = coordsBefore; + }); + }); + it("can find coordinates on node boundaries", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one ", em("two"), " ", em(strong("three")))) + }); + let prev; + allPositions(view.state.doc).forEach((pos)=>{ + const coords = view.coordsAtPos(pos, 1); + if (prev) // eslint-disable-next-line jest/no-conditional-expect + expect(prev.top < coords.top || Math.abs(prev.top - coords.top) < 4 && prev.left < coords.left).toBeTruthy(); + prev = coords; + }); + }); + it("finds proper coordinates in RTL text", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("مرآة نثرية")) + }); + view.dom.style.direction = "rtl"; + let prev; + allPositions(view.state.doc).forEach((pos)=>{ + const coords = view.coordsAtPos(pos, 1); + if (prev) // eslint-disable-next-line jest/no-conditional-expect + expect(prev.top < coords.top || Math.abs(prev.top - coords.top) < 4 && prev.left > coords.left).toBeTruthy(); + prev = coords; + }); + }); + it("can go back and forth between screen coordsa and document positions", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("one"), blockquote(p("two"), p("three"))) + }); + [ + 1, + 2, + 4, + 7, + 14, + 15 + ].forEach((pos)=>{ + const coords = view.coordsAtPos(pos); + const found = view.posAtCoords({ + top: coords.top + 1, + left: coords.left + }).pos; + expect(found).toBe(pos); + }); + }); + it("returns correct screen coordinates for wrapped lines", async ()=>{ + const { view } = tempEditor({}); + const top = view.coordsAtPos(1); + let pos = 1, end; + for(let i = 0; i < 100; i++){ + view.dispatch(view.state.tr.insertText("a bc de fg h")); + pos += 12; + end = view.coordsAtPos(pos); + if (end.bottom > top.bottom + 4) break; + } + expect(view.posAtCoords({ + left: end.left + 50, + top: end.top + 5 + }).pos).toBe(pos); + }); + it("makes arrow motion go through selectable inline nodes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo", img, "bar")) + }); + act(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.from).toBe(4); + act(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.head).toBe(5); + expect(view.state.selection.anchor).toBe(5); + act(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.from).toBe(4); + act(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.head).toBe(4); + expect(view.state.selection.anchor).toBe(4); + }); + it("makes arrow motion go through selectable block nodes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("hello"), hr(), ul(li(p("there")))) + }); + act(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(7); + setSel(view, 11); + act(()=>{ + view.dispatchEvent(event(UP)); + }); + expect(view.state.selection.from).toBe(7); + }); + it("supports arrow motion through adjacent blocks", async ()=>{ + const { view } = tempEditor({ + doc: doc(blockquote(p("hello")), hr(), hr(), p("there")) + }); + act(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(9); + act(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(10); + setSel(view, 14); + act(()=>{ + view.dispatchEvent(event(UP)); + }); + expect(view.state.selection.from).toBe(10); + act(()=>{ + view.dispatchEvent(event(UP)); + }); + expect(view.state.selection.from).toBe(9); + }); + it("support horizontal motion through blocks", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), hr(), hr(), p("bar")) + }); + act(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.from).toBe(5); + act(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.from).toBe(6); + act(()=>{ + view.dispatchEvent(event(RIGHT)); + }); + expect(view.state.selection.head).toBe(8); + act(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.from).toBe(6); + act(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.from).toBe(5); + act(()=>{ + view.dispatchEvent(event(LEFT)); + }); + expect(view.state.selection.head).toBe(4); + }); + it("allows moving directly from an inline node to a block node", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo", img), hr(), p(img, "bar")) + }); + setSel(view, NodeSelection.create(view.state.doc, 4)); + act(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(6); + setSel(view, NodeSelection.create(view.state.doc, 8)); + act(()=>{ + view.dispatchEvent(event(UP)); + }); + expect(view.state.selection.from).toBe(6); + }); + it("updates the selection even if the DOM parameters look unchanged", async ()=>{ + const { view , rerender } = tempEditor({ + doc: doc(p("foobar")) + }); + view.focus(); + const decos = DecorationSet.create(view.state.doc, [ + Decoration.inline(1, 4, { + color: "green" + }) + ]); + rerender({ + decorations () { + return decos; + } + }); + rerender({ + decorations: undefined + }); + rerender({ + decorations () { + return decos; + } + }); + const range = document.createRange(); + range.setEnd(document.getSelection().anchorNode, document.getSelection().anchorOffset); + range.setStart(view.dom, 0); + expect(range.toString()).toBe("foobar"); + }); + it("sets selection even if Selection.extend throws DOMException", async ()=>{ + const originalExtend = window.Selection.prototype.extend; + window.Selection.prototype.extend = ()=>{ + // declare global: DOMException + throw new DOMException("failed"); + }; + try { + const { view } = tempEditor({ + doc: doc(p("foo", img), hr(), p(img, "bar")) + }); + setSel(view, NodeSelection.create(view.state.doc, 4)); + act(()=>{ + view.dispatchEvent(event(DOWN)); + }); + expect(view.state.selection.from).toBe(6); + } finally{ + window.Selection.prototype.extend = originalExtend; + } + }); + it("doesn't put the cursor after BR hack nodes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p()) + }); + view.focus(); + expect(getSelection().focusOffset).toBe(0); + }); +}); diff --git a/dist/esm/components/__tests__/ProseMirror.test.js b/dist/esm/components/__tests__/ProseMirror.test.js new file mode 100644 index 00000000..afa84417 --- /dev/null +++ b/dist/esm/components/__tests__/ProseMirror.test.js @@ -0,0 +1,339 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import { render, screen } from "@testing-library/react"; +import { Schema } from "prosemirror-model"; +import { EditorState, Plugin } from "prosemirror-state"; +import { doc, em, hr, li, p, schema, strong, ul } from "prosemirror-test-builder"; +import React, { forwardRef, useEffect, useState } from "react"; +import { useEditorEffect } from "../../hooks/useEditorEffect.js"; +import { useStopEvent } from "../../hooks/useStopEvent.js"; +import { reactKeys } from "../../plugins/reactKeys.js"; +import { tempEditor } from "../../testing/editorViewTestHelpers.js"; +import { ProseMirror } from "../ProseMirror.js"; +import { ProseMirrorDoc } from "../ProseMirrorDoc.js"; +describe("ProseMirror", ()=>{ + it("renders a contenteditable", async ()=>{ + const schema = new Schema({ + nodes: { + text: {}, + doc: { + content: "text*" + } + } + }); + const editorState = EditorState.create({ + schema + }); + function TestEditor() { + return /*#__PURE__*/ React.createElement(ProseMirror, { + defaultState: editorState + }, /*#__PURE__*/ React.createElement(ProseMirrorDoc, { + "data-testid": "editor" + })); + } + render(/*#__PURE__*/ React.createElement(TestEditor, null)); + const editor = screen.getByTestId("editor"); + editor.focus(); + await browser.keys("H"); + await browser.keys("e"); + await browser.keys("l"); + await browser.keys("l"); + await browser.keys("o"); + await browser.keys(","); + await browser.keys(" "); + await browser.keys("w"); + await browser.keys("o"); + await browser.keys("r"); + await browser.keys("l"); + await browser.keys("d"); + await browser.keys("!"); + expect(editor.textContent).toBe("Hello, world!"); + }); + it("supports lifted editor state", async ()=>{ + const schema = new Schema({ + nodes: { + text: {}, + doc: { + content: "text*" + } + } + }); + let outerEditorState = EditorState.create({ + schema + }); + function TestEditor() { + const [editorState, setEditorState] = useState(outerEditorState); + useEffect(()=>{ + outerEditorState = editorState; + }, [ + editorState + ]); + return /*#__PURE__*/ React.createElement(ProseMirror, { + state: editorState, + dispatchTransaction: (tr)=>setEditorState(editorState.apply(tr)) + }, /*#__PURE__*/ React.createElement(ProseMirrorDoc, { + "data-testid": "editor" + })); + } + render(/*#__PURE__*/ React.createElement(TestEditor, null)); + const editor = screen.getByTestId("editor"); + editor.focus(); + await browser.keys("H"); + await browser.keys("e"); + await browser.keys("l"); + await browser.keys("l"); + await browser.keys("o"); + await browser.keys(","); + await browser.keys(" "); + await browser.keys("w"); + await browser.keys("o"); + await browser.keys("r"); + await browser.keys("l"); + await browser.keys("d"); + await browser.keys("!"); + expect(outerEditorState.doc.textContent).toBe("Hello, world!"); + }); + it("supports React NodeViews", async ()=>{ + const schema = new Schema({ + nodes: { + text: {}, + paragraph: { + content: "text*", + toDOM () { + return [ + "p", + 0 + ]; + } + }, + doc: { + content: "paragraph+" + } + } + }); + const editorState = EditorState.create({ + schema + }); + const Paragraph = /*#__PURE__*/ forwardRef(function Paragraph(param, ref) { + let { children } = param; + return /*#__PURE__*/ React.createElement("p", { + ref: ref, + "data-testid": "paragraph" + }, children); + }); + const reactNodeViews = { + paragraph: Paragraph + }; + function TestEditor() { + return /*#__PURE__*/ React.createElement(ProseMirror, { + defaultState: editorState, + nodeViews: reactNodeViews + }, /*#__PURE__*/ React.createElement(ProseMirrorDoc, { + "data-testid": "editor" + })); + } + render(/*#__PURE__*/ React.createElement(TestEditor, null)); + const editor = screen.getByTestId("editor"); + editor.focus(); + await browser.keys("H"); + await browser.keys("e"); + await browser.keys("l"); + await browser.keys("l"); + await browser.keys("o"); + await browser.keys(","); + await browser.keys(" "); + await browser.keys("w"); + await browser.keys("o"); + await browser.keys("r"); + await browser.keys("l"); + await browser.keys("d"); + await browser.keys("!"); + expect(editor.textContent).toBe("Hello, world!"); + // Ensure that ProseMirror really rendered our Paragraph + // component, not just any old

tag + expect(screen.getAllByTestId("paragraph").length).toBeGreaterThanOrEqual(1); + }); + it("reflects the current state in .props", async ()=>{ + const { view } = tempEditor({ + doc: doc(p()) + }); + expect(view.state).toBe(view.props.state); + }); + it("calls handleScrollToSelection when appropriate", async ()=>{ + let scrolled = 0; + const { view } = tempEditor({ + doc: doc(p()), + handleScrollToSelection: ()=>{ + scrolled++; + return false; + } + }); + view.dispatch(view.state.tr.scrollIntoView()); + expect(scrolled).toBe(1); + }); + it("can be queried for the DOM position at a doc position", async ()=>{ + const { view } = tempEditor({ + doc: doc(ul(li(p(strong("foo"))))) + }); + const inText = view.domAtPos(4); + expect(inText.offset).toBe(1); + expect(inText.node.nodeValue).toBe("foo"); + const beforeLI = view.domAtPos(1); + expect(beforeLI.offset).toBe(0); + expect(beforeLI.node.nodeName).toBe("UL"); + const afterP = view.domAtPos(7); + expect(afterP.offset).toBe(1); + expect(afterP.node.nodeName).toBe("LI"); + }); + it("can bias DOM position queries to enter nodes", async ()=>{ + const { view } = tempEditor({ + doc: doc(p(em(strong("a"), "b"), "c")) + }); + function get(pos, bias) { + const r = view.domAtPos(pos, bias); + return (r.node.nodeType == 1 ? r.node.nodeName : r.node.nodeValue) + "@" + r.offset; + } + expect(get(1, 0)).toBe("P@0"); + expect(get(1, -1)).toBe("P@0"); + expect(get(1, 1)).toBe("a@0"); + expect(get(2, -1)).toBe("a@1"); + expect(get(2, 0)).toBe("EM@1"); + expect(get(2, 1)).toBe("b@0"); + expect(get(3, -1)).toBe("b@1"); + expect(get(3, 0)).toBe("P@1"); + expect(get(3, 1)).toBe("c@0"); + expect(get(4, -1)).toBe("c@1"); + expect(get(4, 0)).toBe("P@2"); + expect(get(4, 1)).toBe("P@2"); + }); + it("can be queried for a node's DOM representation", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), hr()) + }); + expect(view.nodeDOM(0).nodeName).toBe("P"); + expect(view.nodeDOM(5).nodeName).toBe("HR"); + expect(view.nodeDOM(3)).toBeNull(); + }); + it("can map DOM positions to doc positions", async ()=>{ + const { view } = tempEditor({ + doc: doc(p("foo"), hr()) + }); + expect(view.posAtDOM(view.dom.firstChild.firstChild, 2)).toBe(3); + expect(view.posAtDOM(view.dom, 1)).toBe(5); + expect(view.posAtDOM(view.dom, 2)).toBe(6); + expect(view.posAtDOM(view.dom.lastChild, 0, -1)).toBe(5); + expect(view.posAtDOM(view.dom.lastChild, 0, 1)).toBe(6); + }); + it("binds this to itself in dispatchTransaction prop", async ()=>{ + let thisBinding; + const { view } = tempEditor({ + doc: doc(p("foo"), hr()), + dispatchTransaction () { + // eslint-disable-next-line @typescript-eslint/no-this-alias + thisBinding = this; + } + }); + view.dispatch(view.state.tr.insertText("x")); + expect(view).toBe(thisBinding); + }); + it("replaces the EditorView when ProseMirror would redraw", async ()=>{ + const viewPlugin = ()=>new Plugin({ + props: { + nodeViews: { + horizontal_rule () { + const dom = document.createElement("hr"); + return { + dom + }; + } + } + } + }); + const startDoc = doc(p()); + const firstState = EditorState.create({ + doc: startDoc, + schema, + plugins: [ + viewPlugin(), + reactKeys() + ] + }); + let firstView = null; + let secondView = null; + function Test() { + useEditorEffect((v)=>{ + if (firstView === null) { + firstView = v; + } else { + secondView = v; + } + }); + return null; + } + const Paragraph = /*#__PURE__*/ forwardRef(function Paragraph(param, ref) { + let { nodeProps , children , ...props } = param; + return /*#__PURE__*/ React.createElement("p", _extends({ + ref: ref, + "data-testid": "node-view" + }, props), children); + }); + const { rerender } = render(/*#__PURE__*/ React.createElement(ProseMirror, { + state: firstState, + nodeViews: { + paragraph: Paragraph + } + }, /*#__PURE__*/ React.createElement(Test, null), /*#__PURE__*/ React.createElement(ProseMirrorDoc, null))); + expect(()=>screen.getByTestId("node-view")).not.toThrow(); + const secondState = EditorState.create({ + doc: startDoc, + schema, + plugins: [ + viewPlugin(), + reactKeys() + ] + }); + rerender(/*#__PURE__*/ React.createElement(ProseMirror, { + state: secondState, + nodeViews: { + paragraph: Paragraph + } + }, /*#__PURE__*/ React.createElement(Test, null), /*#__PURE__*/ React.createElement(ProseMirrorDoc, null))); + expect(()=>screen.getByTestId("node-view")).not.toThrow(); + expect(firstView).not.toBeNull(); + expect(secondView).not.toBeNull(); + expect(firstView === secondView).toBeFalsy(); + }); + it("supports focusing interactive controls", async ()=>{ + tempEditor({ + doc: doc(hr()), + nodeViews: { + horizontal_rule: /*#__PURE__*/ forwardRef(function Button(param, ref) { + let { nodeProps , ...props } = param; + useStopEvent(()=>{ + return true; + }); + return /*#__PURE__*/ React.createElement("button", _extends({ + id: "button", + ref: ref, + type: "button" + }, props), "Click me"); + }) + } + }); + const button = screen.getByText("Click me"); + await $("#button").click(); + expect(document.activeElement === button).toBeTruthy(); + }); +}); diff --git a/dist/esm/contexts/ChildDescriptorsContext.js b/dist/esm/contexts/ChildDescriptorsContext.js new file mode 100644 index 00000000..7a3a91f8 --- /dev/null +++ b/dist/esm/contexts/ChildDescriptorsContext.js @@ -0,0 +1,9 @@ +import { createContext } from "react"; +export const ChildDescriptorsContext = createContext({ + parentRef: { + current: undefined + }, + siblingsRef: { + current: [] + } +}); diff --git a/dist/esm/contexts/EditorContext.js b/dist/esm/contexts/EditorContext.js new file mode 100644 index 00000000..dccd408d --- /dev/null +++ b/dist/esm/contexts/EditorContext.js @@ -0,0 +1,7 @@ +import { createContext } from "react"; +/** + * Provides the EditorView, as well as the current + * EditorState. Should not be consumed directly; instead + * see `useEditorState`, `useEditorViewEvent`, and + * `useEditorViewLayoutEffect`. + */ export const EditorContext = createContext(null); diff --git a/dist/esm/contexts/EditorStateContext.js b/dist/esm/contexts/EditorStateContext.js new file mode 100644 index 00000000..f7e2fbd4 --- /dev/null +++ b/dist/esm/contexts/EditorStateContext.js @@ -0,0 +1,2 @@ +import { createContext } from "react"; +export const EditorStateContext = createContext(null); diff --git a/dist/esm/contexts/LayoutGroupContext.js b/dist/esm/contexts/LayoutGroupContext.js new file mode 100644 index 00000000..4c66150f --- /dev/null +++ b/dist/esm/contexts/LayoutGroupContext.js @@ -0,0 +1,2 @@ +import { createContext } from "react"; +export const LayoutGroupContext = /*#__PURE__*/ createContext(null); diff --git a/dist/esm/contexts/NodeViewContext.js b/dist/esm/contexts/NodeViewContext.js new file mode 100644 index 00000000..113b0125 --- /dev/null +++ b/dist/esm/contexts/NodeViewContext.js @@ -0,0 +1,2 @@ +import { createContext } from "react"; +export const NodeViewContext = /*#__PURE__*/ createContext(null); diff --git a/dist/esm/contexts/StopEventContext.js b/dist/esm/contexts/StopEventContext.js new file mode 100644 index 00000000..dbc05f74 --- /dev/null +++ b/dist/esm/contexts/StopEventContext.js @@ -0,0 +1,2 @@ +import { createContext } from "react"; +export const StopEventContext = createContext(null); diff --git a/dist/esm/contexts/__tests__/DeferredLayoutEffects.test.js b/dist/esm/contexts/__tests__/DeferredLayoutEffects.test.js new file mode 100644 index 00000000..583e991d --- /dev/null +++ b/dist/esm/contexts/__tests__/DeferredLayoutEffects.test.js @@ -0,0 +1,98 @@ +import { act, render, screen } from "@testing-library/react"; +import React, { useLayoutEffect, useState } from "react"; +import { LayoutGroup } from "../../components/LayoutGroup.js"; +import { useLayoutGroupEffect } from "../../hooks/useLayoutGroupEffect.js"; +describe("DeferredLayoutEffects", ()=>{ + jest.useFakeTimers("modern"); + it("registers multiple effects and runs them", ()=>{ + function Parent() { + return /*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(Child, null)); + } + function Child() { + const [double, setDouble] = useState(1); + useLayoutEffect(()=>{ + if (double === 2) { + setTimeout(()=>{ + setDouble((d)=>d * 2.5); + }, 500); + } + if (double === 20) { + setDouble((d)=>d * 2.5); + } + }, [ + double + ]); + useLayoutGroupEffect(()=>{ + const timeout = setTimeout(()=>{ + setDouble((d)=>d * 2); + }, 1000); + return ()=>{ + clearTimeout(timeout); + }; + }, [ + double + ]); + return /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("div", { + "data-testid": "double" + }, double)); + } + // The component mounts ... + // ... the initial value should be 1 + // ... there should be one timeout scheduled by the deferred effect + render(/*#__PURE__*/ React.createElement(Parent, null)); + expect(screen.getByTestId("double").innerHTML).toBe("1"); + // This block assert that deferred effects run. + // -------------------------------------------- + // 1000 milliseconds go by ... + // ... the timeout set by the deferred effect should run + // ... the timeout should double the new value to 2 + // ... the immediate effect should set a timeout + // ... the deferred effect should set a timeout + act(()=>{ + jest.advanceTimersByTime(1000); + }); + expect(screen.getByTestId("double").innerHTML).toBe("2"); + // The next three blocks assert that cleanup of deferred effects run. + // ------------------------------------------------------------------ + // 500 milliseconds go by ... + // ... the timeout set by the immediate effect should run + // ... the timeout should set the value to 5 + // ... the old deferred effect should cancel its timeout + // ... the new deferred effect should set a new timeout + act(()=>{ + jest.advanceTimersByTime(500); + }); + expect(screen.getByTestId("double").innerHTML).toBe("5"); + // ... 500 more milliseconds go by ... + // ... the canceled timeout should not run + // ... the rescheduled timoeut should not yet run + act(()=>{ + jest.advanceTimersByTime(500); + }); + expect(screen.getByTestId("double").innerHTML).toBe("5"); + // ... 500 more milliseconds go by ... + // ... the rescheduled timeout should run + // ... the timeout should double the value to 10 + // ... the deferred effect should set a new timeout + act(()=>{ + jest.advanceTimersByTime(500); + }); + expect(screen.getByTestId("double").innerHTML).toBe("10"); + // The next block asserts that cancelation of deferred effects works. + // ------------------------------------------------------------------ + // 1000 milliseconds go by ... + // ... the timeout set by the deferred effect should run + // ... the timeout should double the value to 20 + // ... the immediate effect should then set the value to 50 + // ... the deferred effect from the first render should not run + // ... the deferred effect from the second render should run + // ... the deferred effect that does run should set a new timeout + act(()=>{ + jest.advanceTimersByTime(1000); + }); + // For this assertion, we need to clear a timer from the React scheduler. + jest.advanceTimersByTime(1); + expect(screen.getByTestId("double").innerHTML).toBe("50"); + expect(jest.getTimerCount()).toBe(1); + }); +}); diff --git a/dist/esm/decorations/ReactWidgetType.js b/dist/esm/decorations/ReactWidgetType.js new file mode 100644 index 00000000..6c9a9363 --- /dev/null +++ b/dist/esm/decorations/ReactWidgetType.js @@ -0,0 +1,37 @@ +import { Decoration } from "prosemirror-view"; +function compareObjs(a, b) { + if (a == b) return true; + for(const p in a)if (a[p] !== b[p]) return false; + for(const p in b)if (!(p in a)) return false; + return true; +} +const noSpec = { + side: 0 +}; +export class ReactWidgetType { + map(mapping, span, offset, oldOffset) { + const { pos , deleted } = mapping.mapResult(span.from + oldOffset, this.side < 0 ? -1 : 1); + // @ts-expect-error The Decoration constructor is private/internal, but + // we need to use it for our custom widget implementation here. + return deleted ? null : new Decoration(pos - offset, pos - offset, this); + } + valid() { + return true; + } + eq(other) { + return this == other || other instanceof ReactWidgetType && (this.spec.key && this.spec.key == other.spec.key || this.Component == other.Component && compareObjs(this.spec, other.spec)); + } + destroy() { + // Can be implemented with React effect hooks + } + constructor(Component, spec){ + this.Component = Component; + this.spec = spec ?? noSpec; + this.side = this.spec.side ?? 0; + } +} +export function widget(pos, component, spec) { + // @ts-expect-error The Decoration constructor is private/internal, but + // we need to use it for our custom widget implementation here. + return new Decoration(pos, pos, new ReactWidgetType(component, spec)); +} diff --git a/dist/esm/decorations/computeDocDeco.js b/dist/esm/decorations/computeDocDeco.js new file mode 100644 index 00000000..2f3e2cbb --- /dev/null +++ b/dist/esm/decorations/computeDocDeco.js @@ -0,0 +1,44 @@ +import { Decoration } from "prosemirror-view"; +const DocDecorationsCache = new WeakMap(); +/** + * Produces the outer decorations for the doc node, based + * on the attributes editor prop. + * + * The return value of this function is memoized; if it is to + * return an equivalent value to the last time it was called for + * a given EditorView, it will return exactly that previous value. + * + * This makes it safe to call in a React render function, even + * if its result is used in a dependencies array for a hook. + */ export function computeDocDeco(view) { + const attrs = Object.create(null); + attrs.class = "ProseMirror"; + attrs.contenteditable = String(view.editable); + view.someProp("attributes", (value)=>{ + if (typeof value == "function") value = value(view.state); + if (value) for(const attr in value){ + if (attr == "class") attrs.class += " " + value[attr]; + else if (attr == "style") attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr]; + else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName") attrs[attr] = String(value[attr]); + } + }); + if (!attrs.translate) attrs.translate = "no"; + const next = [ + Decoration.node(0, view.state.doc.content.size, attrs) + ]; + const previous = DocDecorationsCache.get(view); + if (!previous) { + DocDecorationsCache.set(view, next); + return next; + } + if (previous[0].to !== view.state.doc.content.size) { + DocDecorationsCache.set(view, next); + return next; + } + // @ts-expect-error Internal property (Decoration.type) + if (!previous[0].type.eq(next[0].type)) { + DocDecorationsCache.set(view, next); + return next; + } + return previous; +} diff --git a/dist/esm/decorations/internalTypes.js b/dist/esm/decorations/internalTypes.js new file mode 100644 index 00000000..2234b9ca --- /dev/null +++ b/dist/esm/decorations/internalTypes.js @@ -0,0 +1 @@ +export { }; diff --git a/dist/esm/decorations/iterDeco.js b/dist/esm/decorations/iterDeco.js new file mode 100644 index 00000000..49af1174 --- /dev/null +++ b/dist/esm/decorations/iterDeco.js @@ -0,0 +1,73 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { ReactWidgetType } from "./ReactWidgetType.js"; +function compareSide(a, b) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return a.type.side - b.type.side; +} +// 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. +export function iterDeco(parent, deco, // Callbacks have been slightly modified to pass +// the offset, so that we can pass the position as +// a prop to components +onWidget, onNode) { + const locals = deco.locals(parent); + let offset = 0; + // Simple, cheap variant for when there are no local decorations + if (locals.length == 0) { + for(let i = 0; i < parent.childCount; i++){ + const child = parent.child(i); + onNode(child, locals, deco.forChild(offset, child), offset, i); + offset += child.nodeSize; + } + return; + } + let decoIndex = 0; + const active = []; + let restNode = null; + for(let parentIndex = 0;;){ + if (decoIndex < locals.length && locals[decoIndex].to == offset) { + const widget = locals[decoIndex++]; + let 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], // eslint-disable-next-line @typescript-eslint/no-explicit-any + !(widgets[i].type instanceof ReactWidgetType), offset, parentIndex + i, !!restNode); + } else { + onWidget(widget, // eslint-disable-next-line @typescript-eslint/no-explicit-any + !(widget.type instanceof ReactWidgetType), offset, 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; + } + } + const outerDeco = child.isInline && !child.isLeaf ? active.filter((d)=>!d.inline) : active.slice(); + onNode(child, outerDeco, deco.forChild(offset, child), offset, index); + offset = end; + } +} diff --git a/dist/esm/decorations/viewDecorations.js b/dist/esm/decorations/viewDecorations.js new file mode 100644 index 00000000..047b7a42 --- /dev/null +++ b/dist/esm/decorations/viewDecorations.js @@ -0,0 +1,162 @@ +import { DecorationSet } from "prosemirror-view"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const none = [], noSpec = {}; +const empty = DecorationSet.empty; +// 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. +let DecorationGroup = class DecorationGroup { + 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++){ + const 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++){ + const locals = this.members[i].localsInner(node); + if (!locals.length) continue; + if (!result) { + result = locals; + } else { + if (sorted) { + result = result.slice(); + sorted = false; + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + for(let j = 0; j < locals.length; j++)result.push(locals[j]); + } + } + return result ? removeOverlap(sorted ? result : result.sort(byPos)) : none; + } + // 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: + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return members[0]; + default: + return new DecorationGroup(members.every((m)=>m instanceof DecorationSet) ? members : members.reduce((r, m)=>r.concat(m instanceof DecorationSet ? m : m.members), [])); + } + } + forEachSet(f) { + for(let i = 0; i < this.members.length; i++)// eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.members[i].forEachSet(f); + } + constructor(members){ + this.members = members; + } +}; +// 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; +} +// 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++){ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const span = working[i]; + if (span.from != span.to) for(let j = i + 1; j < working.length; j++){ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const 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) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + while(i < array.length && byPos(deco, array[i]) > 0)i++; + array.splice(i, 0, deco); +} +const ViewDecorationsCache = new WeakMap(); +/** + * Produces the DecorationSource for the current state, based + * on the decorations editor prop. + * + * The return value of this function is memoized; if it is to + * return an equivalent value to the last time it was called for + * a given EditorView, it will return exactly that previous value. + * + * This makes it safe to call in a React render function, even + * if its result is used in a dependencies array for a hook. + */ export function viewDecorations(view, cursorWrapper) { + const found = []; + view.someProp("decorations", (f)=>{ + const result = f(view.state); + if (result && result != empty) found.push(result); + }); + // We don't have access to types for view.cursorWrapper here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (cursorWrapper) { + found.push(// eslint-disable-next-line @typescript-eslint/no-explicit-any + DecorationSet.create(view.state.doc, [ + cursorWrapper + ])); + } + const previous = ViewDecorationsCache.get(view); + if (!previous) { + const result = DecorationGroup.from(found); + ViewDecorationsCache.set(view, result); + return result; + } + let numPrevious = 0; + let areSetsEqual = true; + previous.forEachSet((set)=>{ + const next = found[numPrevious++]; + if (next !== set) { + areSetsEqual = false; + } + }); + if (numPrevious !== found.length) { + areSetsEqual = false; + } + if (!areSetsEqual) { + const result = DecorationGroup.from(found); + ViewDecorationsCache.set(view, result); + return result; + } + return previous; +} diff --git a/dist/esm/dom.js b/dist/esm/dom.js new file mode 100644 index 00000000..1ee3f587 --- /dev/null +++ b/dist/esm/dom.js @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ export const domIndex = function(node) { + for(let index = 0;; index++){ + node = node.previousSibling; + if (!node) return index; + } +}; +export const parentNode = function(node) { + const 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) { + const 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))) { + const 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; + const index = domIndex(node); + node = node.parentNode; + if (!node) return false; + atStart = atStart && index == 0; + atEnd = atEnd && index == nodeSize(node); + } + return false; +} +export 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) { + return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset, domSel.anchorNode, domSel.anchorOffset); +}; +export function keyEvent(keyCode, key) { + const event = document.createEvent("Event"); + event.initEvent("keydown", true, true); + event.keyCode = keyCode; + event.key = event.code = key; + return event; +} +export function deepActiveElement(doc) { + let elt = doc.activeElement; + while(elt && elt.shadowRoot)elt = elt.shadowRoot.activeElement; + return elt; +} +export function caretFromPoint(doc, x, y) { + if (doc.caretPositionFromPoint) { + try { + // Firefox throws for this call in hard-to-predict circumstances (#994) + const pos = doc.caretPositionFromPoint(x, y); + // Clip the offset, because Chrome will return a text offset + // into nodes, which can't be treated as a regular DOM + // offset + if (pos) return { + node: pos.offsetNode, + offset: Math.min(nodeSize(pos.offsetNode), pos.offset) + }; + } catch (_) { + // pass + } + } + if (doc.caretRangeFromPoint) { + const range = doc.caretRangeFromPoint(x, y); + if (range) return { + node: range.startContainer, + offset: Math.min(nodeSize(range.startContainer), range.startOffset) + }; + } + return; +} diff --git a/dist/esm/hooks/__tests__/useEditorViewLayoutEffect.test.js b/dist/esm/hooks/__tests__/useEditorViewLayoutEffect.test.js new file mode 100644 index 00000000..a82a15a8 --- /dev/null +++ b/dist/esm/hooks/__tests__/useEditorViewLayoutEffect.test.js @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ import { render } from "@testing-library/react"; +import React from "react"; +import { LayoutGroup } from "../../components/LayoutGroup.js"; +import { EditorContext } from "../../contexts/EditorContext.js"; +import { EditorStateContext } from "../../contexts/EditorStateContext.js"; +import { useEditorEffect } from "../useEditorEffect.js"; +function TestComponent(param) { + let { effect , dependencies =[] } = param; + // eslint-disable-next-line react-hooks/exhaustive-deps + useEditorEffect(effect, dependencies); + return null; +} +describe("useEditorViewLayoutEffect", ()=>{ + it("should run the effect", ()=>{ + const effect = jest.fn(); + const editorView = {}; + const editorState = {}; + const registerEventListener = ()=>{}; + const unregisterEventListener = ()=>{}; + render(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, { + value: { + view: editorView, + registerEventListener, + unregisterEventListener + } + }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ React.createElement(TestComponent, { + effect: effect + }))))); + expect(effect).toHaveBeenCalled(); + expect(effect).toHaveBeenCalledWith(editorView); + }); + it("should not re-run the effect if no dependencies change", ()=>{ + const effect = jest.fn(); + const editorView = {}; + const editorState = {}; + const registerEventListener = ()=>{}; + const unregisterEventListener = ()=>{}; + const contextValue = { + view: editorView, + registerEventListener, + unregisterEventListener + }; + const { rerender } = render(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, { + value: contextValue + }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ React.createElement(TestComponent, { + effect: effect, + dependencies: [] + })), " "))); + rerender(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, { + value: contextValue + }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ React.createElement(TestComponent, { + effect: effect, + dependencies: [] + }))))); + expect(effect).toHaveBeenCalledTimes(1); + }); + it("should re-run the effect if dependencies change", ()=>{ + const effect = jest.fn(); + const editorView = {}; + const editorState = {}; + const registerEventListener = ()=>{}; + const unregisterEventListener = ()=>{}; + const { rerender } = render(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, { + value: { + view: editorView, + registerEventListener, + unregisterEventListener + } + }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ React.createElement(TestComponent, { + effect: effect, + dependencies: [ + "one" + ] + }))))); + rerender(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, { + value: { + view: editorView, + registerEventListener, + unregisterEventListener + } + }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, { + value: editorState + }, /*#__PURE__*/ React.createElement(TestComponent, { + effect: effect, + dependencies: [ + "two" + ] + }))))); + expect(effect).toHaveBeenCalledTimes(2); + }); +}); diff --git a/dist/esm/hooks/useComponentEventListeners.js b/dist/esm/hooks/useComponentEventListeners.js new file mode 100644 index 00000000..40b9d18f --- /dev/null +++ b/dist/esm/hooks/useComponentEventListeners.js @@ -0,0 +1,54 @@ +import { useCallback, useMemo, useState } from "react"; +import { componentEventListeners } from "../plugins/componentEventListeners.js"; +/** + * Produces a plugin that can be used with ProseMirror to handle DOM + * events at the EditorView.dom element. + * + * - `reactEventsPlugin` is a ProseMirror plugin for handling DOM events + * at the EditorView.dom element. It should be passed to `useEditorView`, + * along with any other plugins. + * + * - `registerEventListener` and `unregisterEventListener` should be + * passed to `EditorContext.Provider`. + * + * @privateRemarks + * + * This hook uses a combination of mutable and immutable updates to give + * us precise control over when we re-create the ProseMirror plugin. + * + * The plugin has a mutable reference to the set of handlers for each + * event type, but the set of event types is static. This means that we + * need to produce a new ProseMirror plugin whenever a new event type is + * registered. We avoid producing a new ProseMirrer plugin in any other + * scenario to avoid the performance overhead of reconfiguring the plugins + * in the EditorView. + * + * To accomplish this, we shallowly clone the registry whenever a new event + * type is registered. + */ export function useComponentEventListeners() { + const [registry, setRegistry] = useState(new Map()); + const registerEventListener = useCallback((eventType, handler)=>{ + const handlers = registry.get(eventType) ?? []; + handlers.unshift(handler); + if (!registry.has(eventType)) { + registry.set(eventType, handlers); + setRegistry(new Map(registry)); + } + }, [ + registry + ]); + const unregisterEventListener = useCallback((eventType, handler)=>{ + const handlers = registry.get(eventType); + handlers?.splice(handlers.indexOf(handler), 1); + }, [ + registry + ]); + const componentEventListenersPlugin = useMemo(()=>componentEventListeners(registry), [ + registry + ]); + return { + registerEventListener, + unregisterEventListener, + componentEventListenersPlugin + }; +} diff --git a/dist/esm/hooks/useEditor.js b/dist/esm/hooks/useEditor.js new file mode 100644 index 00000000..de3ea567 --- /dev/null +++ b/dist/esm/hooks/useEditor.js @@ -0,0 +1,274 @@ +import { Schema } from "prosemirror-model"; +import { EditorState } from "prosemirror-state"; +import { DecorationSet, EditorView } from "prosemirror-view"; +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { flushSync } from "react-dom"; +import { beforeInputPlugin } from "../plugins/beforeInputPlugin.js"; +import { SelectionDOMObserver } from "../selection/SelectionDOMObserver.js"; +import { NodeViewDesc } from "../viewdesc.js"; +import { useComponentEventListeners } from "./useComponentEventListeners.js"; +import { useForceUpdate } from "./useForceUpdate.js"; +function buildNodeViews(view) { + const result = Object.create(null); + function add(obj) { + for(const prop in obj)if (!Object.prototype.hasOwnProperty.call(result, prop)) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result[prop] = obj[prop]; + } + view.someProp("nodeViews", add); + view.someProp("markViews", add); + return result; +} +function changedNodeViews(a, b) { + let nA = 0, nB = 0; + for(const prop in a){ + if (a[prop] != b[prop]) return true; + nA++; + } + for(const _ in b)nB++; + return nA != nB; +} +function changedProps(a, b) { + for (const prop of Object.keys(a)){ + if (a[prop] !== b[prop]) return true; + } + return false; +} +function getEditable(view) { + return !view.someProp("editable", (value)=>value(view.state) === false); +} +// @ts-expect-error We're making use of knowledge of internal methods here +export class ReactEditorView extends EditorView { + /** + * Whether the EditorView's updateStateInner method thinks that the + * docView needs to be blown away and redrawn. + * + * @privateremarks + * + * When ProseMirror View detects that the EditorState has been reconfigured + * to provide new custom node views, it calls an internal function that + * we can't override in order to recreate the entire editor DOM. + * + * This property mimics that check, so that we can replace the EditorView + * with another of our own, preventing ProseMirror View from taking over + * DOM management responsibility. + */ get needsRedraw() { + if (this.oldProps.state.plugins === this._props.state.plugins && this._props.plugins === this.oldProps.plugins) { + return false; + } + const newNodeViews = buildNodeViews(this); + // @ts-expect-error Internal property + return changedNodeViews(this.nodeViews, newNodeViews); + } + /** + * Like setProps, but without executing any side effects. + * Safe to use in a component render method. + */ pureSetProps(props) { + // this.oldProps = this.props; + this._props = { + ...this._props, + ...props + }; + this.state = this._props.state; + this.editable = getEditable(this); + } + /** + * Triggers any side effects that have been queued by previous + * calls to pureSetProps. + */ runPendingEffects() { + if (changedProps(this.props, this.oldProps)) { + const newProps = this.props; + this._props = this.oldProps; + this.state = this._props.state; + this.update(newProps); + } + } + update(props) { + super.update(props); + // Ensure that side effects aren't re-triggered until + // pureSetProps is called again + this.oldProps = props; + } + updatePluginViews(prevState) { + if (this.shouldUpdatePluginViews) { + // @ts-expect-error We're making use of knowledge of internal methods here + super.updatePluginViews(prevState); + } + } + // We want to trigger the default EditorView cleanup, but without + // the actual view.dom cleanup (which React will have already handled). + // So we give the EditorView a dummy DOM element and ask it to clean up + destroy() { + // @ts-expect-error we're intentionally overwriting this property + // to prevent side effects + this.dom = document.createElement("div"); + super.destroy(); + } + constructor(place, props){ + // Call the superclass constructor with an empty + // document and limited props. We'll set everything + // else ourselves. + super(place, { + state: EditorState.create({ + schema: props.state.schema, + plugins: props.state.plugins + }), + plugins: props.plugins + }); + this.shouldUpdatePluginViews = false; + this.shouldUpdatePluginViews = true; + this._props = props; + this.oldProps = { + state: props.state + }; + this.state = props.state; + // @ts-expect-error We're making use of knowledge of internal attributes here + this.domObserver.stop(); + // @ts-expect-error We're making use of knowledge of internal attributes here + this.domObserver = new SelectionDOMObserver(this); + // @ts-expect-error We're making use of knowledge of internal attributes here + this.domObserver.start(); + this.editable = getEditable(this); + // Destroy the DOM created by the default + // ProseMirror ViewDesc implementation; we + // have a NodeViewDesc from React instead. + // @ts-expect-error We're making use of knowledge of internal attributes here + this.docView.dom.replaceChildren(); + // @ts-expect-error We're making use of knowledge of internal attributes here + this.docView = props.docView; + } +} +const EMPTY_SCHEMA = new Schema({ + nodes: { + doc: { + content: "text*" + }, + text: { + inline: true + } + } +}); +const EMPTY_STATE = EditorState.create({ + schema: EMPTY_SCHEMA +}); +let didWarnValueDefaultValue = false; +/** + * Creates, mounts, and manages a ProseMirror `EditorView`. + * + * All state and props updates are executed in a layout effect. + * To ensure that the EditorState and EditorView are never out of + * sync, it's important that the EditorView produced by this hook + * is only accessed through the `useEditorViewEvent` and + * `useEditorViewLayoutEffect` hooks. + */ export function useEditor(mount, options) { + if (process.env.NODE_ENV !== "production") { + if (options.defaultState !== undefined && options.state !== undefined && !didWarnValueDefaultValue) { + console.error("A component contains a ProseMirror editor with both value and defaultValue props. " + "ProseMirror editors must be either controlled or uncontrolled " + "(specify either the state prop, or the defaultState prop, but not both). " + "Decide between using a controlled or uncontrolled ProseMirror editor " + "and remove one of these props. More info: " + "https://reactjs.org/link/controlled-components"); + didWarnValueDefaultValue = true; + } + } + const [view, setView] = useState(null); + const [cursorWrapper, _setCursorWrapper] = useState(null); + const forceUpdate = useForceUpdate(); + const defaultState = options.defaultState ?? EMPTY_STATE; + const [_state, setState] = useState(defaultState); + const state = options.state ?? _state; + const { componentEventListenersPlugin , registerEventListener , unregisterEventListener } = useComponentEventListeners(); + const setCursorWrapper = useCallback((deco)=>{ + flushSync(()=>{ + _setCursorWrapper(deco); + }); + }, []); + const plugins = useMemo(()=>[ + ...options.plugins ?? [], + componentEventListenersPlugin, + beforeInputPlugin(setCursorWrapper) + ], [ + options.plugins, + componentEventListenersPlugin, + setCursorWrapper + ]); + const dispatchTransaction = useCallback(function dispatchTransaction(tr) { + flushSync(()=>{ + if (!options.state) { + setState((s)=>s.apply(tr)); + } + if (options.dispatchTransaction) { + options.dispatchTransaction.call(this, tr); + } + }); + }, [ + options.dispatchTransaction, + options.state + ]); + const tempDom = document.createElement("div"); + const docViewDescRef = useRef(new NodeViewDesc(undefined, [], -1, state.doc, [], DecorationSet.empty, tempDom, null, tempDom, ()=>false)); + const directEditorProps = { + ...options, + state, + plugins, + dispatchTransaction, + docView: docViewDescRef.current + }; + useLayoutEffect(()=>{ + return ()=>{ + view?.destroy(); + }; + }, [ + view + ]); + // This rule is concerned about infinite updates due to the + // call to setView. These calls are deliberately conditional, + // so this is not a concern. + // eslint-disable-next-line react-hooks/exhaustive-deps + useLayoutEffect(()=>{ + if (view && view.dom !== mount) { + setView(null); + } + if (!mount) { + return; + } + if (!view) { + const newView = new ReactEditorView({ + mount + }, directEditorProps); + setView(newView); + newView.dom.addEventListener("compositionend", forceUpdate); + return; + } + }); + // This rule is concerned about infinite updates due to the + // call to setView. These calls are deliberately conditional, + // so this is not a concern. + // eslint-disable-next-line react-hooks/exhaustive-deps + useLayoutEffect(()=>{ + // If ProseMirror View is about to redraw the entire document's + // DOM, clear the EditorView and reconstruct another, instead. + // This only happens when a newly instantiated EditorState has + // been provided. + if (view?.needsRedraw) { + setView(null); + return; + } else { + // @ts-expect-error Internal property - domObserver + view?.domObserver.selectionToDOM(); + view?.runPendingEffects(); + } + }); + view?.pureSetProps(directEditorProps); + const editor = useMemo(()=>({ + view: view, + registerEventListener, + unregisterEventListener, + cursorWrapper, + docViewDescRef + }), [ + view, + registerEventListener, + unregisterEventListener, + cursorWrapper + ]); + return { + editor, + state + }; +} diff --git a/dist/esm/hooks/useEditorEffect.js b/dist/esm/hooks/useEditorEffect.js new file mode 100644 index 00000000..75039ed8 --- /dev/null +++ b/dist/esm/hooks/useEditorEffect.js @@ -0,0 +1,38 @@ +import { useContext } from "react"; +import { EditorContext } from "../contexts/EditorContext.js"; +import { useLayoutGroupEffect } from "./useLayoutGroupEffect.js"; +/** + * Registers a layout effect to run after the EditorView has + * been updated with the latest EditorState and Decorations. + * + * Effects can take an EditorView instance as an argument. + * This hook should be used to execute layout effects that + * depend on the EditorView, such as for positioning DOM + * nodes based on ProseMirror positions. + * + * Layout effects registered with this hook still fire + * synchronously after all DOM mutations, but they do so + * _after_ the EditorView has been updated, even when the + * EditorView lives in an ancestor component. + */ export function useEditorEffect(effect, dependencies) { + const { view } = useContext(EditorContext); + // The rules of hooks want `effect` to be included in the + // dependency list, but dependency issues for `effect` will + // be caught by the linter at the call-site for + // `useEditorViewLayoutEffect`. + // Note: we specifically don't want to re-run the effect + // every time it changes, because it will most likely + // be defined inline and run on every re-render. + useLayoutGroupEffect(()=>{ + if (view) { + return effect(view); + } + }, // The rules of hooks want to be able to statically + // verify the dependencies for the effect, but this will + // have already happened at the call-site. + // eslint-disable-next-line react-hooks/exhaustive-deps + dependencies && [ + view, + ...dependencies + ]); +} diff --git a/dist/esm/hooks/useEditorEventCallback.js b/dist/esm/hooks/useEditorEventCallback.js new file mode 100644 index 00000000..aef95af5 --- /dev/null +++ b/dist/esm/hooks/useEditorEventCallback.js @@ -0,0 +1,35 @@ +import { useCallback, useContext, useRef } from "react"; +import { EditorContext } from "../contexts/EditorContext.js"; +import { useEditorEffect } from "./useEditorEffect.js"; +/** + * Returns a stable function reference to be used as an + * event handler callback. + * + * The callback will be called with the EditorView instance + * as its first argument. + * + * This hook is dependent on both the + * `EditorViewContext.Provider` and the + * `DeferredLayoutEffectProvider`. It can only be used in a + * component that is mounted as a child of both of these + * providers. + */ export function useEditorEventCallback(callback) { + const ref = useRef(callback); + const { view } = useContext(EditorContext); + useEditorEffect(()=>{ + ref.current = callback; + }, [ + callback + ]); + return useCallback(function() { + for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ + args[_key] = arguments[_key]; + } + if (view) { + return ref.current(view, ...args); + } + return; + }, [ + view + ]); +} diff --git a/dist/esm/hooks/useEditorEventListener.js b/dist/esm/hooks/useEditorEventListener.js new file mode 100644 index 00000000..9a9f833a --- /dev/null +++ b/dist/esm/hooks/useEditorEventListener.js @@ -0,0 +1,28 @@ +import { useCallback, useContext, useRef } from "react"; +import { EditorContext } from "../contexts/EditorContext.js"; +import { useEditorEffect } from "./useEditorEffect.js"; +/** + * Attaches an event listener at the `EditorView`'s DOM node. See + * [the ProseMirror docs](https://prosemirror.net/docs/ref/#view.EditorProps.handleDOMEvents) + * for more details. + */ export function useEditorEventListener(eventType, handler) { + const { registerEventListener , unregisterEventListener } = useContext(EditorContext); + const ref = useRef(handler); + useEditorEffect(()=>{ + ref.current = handler; + }, [ + handler + ]); + const eventHandler = useCallback(function(view, event) { + return ref.current.call(this, view, event); + }, []); + useEditorEffect(()=>{ + registerEventListener(eventType, eventHandler); + return ()=>unregisterEventListener(eventType, eventHandler); + }, [ + eventHandler, + eventType, + registerEventListener, + unregisterEventListener + ]); +} diff --git a/dist/esm/hooks/useEditorState.js b/dist/esm/hooks/useEditorState.js new file mode 100644 index 00000000..0424a85f --- /dev/null +++ b/dist/esm/hooks/useEditorState.js @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { EditorStateContext } from "../contexts/EditorStateContext.js"; +/** + * Provides access to the current EditorState value. + */ export function useEditorState() { + const editorState = useContext(EditorStateContext); + return editorState; +} diff --git a/dist/esm/hooks/useForceUpdate.js b/dist/esm/hooks/useForceUpdate.js new file mode 100644 index 00000000..8fbe3114 --- /dev/null +++ b/dist/esm/hooks/useForceUpdate.js @@ -0,0 +1,8 @@ +import { useReducer } from "react"; +/** + * Provides a function that forces an update of the + * component. + */ export function useForceUpdate() { + const [, forceUpdate] = useReducer((x)=>x + 1, 0); + return forceUpdate; +} diff --git a/dist/esm/hooks/useLayoutGroupEffect.js b/dist/esm/hooks/useLayoutGroupEffect.js new file mode 100644 index 00000000..abe39eac --- /dev/null +++ b/dist/esm/hooks/useLayoutGroupEffect.js @@ -0,0 +1,9 @@ +import { useContext, useLayoutEffect } from "react"; +import { LayoutGroupContext } from "../contexts/LayoutGroupContext.js"; +/** Registers a layout effect to run at the nearest `LayoutGroup` boundary. */ export function useLayoutGroupEffect(effect, deps) { + const register = useContext(LayoutGroupContext); + // The rule for hooks wants to statically verify the deps, + // but the dependencies are up to the caller, not this implementation. + // eslint-disable-next-line react-hooks/exhaustive-deps + useLayoutEffect(()=>register(effect), deps); +} diff --git a/dist/esm/hooks/useNodeViewDescriptor.js b/dist/esm/hooks/useNodeViewDescriptor.js new file mode 100644 index 00000000..bdf03554 --- /dev/null +++ b/dist/esm/hooks/useNodeViewDescriptor.js @@ -0,0 +1,88 @@ +import { useCallback, useContext, useLayoutEffect, useRef, useState } from "react"; +import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js"; +import { EditorContext } from "../contexts/EditorContext.js"; +import { CompositionViewDesc, NodeViewDesc } from "../viewdesc.js"; +export function useNodeViewDescriptor(node, getPos, domRef, nodeDomRef, innerDecorations, outerDecorations, viewDesc, contentDOMRef) { + const { view } = useContext(EditorContext); + const [hasContentDOM, setHasContentDOM] = useState(true); + const nodeViewDescRef = useRef(viewDesc); + const stopEvent = useRef(()=>false); + const setStopEvent = useCallback((newStopEvent)=>{ + stopEvent.current = newStopEvent; + }, []); + const { siblingsRef , parentRef } = useContext(ChildDescriptorsContext); + const childDescriptors = useRef([]); + useLayoutEffect(()=>{ + const siblings = siblingsRef.current; + return ()=>{ + if (!nodeViewDescRef.current) return; + if (siblings.includes(nodeViewDescRef.current)) { + const index = siblings.indexOf(nodeViewDescRef.current); + siblings.splice(index, 1); + } + }; + }, [ + siblingsRef + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + useLayoutEffect(()=>{ + if (!node || !nodeDomRef.current) return; + const firstChildDesc = childDescriptors.current[0]; + if (!nodeViewDescRef.current) { + nodeViewDescRef.current = new NodeViewDesc(parentRef.current, childDescriptors.current, getPos(), node, outerDecorations, innerDecorations, domRef?.current ?? nodeDomRef.current, firstChildDesc?.dom.parentElement ?? null, nodeDomRef.current, (event)=>!!stopEvent.current(event)); + } else { + nodeViewDescRef.current.parent = parentRef.current; + nodeViewDescRef.current.children = childDescriptors.current; + nodeViewDescRef.current.node = node; + nodeViewDescRef.current.pos = getPos(); + nodeViewDescRef.current.outerDeco = outerDecorations; + nodeViewDescRef.current.innerDeco = innerDecorations; + nodeViewDescRef.current.dom = domRef?.current ?? nodeDomRef.current; + // @ts-expect-error We have our own ViewDesc implementations + nodeViewDescRef.current.dom.pmViewDesc = nodeViewDescRef.current; + nodeViewDescRef.current.contentDOM = // If there's already a contentDOM, we can just + // keep it; it won't have changed. This is especially + // important during compositions, where the + // firstChildDesc might not have a correct dom node set yet. + contentDOMRef?.current ?? nodeViewDescRef.current.contentDOM ?? firstChildDesc?.dom.parentElement ?? null; + nodeViewDescRef.current.nodeDOM = nodeDomRef.current; + } + setHasContentDOM(nodeViewDescRef.current.contentDOM !== null); + if (!siblingsRef.current.includes(nodeViewDescRef.current)) { + siblingsRef.current.push(nodeViewDescRef.current); + } + siblingsRef.current.sort((a, b)=>a.pos - b.pos); + for (const childDesc of childDescriptors.current){ + childDesc.parent = nodeViewDescRef.current; + // Because TextNodeViews can't locate the DOM nodes + // for compositions, we need to override them here + if (childDesc instanceof CompositionViewDesc) { + const compositionTopDOM = nodeViewDescRef.current.contentDOM?.firstChild; + if (!compositionTopDOM) throw new Error(`Started a composition but couldn't find the text node it belongs to.`); + let textDOM = compositionTopDOM; + while(textDOM.firstChild){ + textDOM = textDOM.firstChild; + } + if (!textDOM || !(textDOM instanceof Text)) throw new Error(`Started a composition but couldn't find the text node it belongs to.`); + childDesc.dom = compositionTopDOM; + childDesc.textDOM = textDOM; + childDesc.text = textDOM.data; + // @ts-expect-error ??? + childDesc.textDOM.pmViewDesc = childDesc; + // @ts-expect-error ??? + view?.input.compositionNodes.push(childDesc); + } + } + return ()=>{ + if (nodeViewDescRef.current?.children[0] instanceof CompositionViewDesc && !view?.composing) { + nodeViewDescRef.current?.children[0].dom.parentNode?.removeChild(nodeViewDescRef.current?.children[0].dom); + } + }; + }); + return { + hasContentDOM, + childDescriptors, + nodeViewDescRef, + setStopEvent + }; +} diff --git a/dist/esm/hooks/useReactKeys.js b/dist/esm/hooks/useReactKeys.js new file mode 100644 index 00000000..de6de68a --- /dev/null +++ b/dist/esm/hooks/useReactKeys.js @@ -0,0 +1,7 @@ +import { useContext } from "react"; +import { EditorContext } from "../contexts/EditorContext.js"; +import { reactKeysPluginKey } from "../plugins/reactKeys.js"; +export function useReactKeys() { + const { view } = useContext(EditorContext); + return view && reactKeysPluginKey.getState(view.state); +} diff --git a/dist/esm/hooks/useStopEvent.js b/dist/esm/hooks/useStopEvent.js new file mode 100644 index 00000000..db1e2335 --- /dev/null +++ b/dist/esm/hooks/useStopEvent.js @@ -0,0 +1,14 @@ +import { useContext } from "react"; +import { StopEventContext } from "../contexts/StopEventContext.js"; +import { useEditorEffect } from "./useEditorEffect.js"; +import { useEditorEventCallback } from "./useEditorEventCallback.js"; +export function useStopEvent(stopEvent) { + const register = useContext(StopEventContext); + const stopEventMemo = useEditorEventCallback(stopEvent); + useEditorEffect(()=>{ + register(stopEventMemo); + }, [ + register, + stopEventMemo + ]); +} diff --git a/dist/esm/index.js b/dist/esm/index.js new file mode 100644 index 00000000..4db7cfa8 --- /dev/null +++ b/dist/esm/index.js @@ -0,0 +1,10 @@ +"use client"; +export { ProseMirror } from "./components/ProseMirror.js"; +export { ProseMirrorDoc } from "./components/ProseMirrorDoc.js"; +export { useEditorEffect } from "./hooks/useEditorEffect.js"; +export { useEditorEventCallback } from "./hooks/useEditorEventCallback.js"; +export { useEditorEventListener } from "./hooks/useEditorEventListener.js"; +export { useEditorState } from "./hooks/useEditorState.js"; +export { useStopEvent } from "./hooks/useStopEvent.js"; +export { reactKeys } from "./plugins/reactKeys.js"; +export { widget } from "./decorations/ReactWidgetType.js"; diff --git a/dist/esm/plugins/__tests__/reactKeys.test.js b/dist/esm/plugins/__tests__/reactKeys.test.js new file mode 100644 index 00000000..158737f2 --- /dev/null +++ b/dist/esm/plugins/__tests__/reactKeys.test.js @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Schema } from "prosemirror-model"; +import { EditorState } from "prosemirror-state"; +import { reactKeys, reactKeysPluginKey } from "../reactKeys.js"; +const schema = new Schema({ + nodes: { + doc: { + content: "block+" + }, + paragraph: { + group: "block", + content: "inline*" + }, + list: { + group: "block", + content: "list_item+" + }, + list_item: { + content: "inline*" + }, + text: { + group: "inline" + } + } +}); +describe("reactNodeViewPlugin", ()=>{ + it("should create a unique key for each node", ()=>{ + const editorState = EditorState.create({ + doc: schema.topNodeType.create(null, [ + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create() + ]), + plugins: [ + reactKeys() + ] + }); + const pluginState = reactKeysPluginKey.getState(editorState); + expect(pluginState.posToKey.size).toBe(3); + }); + it("should maintain key stability when possible", ()=>{ + const initialEditorState = EditorState.create({ + doc: schema.topNodeType.create(null, [ + schema.nodes.paragraph.create({}, schema.text("Hello")), + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create() + ]), + plugins: [ + reactKeys() + ] + }); + const initialPluginState = reactKeysPluginKey.getState(initialEditorState); + const nextEditorState = initialEditorState.apply(initialEditorState.tr.insertText(", world!", 6)); + const nextPluginState = reactKeysPluginKey.getState(nextEditorState); + expect(Array.from(initialPluginState.keyToPos.keys())).toEqual(Array.from(nextPluginState.keyToPos.keys())); + }); + it("should create unique keys for new nodes", ()=>{ + const initialEditorState = EditorState.create({ + doc: schema.topNodeType.create(null, [ + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create() + ]), + plugins: [ + reactKeys() + ] + }); + const initialPluginState = reactKeysPluginKey.getState(initialEditorState); + const nextEditorState = initialEditorState.apply(initialEditorState.tr.insert(0, schema.nodes.list.createAndFill())); + const nextPluginState = reactKeysPluginKey.getState(nextEditorState); + // Adds new keys for new nodes + expect(nextPluginState.keyToPos.size).toBe(5); + // Maintains keys for previous nodes that are still there + Array.from(initialPluginState.keyToPos.keys()).forEach((key)=>{ + expect(Array.from(nextPluginState.keyToPos.keys())).toContain(key); + }); + }); +}); diff --git a/dist/esm/plugins/beforeInputPlugin.js b/dist/esm/plugins/beforeInputPlugin.js new file mode 100644 index 00000000..79020a4c --- /dev/null +++ b/dist/esm/plugins/beforeInputPlugin.js @@ -0,0 +1,133 @@ +import { Plugin } from "prosemirror-state"; +import { CursorWrapper } from "../components/CursorWrapper.js"; +import { widget } from "../decorations/ReactWidgetType.js"; +import { reactKeysPluginKey } from "./reactKeys.js"; +function insertText(view, eventData) { + let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}; + if (eventData === null) return false; + const from = options.from ?? view.state.selection.from; + const to = options.to ?? view.state.selection.to; + if (view.someProp("handleTextInput", (f)=>f(view, from, to, eventData))) { + return true; + } + const { tr } = view.state; + if (options.marks) tr.ensureMarks(options.marks); + tr.insertText(eventData, from, to); + if (options.bust) { + const $from = view.state.doc.resolve(from); + const sharedAncestorDepth = $from.sharedDepth(to); + const sharedAncestorPos = $from.start(sharedAncestorDepth); + const parentKey = reactKeysPluginKey.getState(view.state)?.posToKey.get(sharedAncestorPos - 1); + tr.setMeta(reactKeysPluginKey, { + type: "bustKey", + payload: { + key: parentKey + } + }); + } + view.dispatch(tr); + return true; +} +export function beforeInputPlugin(setCursorWrapper) { + let compositionText = null; + let compositionMarks = null; + return new Plugin({ + props: { + handleDOMEvents: { + compositionstart (view) { + const { state } = view; + view.dispatch(state.tr.deleteSelection()); + const $pos = state.selection.$from; + if (state.selection.empty && (state.storedMarks || !$pos.textOffset && $pos.parentOffset && $pos.nodeBefore?.marks.some((m)=>m.type.spec.inclusive === false))) { + setCursorWrapper(widget(state.selection.from, CursorWrapper, { + key: "cursor-wrapper", + marks: state.storedMarks ?? $pos.marks() + })); + } + compositionMarks = state.storedMarks ?? $pos.marks(); + // @ts-expect-error Internal property - input + view.input.composing = true; + return true; + }, + compositionupdate () { + return true; + }, + compositionend (view) { + // @ts-expect-error Internal property - input + view.input.composing = false; + if (compositionText === null) return; + insertText(view, compositionText, { + // TODO: Rather than busting the reactKey cache here, + // which is pretty blunt and doesn't work for + // multi-node replacements, we should attempt to + // snapshot the selected DOM during compositionstart + // and restore it before we end the composition. + // This should allow React to successfully clean up + // and insert the newly composed text, without requiring + // any remounts + bust: true, + marks: compositionMarks + }); + compositionText = null; + compositionMarks = null; + setCursorWrapper(null); + return true; + }, + beforeinput (view, event) { + event.preventDefault(); + switch(event.inputType){ + case "insertCompositionText": + { + if (event.data === null) break; + compositionText = event.data; + break; + } + case "insertReplacementText": + { + const ranges = event.getTargetRanges(); + event.dataTransfer?.items[0]?.getAsString((data)=>{ + for (const range of ranges){ + const from = view.posAtDOM(range.startContainer, range.startOffset, 1); + const to = view.posAtDOM(range.endContainer, range.endOffset, 1); + insertText(view, data, { + from, + to + }); + } + }); + break; + } + case "insertText": + { + insertText(view, event.data); + break; + } + case "deleteWordBackward": + case "deleteContentBackward": + case "deleteWordForward": + case "deleteContentForward": + case "deleteContent": + { + const targetRanges = event.getTargetRanges(); + const { tr } = view.state; + for (const range of targetRanges){ + const start = view.posAtDOM(range.startContainer, range.startOffset); + const end = view.posAtDOM(range.endContainer, range.endOffset); + const { doc } = view.state; + const storedMarks = doc.resolve(start).marksAcross(doc.resolve(end)); + tr.delete(start, end).setStoredMarks(storedMarks); + } + view.dispatch(tr); + break; + } + default: + { + break; + } + } + return true; + } + } + } + }); +} diff --git a/dist/esm/plugins/componentEventListeners.js b/dist/esm/plugins/componentEventListeners.js new file mode 100644 index 00000000..4b5f0792 --- /dev/null +++ b/dist/esm/plugins/componentEventListeners.js @@ -0,0 +1,25 @@ +import { Plugin, PluginKey } from "prosemirror-state"; +import { unstable_batchedUpdates as batch } from "react-dom"; +export function componentEventListeners(eventHandlerRegistry) { + const domEventHandlers = {}; + for (const [eventType, handlers] of eventHandlerRegistry.entries()){ + function handleEvent(view, event) { + for (const handler of handlers){ + let handled = false; + batch(()=>{ + handled = !!handler.call(this, view, event); + }); + if (handled || event.defaultPrevented) return true; + } + return false; + } + domEventHandlers[eventType] = handleEvent; + } + const plugin = new Plugin({ + key: new PluginKey("@nytimes/react-prosemirror/componentEventListeners"), + props: { + handleDOMEvents: domEventHandlers + } + }); + return plugin; +} diff --git a/dist/esm/plugins/componentEventListenersPlugin.js b/dist/esm/plugins/componentEventListenersPlugin.js new file mode 100644 index 00000000..ce80ffc7 --- /dev/null +++ b/dist/esm/plugins/componentEventListenersPlugin.js @@ -0,0 +1,25 @@ +import { Plugin, PluginKey } from "prosemirror-state"; +import { unstable_batchedUpdates as batch } from "react-dom"; +export function createComponentEventListenersPlugin(eventHandlerRegistry) { + const domEventHandlers = {}; + for (const [eventType, handlers] of eventHandlerRegistry.entries()){ + function handleEvent(view, event) { + for (const handler of handlers){ + let handled = false; + batch(()=>{ + handled = !!handler.call(this, view, event); + }); + if (handled || event.defaultPrevented) return true; + } + return false; + } + domEventHandlers[eventType] = handleEvent; + } + const plugin = new Plugin({ + key: new PluginKey("componentEventListeners"), + props: { + handleDOMEvents: domEventHandlers + } + }); + return plugin; +} diff --git a/dist/esm/plugins/reactKeys.js b/dist/esm/plugins/reactKeys.js new file mode 100644 index 00000000..783baad8 --- /dev/null +++ b/dist/esm/plugins/reactKeys.js @@ -0,0 +1,81 @@ +import { Plugin, PluginKey } from "prosemirror-state"; +export function createNodeKey() { + const key = Math.floor(Math.random() * 0xffffffffffff).toString(16); + return key; +} +export const reactKeysPluginKey = new PluginKey("@nytimes/react-prosemirror/reactKeys"); +/** + * Tracks a unique key for each (non-text) node in the + * document, identified by its current position. Keys are + * (mostly) stable across transaction applications. The + * key for a given node can be accessed by that node's + * current position in the document, and vice versa. + */ export function reactKeys() { + let composing = false; + return new Plugin({ + key: reactKeysPluginKey, + state: { + init (_, state) { + const next = { + posToKey: new Map(), + keyToPos: new Map() + }; + state.doc.descendants((_, pos)=>{ + const key = createNodeKey(); + next.posToKey.set(pos, key); + next.keyToPos.set(key, pos); + return true; + }); + return next; + }, + /** + * Keeps node keys stable across transactions. + * + * To accomplish this, we map each node position forwards + * through the transaction to identify its current position, + * and assign its key to that new position, dropping it if the + * node was deleted. + */ apply (tr, value, _, newState) { + if (!tr.docChanged || composing) return value; + const meta = tr.getMeta(reactKeysPluginKey); + const keyToBust = meta?.type === "bustKey" && meta.payload.key; + const next = { + posToKey: new Map(), + keyToPos: new Map() + }; + const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{ + let [a] = param, [b] = param1; + return a - b; + }); + for (const [pos, key] of posToKeyEntries){ + const { pos: newPos , deleted } = tr.mapping.mapResult(pos); + if (deleted) continue; + let newKey = key; + if (keyToBust === key) { + newKey = createNodeKey(); + } + next.posToKey.set(newPos, newKey); + next.keyToPos.set(newKey, newPos); + } + newState.doc.descendants((_, pos)=>{ + if (next.posToKey.has(pos)) return true; + const key = createNodeKey(); + next.posToKey.set(pos, key); + next.keyToPos.set(key, pos); + return true; + }); + return next; + } + }, + props: { + handleDOMEvents: { + compositionstart: ()=>{ + composing = true; + }, + compositionend: ()=>{ + composing = false; + } + } + } + }); +} diff --git a/dist/esm/props.js b/dist/esm/props.js new file mode 100644 index 00000000..a13fe897 --- /dev/null +++ b/dist/esm/props.js @@ -0,0 +1,251 @@ +import cx from "classnames"; +export function kebabCaseToCamelCase(str) { + return str.replaceAll(/-[a-z]/g, (g)=>g[1]?.toUpperCase() ?? ""); +} +/** + * Converts a CSS style string to an object + * that can be passed to a React component's + * `style` prop. + */ export function cssToStyles(css) { + const stylesheet = new CSSStyleSheet(); + stylesheet.insertRule(`* { ${css} }`); + const insertedRule = stylesheet.cssRules[0]; + const declaration = insertedRule.style; + const styles = {}; + for(let i = 0; i < declaration.length; i++){ + const property = declaration.item(i); + const value = declaration.getPropertyValue(property); + const camelCasePropertyName = property.startsWith("--") ? property : kebabCaseToCamelCase(property); + styles[camelCasePropertyName] = value; + } + return styles; +} +/** + * Merges two sets of React props. Class names + * will be concatenated and style objects + * will be merged. + */ export function mergeReactProps(a, b) { + return { + ...a, + ...b, + className: cx(a.className, b.className), + style: { + ...a.style, + ...b.style + } + }; +} +/** + * Given a record of HTML attributes, returns tho + * equivalent React props. + */ export function htmlAttrsToReactProps(attrs) { + const props = {}; + for (const [attrName, attrValue] of Object.entries(attrs)){ + switch(attrName.toLowerCase()){ + case "class": + { + props.className = attrValue; + break; + } + case "style": + { + props.style = cssToStyles(attrValue); + break; + } + case "autocapitalize": + { + props.autoCapitalize = attrValue; + break; + } + case "contenteditable": + { + if (attrValue === "" || attrValue === "true") { + props.contentEditable = true; + break; + } + if (attrValue === "false") { + props.contentEditable = false; + break; + } + if (attrValue === "plaintext-only") { + props.contentEditable = "plaintext-only"; + break; + } + props.contentEditable = null; + break; + } + case "draggable": + { + props.draggable = attrValue != null; + break; + } + case "enterkeyhint": + { + props.enterKeyHint = attrValue; + break; + } + case "for": + { + props.htmlFor = attrValue; + break; + } + case "hidden": + { + props.hidden = attrValue; + break; + } + case "inputmode": + { + props.inputMode = attrValue; + break; + } + case "itemprop": + { + props.itemProp = attrValue; + break; + } + case "spellcheck": + { + if (attrValue === "" || attrValue === "true") { + props.spellCheck = true; + break; + } + if (attrValue === "false") { + props.spellCheck = false; + break; + } + props.spellCheck = null; + break; + } + case "tabindex": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.tabIndex = numValue; + } + break; + } + case "autocomplete": + { + props.autoComplete = attrValue; + break; + } + case "autofocus": + { + props.autoFocus = attrValue != null; + break; + } + case "formaction": + { + props.formAction = attrValue; + break; + } + case "formenctype": + { + props.formEnctype = attrValue; + break; + } + case "formmethod": + { + props.formMethod = attrValue; + break; + } + case "formnovalidate": + { + props.formNoValidate = attrValue; + break; + } + case "formtarget": + { + props.formTarget = attrValue; + break; + } + case "maxlength": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.maxLength = attrValue; + } + break; + } + case "minlength": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.minLength = attrValue; + } + break; + } + case "max": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.max = attrValue; + } + break; + } + case "min": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.min = attrValue; + } + break; + } + case "multiple": + { + props.multiple = attrValue != null; + break; + } + case "readonly": + { + props.readOnly = attrValue != null; + break; + } + case "required": + { + props.required = attrValue != null; + break; + } + case "size": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.size = attrValue; + } + break; + } + case "step": + { + if (attrValue === "any") { + props.step = attrValue; + break; + } + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue) && numValue > 0) { + props.step = attrValue; + } + break; + } + case "disabled": + { + props.disabled = attrValue != null; + break; + } + case "rows": + { + const numValue = parseInt(attrValue, 10); + if (!Number.isNaN(numValue)) { + props.rows = attrValue; + } + break; + } + default: + { + props[attrName] = attrValue; + break; + } + } + } + return props; +} diff --git a/dist/esm/selection/SelectionDOMObserver.js b/dist/esm/selection/SelectionDOMObserver.js new file mode 100644 index 00000000..0ee9455a --- /dev/null +++ b/dist/esm/selection/SelectionDOMObserver.js @@ -0,0 +1,162 @@ +import { Selection } from "prosemirror-state"; +import { browser } from "../browser.js"; +import { parentNode, selectionCollapsed } from "../dom.js"; +import { hasFocusAndSelection } from "./hasFocusAndSelection.js"; +import { selectionFromDOM } from "./selectionFromDOM.js"; +import { isEquivalentPosition, selectionToDOM } from "./selectionToDOM.js"; +let SelectionState = class SelectionState { + set(sel) { + this.anchorNode = sel.anchorNode; + this.anchorOffset = sel.anchorOffset; + this.focusNode = sel.focusNode; + this.focusOffset = sel.focusOffset; + } + clear() { + this.anchorNode = this.focusNode = null; + } + eq(sel) { + return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset && sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset; + } + constructor(){ + this.anchorNode = null; + this.anchorOffset = 0; + this.focusNode = null; + this.focusOffset = 0; + } +}; +export class SelectionDOMObserver { + connectSelection() { + this.view.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange); + } + disconnectSelection() { + this.view.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange); + } + stop() { + this.disconnectSelection(); + } + start() { + this.connectSelection(); + } + suppressSelectionUpdates() { + this.suppressingSelectionUpdates = true; + setTimeout(()=>this.suppressingSelectionUpdates = false, 50); + } + setCurSelection() { + // @ts-expect-error Internal method + this.currentSelection.set(this.view.domSelectionRange()); + } + ignoreSelectionChange(sel) { + if (!sel.focusNode) return true; + const ancestors = new Set(); + let container; + for(let scan = sel.focusNode; scan; scan = parentNode(scan))ancestors.add(scan); + for(let scan = sel.anchorNode; scan; scan = parentNode(scan))if (ancestors.has(scan)) { + container = scan; + break; + } + // @ts-expect-error Internal property (docView) + const desc = container && this.view.docView.nearestDesc(container); + if (desc && desc.ignoreMutation({ + type: "selection", + target: container?.nodeType == 3 ? container?.parentNode : container + })) { + this.setCurSelection(); + return true; + } + return; + } + registerMutation() { + // pass + } + flushSoon() { + if (this.flushingSoon < 0) this.flushingSoon = window.setTimeout(()=>{ + this.flushingSoon = -1; + this.flush(); + }, 20); + } + updateSelection() { + const { view } = this; + const compositionID = // @ts-expect-error Internal property (input) + view.input.compositionPendingChanges || // @ts-expect-error Internal property (input) + (view.composing ? view.input.compositionID : 0); + // @ts-expect-error Internal property (input) + view.input.compositionPendingChanges = 0; + const origin = // @ts-expect-error Internal property (input) + view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null; + const newSel = selectionFromDOM(view, origin); + if (newSel && !view.state.selection.eq(newSel)) { + const tr = view.state.tr.setSelection(newSel); + if (origin == "pointer") tr.setMeta("pointer", true); + else if (origin == "key") tr.scrollIntoView(); + if (compositionID) tr.setMeta("composition", compositionID); + view.dispatch(tr); + } + } + selectionToDOM() { + const { view } = this; + selectionToDOM(view); + // @ts-expect-error Internal property (domSelectionRange) + const sel = view.domSelectionRange(); + this.currentSelection.set(sel); + } + flush() { + const { view } = this; + // @ts-expect-error Internal property (docView) + if (!view.docView || this.flushingSoon > -1) return; + // @ts-expect-error Internal property (domSelectionRange) + const sel = view.domSelectionRange(); + const newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel); + let readSel = null; + // If it looks like the browser has reset the selection to the + // start of the document after focus, restore the selection from + // the state + if (newSel && // @ts-expect-error Internal property (input) + view.input.lastFocus > Date.now() - 200 && // @ts-expect-error Internal property (input) + Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 && selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) && readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) { + // @ts-expect-error Internal property (input) + view.input.lastFocus = 0; + selectionToDOM(view); + this.currentSelection.set(sel); + // @ts-expect-error Internal property (scrollToSelection) + view.scrollToSelection(); + } else if (newSel) { + this.updateSelection(); + if (!this.currentSelection.eq(sel)) selectionToDOM(view); + this.currentSelection.set(sel); + } + } + selectionChanged(sel) { + return !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel); + } + forceFlush() { + if (this.flushingSoon > -1) { + window.clearTimeout(this.flushingSoon); + this.flushingSoon = -1; + this.flush(); + } + } + onSelectionChange() { + if (!hasFocusAndSelection(this.view)) return; + if (this.view.composing) 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) { + // @ts-expect-error Internal method + const sel = this.view.domSelectionRange(); + // Selection.isCollapsed isn't reliable on IE + if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sel.anchorNode, sel.anchorOffset)) return this.flushSoon(); + } + this.flush(); + } + constructor(view){ + this.view = view; + this.flushingSoon = -1; + this.currentSelection = new SelectionState(); + this.suppressingSelectionUpdates = false; + this.view = view; + this.onSelectionChange = this.onSelectionChange.bind(this); + } +} diff --git a/dist/esm/selection/hasFocusAndSelection.js b/dist/esm/selection/hasFocusAndSelection.js new file mode 100644 index 00000000..b294c9a3 --- /dev/null +++ b/dist/esm/selection/hasFocusAndSelection.js @@ -0,0 +1,17 @@ +export function hasFocusAndSelection(view) { + if (view.editable && !view.hasFocus()) return false; + return hasSelection(view); +} +export function hasSelection(view) { + // @ts-expect-error Internal method + const sel = view.domSelectionRange(); + 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; + } +} diff --git a/dist/esm/selection/selectionFromDOM.js b/dist/esm/selection/selectionFromDOM.js new file mode 100644 index 00000000..15899f81 --- /dev/null +++ b/dist/esm/selection/selectionFromDOM.js @@ -0,0 +1,59 @@ +import { NodeSelection, TextSelection } from "prosemirror-state"; +import { isOnEdge, selectionCollapsed } from "../dom.js"; +export function selectionBetween(view, $anchor, $head, bias) { + return view.someProp("createSelectionBetween", (f)=>f(view, $anchor, $head)) || TextSelection.between($anchor, $head, bias); +} +export function selectionFromDOM(view) { + let origin = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : null; + // @ts-expect-error Internal method + const domSel = view.domSelectionRange(), doc = view.state.doc; + if (!domSel.focusNode) return null; + // @ts-expect-error Internal method + let nearestDesc = view.docView.nearestDesc(domSel.focusNode); + const inWidget = nearestDesc && nearestDesc.size == 0; + // @ts-expect-error Internal method + let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1); + if (head < 0) return null; + let $head = doc.resolve(head), anchor, selection; + if (selectionCollapsed(domSel)) { + anchor = head; + while(nearestDesc && !nearestDesc.node)nearestDesc = nearestDesc.parent; + const nearestDescNode = nearestDesc.node; + if (nearestDesc && nearestDescNode.isAtom && NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent && !(nearestDescNode.isInline && isOnEdge(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) { + const pos = nearestDesc.posBefore; + selection = new NodeSelection(head == pos ? $head : doc.resolve(pos)); + } + } else { + if (// eslint-disable-next-line @typescript-eslint/no-non-null-assertion + domSel instanceof view.dom.ownerDocument.defaultView.Selection && domSel.rangeCount > 1) { + let min = head, max = head; + for(let i = 0; i < domSel.rangeCount; i++){ + const range = domSel.getRangeAt(i); + min = Math.min(min, // @ts-expect-error Internal method + view.docView.posFromDOM(range.startContainer, range.startOffset, 1)); + max = Math.max(max, // @ts-expect-error Internal method + view.docView.posFromDOM(range.endContainer, range.endOffset, -1)); + } + if (min < 0) return null; + [anchor, head] = max == view.state.selection.anchor ? [ + max, + min + ] : [ + min, + max + ]; + $head = doc.resolve(head); + } else { + // @ts-expect-error Internal method + anchor = view.docView.posFromDOM(// eslint-disable-next-line @typescript-eslint/no-non-null-assertion + domSel.anchorNode, domSel.anchorOffset, 1); + } + if (anchor < 0) return null; + } + const $anchor = doc.resolve(anchor); + if (!selection) { + const bias = origin == "pointer" || view.state.selection.head < $head.pos && !inWidget ? 1 : -1; + selection = selectionBetween(view, $anchor, $head, bias); + } + return selection; +} diff --git a/dist/esm/selection/selectionToDOM.js b/dist/esm/selection/selectionToDOM.js new file mode 100644 index 00000000..bf6630c9 --- /dev/null +++ b/dist/esm/selection/selectionToDOM.js @@ -0,0 +1,196 @@ +import { NodeSelection, TextSelection } from "prosemirror-state"; +import { browser } from "../browser.js"; +// 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)); +}; +export 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); +} +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))) { + const 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) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + node = node.childNodes[off + (dir < 0 ? -1 : 0)]; + if (node.contentEditable == "false") return false; + off = dir < 0 ? nodeSize(node) : 0; + } else { + return false; + } + } +} +export const domIndex = function(node) { + let n = node; + for(let index = 0;; index++){ + n = n.previousSibling; + if (!n) return index; + } +}; +export function nodeSize(node) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length; +} +export function syncNodeSelection(view, sel) { + const v = view; + if (sel instanceof NodeSelection) { + const desc = v.docView.descAt(sel.from); + if (desc != v.lastSelectedViewDesc) { + clearNodeSelection(v); + if (desc) desc.selectNode(); + v.lastSelectedViewDesc = desc; + } + } else { + clearNodeSelection(v); + } +} +// Clear all DOM statefulness of the last node selection. +function clearNodeSelection(view) { + const v = view; + if (v.lastSelectedViewDesc) { + if (v.lastSelectedViewDesc.parent) v.lastSelectedViewDesc.deselectNode(); + v.lastSelectedViewDesc = undefined; + } +} +export function hasSelection(view) { + const v = view; + const sel = v.domSelectionRange(); + 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 v.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) && (v.editable || v.dom.contains(// eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sel.focusNode.nodeType == 3 ? sel.focusNode.parentNode : sel.focusNode)); + } catch (_) { + return false; + } +} +function editorOwnsSelection(view) { + return view.editable ? view.hasFocus() : hasSelection(view) && document.activeElement && document.activeElement.contains(view.dom); +} +function selectCursorWrapper(view) { + const v = view; + const domSel = v.domSelection(), range = document.createRange(); + if (!domSel) return; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const node = v.cursorWrapper.dom, img = node.nodeName == "IMG"; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (img) range.setStart(node.parentNode, domIndex(node) + 1); + else range.setStart(node, 0); + range.collapse(true); + 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 && !v.state.selection.visible && browser.ie && browser.ie_version <= 11) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + node.disabled = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + node.disabled = false; + } +} +function temporarilyEditableNear(view, pos) { + const v = view; + const { node , offset } = v.docView.domFromPos(pos, 0); + const after = offset < node.childNodes.length ? node.childNodes[offset] : null; + const 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); + } + return; +} +function setEditable(element) { + element.contentEditable = "true"; + if (browser.safari && element.draggable) { + element.draggable = false; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element.wasDraggable = true; + } + return element; +} +function resetEditable(element) { + element.contentEditable = "false"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (element.wasDraggable) { + element.draggable = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element.wasDraggable = null; + } +} +function removeClassOnSelectionChange(view) { + const v = view; + const doc = v.dom.ownerDocument; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + doc.removeEventListener("selectionchange", v.input.hideSelectionGuard); + const domSel = v.domSelectionRange(); + const node = domSel.anchorNode, offset = domSel.anchorOffset; + doc.addEventListener("selectionchange", v.input.hideSelectionGuard = ()=>{ + if (domSel.anchorNode != node || domSel.anchorOffset != offset) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + doc.removeEventListener("selectionchange", v.input.hideSelectionGuard); + setTimeout(()=>{ + if (!editorOwnsSelection(v) || v.state.selection.visible) v.dom.classList.remove("ProseMirror-hideselection"); + }, 20); + } + }); +} +const brokenSelectBetweenUneditable = browser.safari || browser.chrome && browser.chrome_version < 63; +export function selectionToDOM(view) { + let force = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + const v = view; + const sel = v.state.selection; + syncNodeSelection(v, sel); + if (!editorOwnsSelection(v)) 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 && v.input.mouseDown && v.input.mouseDown.allowDefault && browser.chrome) { + const domSel = v.domSelectionRange(), curSel = v.domObserver.currentSelection; + if (domSel.anchorNode && curSel.anchorNode && isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset)) { + v.input.mouseDown.delayedSelectionSync = true; + v.domObserver.setCurSelection(); + return; + } + } + v.domObserver.disconnectSelection(); + if (v.cursorWrapper) { + selectCursorWrapper(v); + } else { + const { anchor , head } = sel; + let resetEditableFrom; + let resetEditableTo; + if (brokenSelectBetweenUneditable && !(sel instanceof TextSelection)) { + if (!sel.$from.parent.inlineContent) resetEditableFrom = temporarilyEditableNear(v, sel.from); + if (!sel.empty && !sel.$from.parent.inlineContent) resetEditableTo = temporarilyEditableNear(v, sel.to); + } + v.docView.setSelection(anchor, head, v.root, force); + if (brokenSelectBetweenUneditable) { + if (resetEditableFrom) resetEditable(resetEditableFrom); + if (resetEditableTo) resetEditable(resetEditableTo); + } + if (sel.visible) { + v.dom.classList.remove("ProseMirror-hideselection"); + } else { + v.dom.classList.add("ProseMirror-hideselection"); + if ("onselectionchange" in document) removeClassOnSelectionChange(v); + } + } + v.domObserver.setCurSelection(); + v.domObserver.connectSelection(); +} diff --git a/dist/esm/testing/editorViewTestHelpers.js b/dist/esm/testing/editorViewTestHelpers.js new file mode 100644 index 00000000..161ac9cb --- /dev/null +++ b/dist/esm/testing/editorViewTestHelpers.js @@ -0,0 +1,98 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import { render } from "@testing-library/react"; +import { expect } from "expect"; +import { Node } from "prosemirror-model"; +import { EditorState, TextSelection } from "prosemirror-state"; +import { doc, eq, p, schema } from "prosemirror-test-builder"; +import React from "react"; +import { ProseMirror } from "../components/ProseMirror.js"; +import { ProseMirrorDoc } from "../components/ProseMirrorDoc.js"; +import { useEditorEffect } from "../hooks/useEditorEffect.js"; +import { reactKeys } from "../plugins/reactKeys.js"; +const toEqualNode = function(actual, expected) { + if (!(actual instanceof Node && expected instanceof Node)) { + throw new Error("Must be comparing nodes"); + } + const pass = eq(actual, expected); + return { + message: ()=>// `this` context will have correct typings + `expected ${this.utils.printReceived(actual)} ${pass ? "not " : ""}to equal ${this.utils.printExpected(expected)}`, + pass + }; +}; +expect.extend({ + toEqualNode +}); +export function tempEditor(param) { + let { doc: startDoc , selection , controlled , plugins , ...props } = param; + startDoc = startDoc ?? doc(p()); + const state = EditorState.create({ + doc: startDoc, + schema, + selection: selection ?? startDoc.tag?.a ? TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined, + plugins: [ + ...plugins ?? [], + reactKeys() + ] + }); + let view = null; + function Test() { + useEditorEffect((v)=>{ + view = v; + }); + return null; + } + const { rerender , unmount } = render(/*#__PURE__*/ React.createElement(ProseMirror, _extends({}, controlled ? { + state + } : { + defaultState: state + }, props), /*#__PURE__*/ React.createElement(Test, null), /*#__PURE__*/ React.createElement(ProseMirrorDoc, null))); + function rerenderEditor() { + let { ...newProps } = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; + rerender(/*#__PURE__*/ React.createElement(ProseMirror, _extends({}, controlled ? { + state + } : { + defaultState: state + }, { + ...props, + ...newProps + }), /*#__PURE__*/ React.createElement(Test, null), /*#__PURE__*/ React.createElement(ProseMirrorDoc, null))); + return view; + } + // We need two renders for the hasContentDOM state to settle + rerenderEditor(); + return { + view: view, + rerender: rerenderEditor, + unmount + }; +} +function findTextNodeInner(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){ + const found = findTextNodeInner(ch, text); + if (found) return found; + } + } + return undefined; +} +export function findTextNode(node, text) { + const found = findTextNodeInner(node, text); + if (found) return found; + throw new Error("Unable to find matching text node"); +} diff --git a/dist/esm/testing/setupProseMirrorView.js b/dist/esm/testing/setupProseMirrorView.js new file mode 100644 index 00000000..8bf8e629 --- /dev/null +++ b/dist/esm/testing/setupProseMirrorView.js @@ -0,0 +1,76 @@ +let oldElementFromPoint; +let oldGetClientRects; +let oldGetBoundingClientRect; +const mockElementFromPoint = ()=>globalThis.document.body; +const mockGetBoundingClientRect = ()=>{ + return { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0, + toJSON () { + return { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0 + }; + } + }; +}; +const mockGetClientRects = ()=>{ + const list = [ + { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0, + toJSON () { + return { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0 + }; + } + } + ]; + const domRectList = Object.assign(list, { + item (index) { + return list[index] ?? null; + } + }); + return domRectList; +}; +export function setupProseMirrorView() { + oldElementFromPoint = Document.prototype.elementFromPoint; + Document.prototype.elementFromPoint = mockElementFromPoint; + oldGetClientRects = Range.prototype.getClientRects; + Range.prototype.getClientRects = mockGetClientRects; + oldGetBoundingClientRect = Range.prototype.getBoundingClientRect; + Range.prototype.getBoundingClientRect = mockGetBoundingClientRect; +} +export function teardownProseMirrorView() { + // @ts-expect-error jsdom actually doesn't implement these, so they might be undefined + Document.prototype.elementFromPoint = oldElementFromPoint; + // @ts-expect-error jsdom actually doesn't implement these, so they might be undefined + Range.prototype.getClientRects = oldGetClientRects; + // @ts-expect-error jsdom actually doesn't implement these, so they might be undefined + Range.prototype.getBoundingClientRect = oldGetBoundingClientRect; +} diff --git a/dist/esm/viewdesc.js b/dist/esm/viewdesc.js new file mode 100644 index 00000000..2930cfaa --- /dev/null +++ b/dist/esm/viewdesc.js @@ -0,0 +1,648 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Fragment } from "prosemirror-model"; +import { browser } from "./browser.js"; +import { domIndex, isEquivalentPosition } from "./selection/selectionToDOM.js"; +// 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. +export class ViewDesc { + // Used to check whether a given description corresponds to a + // widget/mark/node. + matchesWidget(_widget) { + return false; + } + matchesMark(_mark) { + return false; + } + matchesNode(_node, _outerDeco, _innerDeco) { + return false; + } + // @ts-expect-error ... + matchesHack(nodeName) { + return false; + } + // 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; + } + // Used by the editor's event handler to ignore events that come + // from certain descs. + // @ts-expect-error ... + stopEvent(event) { + 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++)// @ts-expect-error ... + 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() { + // pass + } + posBeforeChild(child) { + for(let i = 0, pos = this.posAtStart;; i++){ + const cur = this.children[i]; + if (cur == child) return pos; + // @ts-expect-error ... + 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; + } + 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.previousSibling) break; + } + if (atEnd == null && offset == dom.childNodes.length) for(let search = dom;; search = search.parentNode){ + if (search == this.dom) { + atEnd = true; + break; + } + if (search.nextSibling) break; + } + } + return (atEnd == null ? bias > 0 : atEnd) ? this.posAtEnd : this.posAtStart; + } + nearestDesc(dom) { + let onlyNodes = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + for(let first = true, cur = dom; cur; cur = cur.parentNode){ + const desc = this.getDesc(cur); + let nodeDOM; + if (desc && (!onlyNodes || desc.node)) { + // If dom is outside of this desc's nodeDOM, don't count it. + if (first && (nodeDOM = desc.nodeDOM) && !(nodeDOM.nodeType == 1 ? nodeDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode) : nodeDOM == dom)) first = false; + else return desc; + } + } + return; + } + getDesc(dom) { + const desc = dom.pmViewDesc; + for(let cur = desc; cur; cur = cur.parent)if (cur == this) return desc; + return; + } + posFromDOM(dom, offset, bias) { + for(let scan = dom; scan; scan = scan.parentNode){ + const desc = this.getDesc(scan); + if (desc) return desc.localPosFromDOM(dom, offset, bias); + } + return -1; + } + // 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]; + const 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; + } + return; + } + domFromPos(pos, side) { + if (!this.contentDOM) return { + node: this.dom, + offset: 0, + atom: pos + 1 + }; + // First find the position in the child array + let i = 0, offset = 0; + for(let curPos = 0; i < this.children.length; i++){ + const 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.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) { + let base = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : 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++){ + const child = this.children[i], end = offset + child.size; + if (fromOffset == -1 && from <= end) { + const 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--){ + const 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++){ + const 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; + const child = this.children[side < 0 ? 0 : this.children.length - 1]; + // @ts-expect-error ... + return child.size == 0 || child.emptyChildAt(side); + } + domAfterPos(pos) { + const { node , offset } = this.domFromPos(pos, 0); + if (node.nodeType != 1 || offset == node.childNodes.length) throw new RangeError("No node after pos " + pos); + // @ts-expect-error ... + return node.childNodes[offset]; + } + // 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) { + let force = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : false; + // If the selection falls entirely in a child, give it to that child + const from = Math.min(anchor, head), to = Math.max(anchor, head); + for(let i = 0, offset = 0; i < this.children.length; i++){ + const 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); + const 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) { + const { 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; + } + const desc = scan.pmViewDesc; + if (desc && desc.node && desc.node.isBlock) break; + } + } + } else { + const prev = node.childNodes[offset - 1]; + // @ts-expect-error ... + 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) { + const 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 (_) { + // 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. + // Similarly, this could crash on Safari if the editor is hidden, and + // there was no selection. + } + } + if (!domSelExtended) { + if (anchor > head) { + const tmp = anchorDOM; + anchorDOM = headDOM; + headDOM = tmp; + } + const range = document.createRange(); + range.setEnd(headDOM.node, headDOM.offset); + range.setStart(anchorDOM.node, anchorDOM.offset); + domSel.removeAllRanges(); + domSel.addRange(range); + } + } + 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++){ + const child = this.children[i], end = offset + child.size; + if (offset == end ? from <= end && to >= offset : from < end && to > offset) { + const 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++){ + const dirty = level == 1 ? CONTENT_DIRTY : CHILD_DIRTY; + if (node.dirty < dirty) node.dirty = dirty; + } + } + get domAtom() { + return false; + } + get ignoreForCoords() { + return false; + } + constructor(parent, children, pos, dom, contentDOM){ + this.parent = parent; + this.children = children; + this.pos = pos; + this.dom = dom; + this.contentDOM = contentDOM; + this.dirty = NOT_DIRTY; + // An expando property on the DOM node provides a link back to its + // description. + // @ts-expect-error We're using custom view implementations here but + // we match the API so this is relatively safe. + dom.pmViewDesc = this; + } +} +// A widget desc represents a widget decoration, which is a DOM node +// drawn between the document nodes. +export class WidgetViewDesc extends ViewDesc { + matchesWidget(widget) { + return this.dirty == NOT_DIRTY && widget.type.eq(this.widget.type); + } + parseRule() { + return { + ignore: true + }; + } + stopEvent(event) { + const stop = this.widget.spec.stopEvent; + return stop ? stop(event) : false; + } + ignoreMutation(mutation) { + return mutation.type != "selection" || this.widget.spec.ignoreSelection; + } + get domAtom() { + return true; + } + get side() { + return this.widget.type.side; + } + constructor(parent, pos, widget, dom){ + super(parent, [], pos, dom, null); + this.widget = widget; + this.widget = widget; + } +} +export class CompositionViewDesc extends ViewDesc { + 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; + } + constructor(parent, pos, dom, textDOM, text){ + super(parent, [], pos, dom, null); + this.textDOM = textDOM; + this.text = text; + } +} +// 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. +export class MarkViewDesc extends ViewDesc { + 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; + } + } + constructor(parent, children, pos, mark, dom, contentDOM){ + super(parent, children, pos, dom, contentDOM); + this.mark = mark; + } +} +// 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. +export class NodeViewDesc extends ViewDesc { + updateOuterDeco() { + // pass + } + 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 + const 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--){ + const child = this.children[i]; + // @ts-expect-error ... + if (this.dom.contains(child.dom.parentNode)) { + // @ts-expect-error ... + 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; + } + // If this desc must be updated to match the given node decoration, + // do so and return true. + update(_node, _outerDeco, _innerDeco, _view) { + return true; + } + // Mark this node as being the selected node. + selectNode() { + if (this.nodeDOM.nodeType == 1) 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() { + if (this.nodeDOM.nodeType == 1) { + this.nodeDOM.classList.remove("ProseMirror-selectednode"); + if (this.contentDOM || !this.node.type.spec.draggable) this.dom.removeAttribute("draggable"); + } + } + get domAtom() { + return this.node.isAtom; + } + constructor(parent, children, pos, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, stopEvent){ + super(parent, children, pos, dom, contentDOM); + this.node = node; + this.outerDeco = outerDeco; + this.innerDeco = innerDeco; + this.nodeDOM = nodeDOM; + this.stopEvent = stopEvent; + } +} +export class TextViewDesc extends NodeViewDesc { + parseRule() { + let skip = this.nodeDOM.parentNode; + while(skip && skip != this.dom && !skip.pmIsDeco)skip = skip.parentNode; + return { + skip: skip || true + }; + } + update(_node, _outerDeco, _innerDeco, _view) { + return true; + } + inParent() { + const 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"; + } + 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; + } + constructor(parent, children, pos, node, outerDeco, innerDeco, dom, nodeDOM){ + super(parent, children, pos, node, outerDeco, innerDeco, dom, null, nodeDOM, ()=>false); + } +} +// A dummy desc used to tag trailing BR or IMG nodes created to work +// around contentEditable terribleness. +export 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"; + } +} +function sameOuterDeco(a, b) { + if (a.length != b.length) return false; + // @ts-expect-error ... + for(let i = 0; i < a.length; i++)if (!a[i].type.eq(b[i].type)) return false; + return true; +} diff --git a/dist/tsconfig.tsbuildinfo b/dist/tsconfig.tsbuildinfo new file mode 100644 index 00000000..187709b3 --- /dev/null +++ b/dist/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"program":{"fileNames":["../node_modules/typescript/lib/lib.es5.d.ts","../node_modules/typescript/lib/lib.es2015.d.ts","../node_modules/typescript/lib/lib.es2016.d.ts","../node_modules/typescript/lib/lib.es2017.d.ts","../node_modules/typescript/lib/lib.es2018.d.ts","../node_modules/typescript/lib/lib.es2019.d.ts","../node_modules/typescript/lib/lib.es2020.d.ts","../node_modules/typescript/lib/lib.es2021.d.ts","../node_modules/typescript/lib/lib.es2022.d.ts","../node_modules/typescript/lib/lib.dom.d.ts","../node_modules/typescript/lib/lib.es2015.core.d.ts","../node_modules/typescript/lib/lib.es2015.collection.d.ts","../node_modules/typescript/lib/lib.es2015.generator.d.ts","../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../node_modules/typescript/lib/lib.es2015.promise.d.ts","../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../node_modules/typescript/lib/lib.es2017.object.d.ts","../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2017.string.d.ts","../node_modules/typescript/lib/lib.es2017.intl.d.ts","../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../node_modules/typescript/lib/lib.es2018.intl.d.ts","../node_modules/typescript/lib/lib.es2018.promise.d.ts","../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../node_modules/typescript/lib/lib.es2019.array.d.ts","../node_modules/typescript/lib/lib.es2019.object.d.ts","../node_modules/typescript/lib/lib.es2019.string.d.ts","../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../node_modules/typescript/lib/lib.es2019.intl.d.ts","../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../node_modules/typescript/lib/lib.es2020.date.d.ts","../node_modules/typescript/lib/lib.es2020.promise.d.ts","../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2020.string.d.ts","../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2020.intl.d.ts","../node_modules/typescript/lib/lib.es2020.number.d.ts","../node_modules/typescript/lib/lib.es2021.promise.d.ts","../node_modules/typescript/lib/lib.es2021.string.d.ts","../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../node_modules/typescript/lib/lib.es2021.intl.d.ts","../node_modules/typescript/lib/lib.es2022.array.d.ts","../node_modules/typescript/lib/lib.es2022.error.d.ts","../node_modules/typescript/lib/lib.es2022.intl.d.ts","../node_modules/typescript/lib/lib.es2022.object.d.ts","../node_modules/typescript/lib/lib.es2022.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2022.string.d.ts","../node_modules/typescript/lib/lib.esnext.intl.d.ts","../src/browser.ts","../src/dom.ts","../node_modules/orderedmap/dist/index.d.ts","../node_modules/prosemirror-model/dist/index.d.ts","../node_modules/prosemirror-state/node_modules/prosemirror-transform/dist/index.d.ts","../node_modules/prosemirror-state/dist/index.d.ts","../node_modules/prosemirror-view/node_modules/prosemirror-transform/dist/index.d.ts","../node_modules/prosemirror-view/dist/index.d.ts","../node_modules/@types/react/global.d.ts","../node_modules/csstype/index.d.ts","../node_modules/@types/prop-types/index.d.ts","../node_modules/@types/scheduler/tracing.d.ts","../node_modules/@types/react/index.d.ts","../node_modules/@types/react-dom/node_modules/@types/react/global.d.ts","../node_modules/@types/react-dom/node_modules/@types/react/index.d.ts","../node_modules/@types/react-dom/index.d.ts","../src/plugins/componentEventListeners.ts","../src/contexts/EditorContext.ts","../src/contexts/EditorStateContext.ts","../src/components/NodeViewComponentProps.tsx","../src/contexts/NodeViewContext.tsx","../src/decorations/computeDocDeco.ts","../node_modules/prosemirror-transform/dist/index.d.ts","../src/decorations/internalTypes.ts","../src/decorations/viewDecorations.tsx","../src/contexts/LayoutGroupContext.tsx","../src/hooks/useLayoutGroupEffect.ts","../src/hooks/useEditorEffect.ts","../src/decorations/ReactWidgetType.ts","../src/components/WidgetViewComponentProps.ts","../src/components/CursorWrapper.tsx","../src/plugins/reactKeys.ts","../src/plugins/beforeInputPlugin.ts","../src/selection/hasFocusAndSelection.ts","../src/selection/selectionToDOM.ts","../src/viewdesc.ts","../src/selection/selectionFromDOM.ts","../src/selection/SelectionDOMObserver.ts","../src/hooks/useComponentEventListeners.tsx","../src/hooks/useForceUpdate.ts","../src/hooks/useEditor.ts","../src/components/LayoutGroup.tsx","../src/contexts/ChildDescriptorsContext.ts","../src/hooks/useNodeViewDescriptor.ts","../src/decorations/iterDeco.ts","../src/hooks/useReactKeys.ts","../node_modules/classnames/index.d.ts","../src/props.ts","../src/components/OutputSpec.tsx","../src/components/MarkView.tsx","../src/components/NativeWidgetView.tsx","../src/contexts/StopEventContext.ts","../src/components/NodeView.tsx","../src/components/SeparatorHackView.tsx","../src/components/TextNodeView.tsx","../src/components/TrailingHackView.tsx","../src/components/WidgetView.tsx","../src/components/ChildNodeViews.tsx","../src/components/DocNodeView.tsx","../src/components/ProseMirrorDoc.tsx","../src/components/ProseMirror.tsx","../src/hooks/useEditorEventCallback.ts","../src/hooks/useEditorEventListener.ts","../src/hooks/useEditorState.ts","../src/hooks/useStopEvent.ts","../src/index.ts","../node_modules/prosemirror-test-builder/dist/index.d.ts","../node_modules/@types/react-dom/client.d.ts","../node_modules/@types/aria-query/index.d.ts","../node_modules/@testing-library/dom/types/matches.d.ts","../node_modules/@testing-library/dom/types/wait-for.d.ts","../node_modules/@testing-library/dom/types/query-helpers.d.ts","../node_modules/@testing-library/dom/types/queries.d.ts","../node_modules/@testing-library/dom/types/get-queries-for-element.d.ts","../node_modules/@testing-library/dom/node_modules/pretty-format/build/types.d.ts","../node_modules/@testing-library/dom/node_modules/pretty-format/build/index.d.ts","../node_modules/@testing-library/dom/types/screen.d.ts","../node_modules/@testing-library/dom/types/wait-for-element-to-be-removed.d.ts","../node_modules/@testing-library/dom/types/get-node-text.d.ts","../node_modules/@testing-library/dom/types/events.d.ts","../node_modules/@testing-library/dom/types/pretty-dom.d.ts","../node_modules/@testing-library/dom/types/role-helpers.d.ts","../node_modules/@testing-library/dom/types/config.d.ts","../node_modules/@testing-library/dom/types/suggestions.d.ts","../node_modules/@testing-library/dom/types/index.d.ts","../node_modules/@types/react-dom/test-utils/index.d.ts","../node_modules/@testing-library/react/types/index.d.ts","../node_modules/expect/node_modules/@jest/expect-utils/build/index.d.ts","../node_modules/chalk/index.d.ts","../node_modules/@sinclair/typebox/typebox.d.ts","../node_modules/pretty-format/node_modules/@jest/schemas/build/index.d.ts","../node_modules/pretty-format/build/index.d.ts","../node_modules/jest-matcher-utils/node_modules/jest-diff/build/index.d.ts","../node_modules/jest-matcher-utils/build/index.d.ts","../node_modules/expect/build/index.d.ts","../src/testing/editorViewTestHelpers.tsx","../src/components/__tests__/ProseMirror.composition.test.tsx","../node_modules/@wdio/types/build/Automation.d.ts","../node_modules/@wdio/types/build/Frameworks.d.ts","../node_modules/@wdio/types/build/Workers.d.ts","../node_modules/@wdio/types/build/Services.d.ts","../node_modules/@wdio/types/build/Reporters.d.ts","../node_modules/@wdio/types/build/Options.d.ts","../node_modules/@wdio/types/build/Capabilities.d.ts","../node_modules/@wdio/types/build/Network.d.ts","../node_modules/@wdio/types/build/index.d.ts","../node_modules/@wdio/protocols/build/types.d.ts","../node_modules/@wdio/protocols/build/commands/appium.d.ts","../node_modules/@wdio/protocols/build/commands/chromium.d.ts","../node_modules/@wdio/protocols/build/commands/gecko.d.ts","../node_modules/@wdio/protocols/build/commands/mjsonwp.d.ts","../node_modules/@wdio/protocols/build/commands/saucelabs.d.ts","../node_modules/@wdio/protocols/build/commands/selenium.d.ts","../node_modules/@wdio/protocols/build/commands/webdriver.d.ts","../node_modules/@wdio/protocols/build/protocols/webdriver.d.ts","../node_modules/@wdio/protocols/build/protocols/webdriverBidi.d.ts","../node_modules/@wdio/protocols/build/protocols/mjsonwp.d.ts","../node_modules/@wdio/protocols/build/protocols/appium.d.ts","../node_modules/@wdio/protocols/build/protocols/chromium.d.ts","../node_modules/@wdio/protocols/build/protocols/gecko.d.ts","../node_modules/@wdio/protocols/build/protocols/saucelabs.d.ts","../node_modules/@wdio/protocols/build/protocols/selenium.d.ts","../node_modules/@wdio/protocols/build/index.d.ts","../node_modules/webdriver/build/request/index.d.ts","../node_modules/webdriver/build/bidi/localTypes.d.ts","../node_modules/webdriver/build/bidi/remoteTypes.d.ts","../node_modules/@types/node/assert.d.ts","../node_modules/@types/node/assert/strict.d.ts","../node_modules/@types/node/globals.d.ts","../node_modules/@types/node/async_hooks.d.ts","../node_modules/@types/node/buffer.d.ts","../node_modules/@types/node/child_process.d.ts","../node_modules/@types/node/cluster.d.ts","../node_modules/@types/node/console.d.ts","../node_modules/@types/node/constants.d.ts","../node_modules/@types/node/crypto.d.ts","../node_modules/@types/node/dgram.d.ts","../node_modules/@types/node/diagnostics_channel.d.ts","../node_modules/@types/node/dns.d.ts","../node_modules/@types/node/dns/promises.d.ts","../node_modules/@types/node/domain.d.ts","../node_modules/@types/node/dom-events.d.ts","../node_modules/@types/node/events.d.ts","../node_modules/@types/node/fs.d.ts","../node_modules/@types/node/fs/promises.d.ts","../node_modules/@types/node/http.d.ts","../node_modules/@types/node/http2.d.ts","../node_modules/@types/node/https.d.ts","../node_modules/@types/node/inspector.d.ts","../node_modules/@types/node/module.d.ts","../node_modules/@types/node/net.d.ts","../node_modules/@types/node/os.d.ts","../node_modules/@types/node/path.d.ts","../node_modules/@types/node/perf_hooks.d.ts","../node_modules/@types/node/process.d.ts","../node_modules/@types/node/punycode.d.ts","../node_modules/@types/node/querystring.d.ts","../node_modules/@types/node/readline.d.ts","../node_modules/@types/node/readline/promises.d.ts","../node_modules/@types/node/repl.d.ts","../node_modules/@types/node/stream.d.ts","../node_modules/@types/node/stream/promises.d.ts","../node_modules/@types/node/stream/consumers.d.ts","../node_modules/@types/node/stream/web.d.ts","../node_modules/@types/node/string_decoder.d.ts","../node_modules/@types/node/test.d.ts","../node_modules/@types/node/timers.d.ts","../node_modules/@types/node/timers/promises.d.ts","../node_modules/@types/node/tls.d.ts","../node_modules/@types/node/trace_events.d.ts","../node_modules/@types/node/tty.d.ts","../node_modules/@types/node/url.d.ts","../node_modules/@types/node/util.d.ts","../node_modules/@types/node/v8.d.ts","../node_modules/@types/node/vm.d.ts","../node_modules/@types/node/wasi.d.ts","../node_modules/@types/node/worker_threads.d.ts","../node_modules/@types/node/zlib.d.ts","../node_modules/@types/node/globals.global.d.ts","../node_modules/@types/node/index.d.ts","../node_modules/@types/ws/index.d.ts","../node_modules/webdriver/build/bidi/core.d.ts","../node_modules/webdriver/build/bidi/handler.d.ts","../node_modules/webdriver/build/types.d.ts","../node_modules/webdriver/build/command.d.ts","../node_modules/webdriver/build/constants.d.ts","../node_modules/webdriver/build/utils.d.ts","../node_modules/webdriver/build/index.d.ts","../node_modules/webdriverio/build/utils/SevereServiceError.d.ts","../node_modules/webdriverio/build/dialog.d.ts","../node_modules/webdriverio/build/commands/browser/$$.d.ts","../node_modules/webdriverio/build/commands/browser/$.d.ts","../node_modules/webdriverio/build/utils/actions/base.d.ts","../node_modules/webdriverio/build/utils/actions/key.d.ts","../node_modules/webdriverio/build/utils/actions/pointer.d.ts","../node_modules/webdriverio/build/utils/actions/wheel.d.ts","../node_modules/webdriverio/build/utils/actions/index.d.ts","../node_modules/webdriverio/build/commands/browser/action.d.ts","../node_modules/webdriverio/build/commands/browser/actions.d.ts","../node_modules/webdriverio/build/commands/browser/addInitScript.d.ts","../node_modules/webdriverio/build/commands/browser/call.d.ts","../node_modules/webdriverio/build/commands/browser/custom$$.d.ts","../node_modules/webdriverio/build/commands/browser/custom$.d.ts","../node_modules/webdriverio/build/commands/browser/debug.d.ts","../node_modules/webdriverio/build/commands/browser/deleteCookies.d.ts","../node_modules/webdriverio/build/commands/browser/downloadFile.d.ts","../node_modules/@types/sinonjs__fake-timers/index.d.ts","../node_modules/webdriverio/build/clock.d.ts","../node_modules/webdriverio/build/deviceDescriptorsSource.d.ts","../node_modules/webdriverio/build/commands/browser/emulate.d.ts","../node_modules/webdriverio/build/commands/browser/execute.d.ts","../node_modules/webdriverio/build/commands/browser/executeAsync.d.ts","../node_modules/webdriverio/build/commands/browser/getCookies.d.ts","../node_modules/webdriverio/build/commands/browser/getPuppeteer.d.ts","../node_modules/webdriverio/build/commands/browser/getWindowSize.d.ts","../node_modules/webdriverio/build/commands/browser/keys.d.ts","../node_modules/webdriverio/build/utils/interception/types.d.ts","../node_modules/urlpattern-polyfill/dist/types.d.ts","../node_modules/urlpattern-polyfill/dist/index.d.ts","../node_modules/webdriverio/build/utils/interception/index.d.ts","../node_modules/webdriverio/build/commands/browser/mock.d.ts","../node_modules/webdriverio/build/commands/browser/mockClearAll.d.ts","../node_modules/webdriverio/build/commands/browser/mockRestoreAll.d.ts","../node_modules/webdriverio/build/commands/browser/newWindow.d.ts","../node_modules/webdriverio/build/commands/browser/pause.d.ts","../node_modules/webdriverio/build/commands/browser/react$$.d.ts","../node_modules/webdriverio/build/commands/browser/react$.d.ts","../node_modules/webdriverio/build/commands/browser/reloadSession.d.ts","../node_modules/webdriverio/build/commands/browser/restore.d.ts","../node_modules/webdriverio/build/commands/browser/savePDF.d.ts","../node_modules/webdriverio/build/commands/browser/saveRecordingScreen.d.ts","../node_modules/webdriverio/build/commands/browser/saveScreenshot.d.ts","../node_modules/webdriverio/build/commands/browser/scroll.d.ts","../node_modules/webdriverio/build/commands/browser/setCookies.d.ts","../node_modules/webdriverio/build/commands/browser/setTimeout.d.ts","../node_modules/webdriverio/build/commands/browser/setViewport.d.ts","../node_modules/webdriverio/build/commands/browser/setWindowSize.d.ts","../node_modules/webdriverio/build/commands/browser/switchWindow.d.ts","../node_modules/webdriverio/build/commands/browser/throttle.d.ts","../node_modules/webdriverio/build/commands/browser/throttleCPU.d.ts","../node_modules/webdriverio/build/commands/browser/throttleNetwork.d.ts","../node_modules/webdriverio/build/commands/browser/touchAction.d.ts","../node_modules/webdriverio/build/commands/browser/uploadFile.d.ts","../node_modules/webdriverio/build/commands/browser/url.d.ts","../node_modules/webdriverio/build/commands/browser/waitUntil.d.ts","../node_modules/webdriverio/build/commands/browser.d.ts","../node_modules/webdriverio/build/commands/element/$$.d.ts","../node_modules/webdriverio/build/commands/element/$.d.ts","../node_modules/webdriverio/build/commands/element/addValue.d.ts","../node_modules/webdriverio/build/commands/element/clearValue.d.ts","../node_modules/webdriverio/build/commands/element/click.d.ts","../node_modules/webdriverio/build/commands/element/custom$$.d.ts","../node_modules/webdriverio/build/commands/element/custom$.d.ts","../node_modules/webdriverio/build/commands/element/doubleClick.d.ts","../node_modules/webdriverio/build/commands/element/dragAndDrop.d.ts","../node_modules/webdriverio/build/commands/element/execute.d.ts","../node_modules/webdriverio/build/commands/element/executeAsync.d.ts","../node_modules/webdriverio/build/commands/element/getAttribute.d.ts","../node_modules/webdriverio/build/commands/element/getCSSProperty.d.ts","../node_modules/webdriverio/build/commands/element/getComputedRole.d.ts","../node_modules/webdriverio/build/commands/element/getComputedLabel.d.ts","../node_modules/webdriverio/build/commands/element/getElement.d.ts","../node_modules/webdriverio/build/commands/element/getHTML.d.ts","../node_modules/webdriverio/build/commands/element/getLocation.d.ts","../node_modules/webdriverio/build/commands/element/getProperty.d.ts","../node_modules/webdriverio/build/commands/element/getSize.d.ts","../node_modules/webdriverio/build/commands/element/getTagName.d.ts","../node_modules/webdriverio/build/commands/element/getText.d.ts","../node_modules/webdriverio/build/commands/element/getValue.d.ts","../node_modules/webdriverio/build/commands/element/isClickable.d.ts","../node_modules/webdriverio/build/commands/element/isDisplayed.d.ts","../node_modules/webdriverio/build/commands/element/isEnabled.d.ts","../node_modules/webdriverio/build/commands/element/isEqual.d.ts","../node_modules/webdriverio/build/commands/element/isExisting.d.ts","../node_modules/webdriverio/build/commands/element/isFocused.d.ts","../node_modules/webdriverio/build/commands/element/isSelected.d.ts","../node_modules/webdriverio/build/commands/element/isStable.d.ts","../node_modules/webdriverio/build/commands/element/moveTo.d.ts","../node_modules/webdriverio/build/commands/element/nextElement.d.ts","../node_modules/webdriverio/build/commands/element/parentElement.d.ts","../node_modules/webdriverio/build/commands/element/previousElement.d.ts","../node_modules/webdriverio/build/commands/element/react$$.d.ts","../node_modules/webdriverio/build/commands/element/react$.d.ts","../node_modules/webdriverio/build/commands/element/saveScreenshot.d.ts","../node_modules/webdriverio/build/commands/element/scrollIntoView.d.ts","../node_modules/webdriverio/build/commands/element/selectByAttribute.d.ts","../node_modules/webdriverio/build/commands/element/selectByIndex.d.ts","../node_modules/webdriverio/build/commands/element/selectByVisibleText.d.ts","../node_modules/webdriverio/build/commands/element/setValue.d.ts","../node_modules/webdriverio/build/commands/element/shadow$$.d.ts","../node_modules/webdriverio/build/commands/element/shadow$.d.ts","../node_modules/webdriverio/build/commands/element/touchAction.d.ts","../node_modules/webdriverio/build/commands/element/waitForClickable.d.ts","../node_modules/webdriverio/build/commands/element/waitForDisplayed.d.ts","../node_modules/webdriverio/build/commands/element/waitForEnabled.d.ts","../node_modules/webdriverio/build/commands/element/waitForExist.d.ts","../node_modules/webdriverio/build/commands/element/waitForStable.d.ts","../node_modules/webdriverio/build/commands/element/waitUntil.d.ts","../node_modules/webdriverio/build/commands/element.d.ts","../node_modules/webdriverio/build/types.d.ts","../node_modules/webdriverio/build/index.d.ts","../src/components/__tests__/ProseMirror.domchange.test.tsx","../src/components/__tests__/ProseMirror.draw-decoration.test.tsx","../src/components/__tests__/ProseMirror.draw.test.tsx","../src/components/__tests__/ProseMirror.node-view.test.tsx","../src/components/__tests__/ProseMirror.selection.test.tsx","../src/components/__tests__/ProseMirror.test.tsx","../src/contexts/__tests__/DeferredLayoutEffects.test.tsx","../src/hooks/__tests__/useEditorViewLayoutEffect.test.tsx","../src/plugins/componentEventListenersPlugin.ts","../src/plugins/__tests__/reactKeys.test.ts","../src/testing/setupProseMirrorView.ts","../node_modules/@vitest/pretty-format/dist/index.d.ts","../node_modules/@vitest/snapshot/dist/environment-Ddx0EDtY.d.ts","../node_modules/@vitest/snapshot/dist/index-Y6kQUiCB.d.ts","../node_modules/@vitest/snapshot/dist/index.d.ts","../node_modules/expect-webdriverio/types/expect-webdriverio.d.ts","../node_modules/expect-webdriverio/types/standalone.d.ts","../node_modules/@wdio/globals/types.d.ts","../node_modules/@types/mocha/index.d.ts","../node_modules/@wdio/mocha-framework/build/types.d.ts","../node_modules/@wdio/mocha-framework/build/index.d.ts","../node_modules/@types/jest/node_modules/jest-diff/build/cleanupSemantic.d.ts","../node_modules/@types/jest/node_modules/pretty-format/build/index.d.ts","../node_modules/@types/jest/node_modules/jest-diff/build/types.d.ts","../node_modules/@types/jest/node_modules/jest-diff/build/diffLines.d.ts","../node_modules/@types/jest/node_modules/jest-diff/build/printDiffs.d.ts","../node_modules/@types/jest/node_modules/jest-diff/build/index.d.ts","../node_modules/@types/jest/node_modules/jest-matcher-utils/build/index.d.ts","../node_modules/@types/jest/index.d.ts"],"fileInfos":[{"version":"8730f4bf322026ff5229336391a18bcaa1f94d4f82416c8b2f3954e2ccaae2ba","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","4b421cbfb3a38a27c279dec1e9112c3d1da296f77a1a85ddadf7e7a425d45d18","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9","746d62152361558ea6d6115cf0da4dd10ede041d14882ede3568bce5dc4b4f1f","d11a03592451da2d1065e09e61f4e2a9bf68f780f4f6623c18b57816a9679d17",{"version":"3aafcb693fe5b5c3bd277bd4c3a617b53db474fe498fc5df067c5603b1eebde7","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"5f406584aef28a331c36523df688ca3650288d14f39c5d2e555c95f0d2ff8f6f","affectsGlobalScope":true},{"version":"22f230e544b35349cfb3bd9110b6ef37b41c6d6c43c3314a31bd0d9652fcec72","affectsGlobalScope":true},{"version":"7ea0b55f6b315cf9ac2ad622b0a7813315bb6e97bf4bb3fbf8f8affbca7dc695","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"eb26de841c52236d8222f87e9e6a235332e0788af8c87a71e9e210314300410a","affectsGlobalScope":true},{"version":"3be5a1453daa63e031d266bf342f3943603873d890ab8b9ada95e22389389006","affectsGlobalScope":true},{"version":"17bb1fc99591b00515502d264fa55dc8370c45c5298f4a5c2083557dccba5a2a","affectsGlobalScope":true},{"version":"7ce9f0bde3307ca1f944119f6365f2d776d281a393b576a18a2f2893a2d75c98","affectsGlobalScope":true},{"version":"6a6b173e739a6a99629a8594bfb294cc7329bfb7b227f12e1f7c11bc163b8577","affectsGlobalScope":true},{"version":"81cac4cbc92c0c839c70f8ffb94eb61e2d32dc1c3cf6d95844ca099463cf37ea","affectsGlobalScope":true},{"version":"b0124885ef82641903d232172577f2ceb5d3e60aed4da1153bab4221e1f6dd4e","affectsGlobalScope":true},{"version":"0eb85d6c590b0d577919a79e0084fa1744c1beba6fd0d4e951432fa1ede5510a","affectsGlobalScope":true},{"version":"da233fc1c8a377ba9e0bed690a73c290d843c2c3d23a7bd7ec5cd3d7d73ba1e0","affectsGlobalScope":true},{"version":"d154ea5bb7f7f9001ed9153e876b2d5b8f5c2bb9ec02b3ae0d239ec769f1f2ae","affectsGlobalScope":true},{"version":"bb2d3fb05a1d2ffbca947cc7cbc95d23e1d053d6595391bd325deb265a18d36c","affectsGlobalScope":true},{"version":"c80df75850fea5caa2afe43b9949338ce4e2de086f91713e9af1a06f973872b8","affectsGlobalScope":true},{"version":"9d57b2b5d15838ed094aa9ff1299eecef40b190722eb619bac4616657a05f951","affectsGlobalScope":true},{"version":"6c51b5dd26a2c31dbf37f00cfc32b2aa6a92e19c995aefb5b97a3a64f1ac99de","affectsGlobalScope":true},{"version":"6e7997ef61de3132e4d4b2250e75343f487903ddf5370e7ce33cf1b9db9a63ed","affectsGlobalScope":true},{"version":"2ad234885a4240522efccd77de6c7d99eecf9b4de0914adb9a35c0c22433f993","affectsGlobalScope":true},{"version":"5e5e095c4470c8bab227dbbc61374878ecead104c74ab9960d3adcccfee23205","affectsGlobalScope":true},{"version":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"2768ef564cfc0689a1b76106c421a2909bdff0acbe87da010785adab80efdd5c","affectsGlobalScope":true},{"version":"b248e32ca52e8f5571390a4142558ae4f203ae2f94d5bac38a3084d529ef4e58","affectsGlobalScope":true},{"version":"6c55633c733c8378db65ac3da7a767c3cf2cf3057f0565a9124a16a3a2019e87","affectsGlobalScope":true},{"version":"fb4416144c1bf0323ccbc9afb0ab289c07312214e8820ad17d709498c865a3fe","affectsGlobalScope":true},{"version":"5b0ca94ec819d68d33da516306c15297acec88efeb0ae9e2b39f71dbd9685ef7","affectsGlobalScope":true},{"version":"34c839eaaa6d78c8674ae2c37af2236dee6831b13db7b4ef4df3ec889a04d4f2","affectsGlobalScope":true},{"version":"34478567f8a80171f88f2f30808beb7da15eac0538ae91282dd33dce928d98ed","affectsGlobalScope":true},{"version":"ab7d58e6161a550ff92e5aff755dc37fe896245348332cd5f1e1203479fe0ed1","affectsGlobalScope":true},{"version":"6bda95ea27a59a276e46043b7065b55bd4b316c25e70e29b572958fa77565d43","affectsGlobalScope":true},{"version":"aedb8de1abb2ff1095c153854a6df7deae4a5709c37297f9d6e9948b6806fa66","affectsGlobalScope":true},{"version":"a4da0551fd39b90ca7ce5f68fb55d4dc0c1396d589b612e1902f68ee090aaada","affectsGlobalScope":true},{"version":"11ffe3c281f375fff9ffdde8bbec7669b4dd671905509079f866f2354a788064","affectsGlobalScope":true},{"version":"52d1bb7ab7a3306fd0375c8bff560feed26ed676a5b0457fa8027b563aecb9a4","affectsGlobalScope":true},{"version":"5b5c3e9bcbdad9256a14bd8f74844449cba0bf947f46f6d829c86e049f0a2052","signature":"aebbc63575486ad034a7c43fae3d5a2668cc3593c8934e0a0d6a9e72d0500be8"},{"version":"af48c893c768f3209eaa2d72a9dcad8fa76e08dfa096946e945c391e47cbb3b7","signature":"2751c3a8dfaaf555fb19127ae178d221bb67bdf93bf36e060e95d52dd7e83698"},"3c616cbdf6f11c71a1cadba225121b0733b1c3da2a4855d7247a22bc64ee28cd","5d397401fd58e55fa1a75cf509591e160bd4a3b58a891f6866002951a340791e","1ef7565ffec4456d8a50e72f1cc254f46f2d35e0f34692f9096b11266fdfc6b9","e29c3246bccba476f4285c89ea0c026b6bfdf9e3d15b6edf2d50e7ea1a59ecfb","1ef7565ffec4456d8a50e72f1cc254f46f2d35e0f34692f9096b11266fdfc6b9",{"version":"0bfa81a8b71a10d7e332b88bfc482e6d6a74692705a9d4510bf6b049ceaaa0d2","affectsGlobalScope":true},{"version":"bbdf156fea2fabed31a569445835aeedcc33643d404fcbaa54541f06c109df3f","affectsGlobalScope":true},"1c29793071152b207c01ea1954e343be9a44d85234447b2b236acae9e709a383","6a386ff939f180ae8ef064699d8b7b6e62bc2731a62d7fbf5e02589383838dea","f5a8b384f182b3851cec3596ccc96cb7464f8d3469f48c74bf2befb782a19de5",{"version":"7fb6faf1006c3503d44e4922c8c65772ddc98e92bad7c2ae2d5db123f3e297b3","affectsGlobalScope":true},{"version":"bbdf156fea2fabed31a569445835aeedcc33643d404fcbaa54541f06c109df3f","affectsGlobalScope":true},{"version":"d11a5d3967290ea6be7e6d7667c4673a7626cfaa32cfc68101000d2b218606ab","affectsGlobalScope":true},"e4dd91dd4789a109aab51d8a0569a282369fcda9ba6f2b2297bc61bacfb1a042",{"version":"e5f37dbfe704025bdb85794d974be2f5826824473845878a44aa1a186a9c8140","signature":"b3dc9f1efc355e2871fad138ba7368337463b23c7f497534beb03056e4af4b32"},{"version":"d07184f3a87f2bbc1475e91248fe4428c921d203ad2d469a6aa1326b7f3e6932","signature":"d8dd20482ecba9a40ffe9a977c4d425c5d7d860f23127a1b6575f55c685d0957"},{"version":"b212f0238994bdd57de7a425006b0df90aa722d4b02710042fa9a1d23bec4af8","signature":"b44bb8a9294a2f46998581bb765b4eab9cb0b6a658a8ca8437ccbe71503d111c"},{"version":"b7fd958851d1b238dd7a2e1e629c136d26987e24108a63be416b1a751718df28","signature":"ae81fee2c29f557499d0bfc3875cff720867a28da336fde92045a7816cef891c"},{"version":"9ed6c2ec2109c03351224547936c83bf265c6b5c268570f115f1dbe061631ccd","signature":"ad4fec76299a49e89432ab06de31eae7a2e71d62518273efdecb8fc2e968240a"},{"version":"ca61d0ae658693bf65c825554648a12adab47e16941cfdc5a162e499ad2d1355","signature":"f671df38e8ccc0826e2bac98a22076f14689293d7bca4fbc6f06a94c91585497"},"b05d32c486bb23840ec5623f9604e7cb1a97be6fe512f33fce8561c20e1c68c0",{"version":"50dea74987fa6c6b20a5f9f5fd1f8036063fda0a9bde8bbc0da978de0371fef2","signature":"1e9bad8619d085f60c6c836bfcce4d0ca002030dad769d7844a2dac0cec57336"},{"version":"9e664ef9f93adfbd1d5d585136b5240fe251bcee6576287b23c4b5869fbd022b","signature":"fe2612a0281d70e958c5522f0d15756c1c345f6c90d008781ceac5708f9fd3fd"},{"version":"564398bb53c7c06506768bf88b2ebbb094dd71b90a22cf2fd9700beb7cd9c382","signature":"8dd028de430fa7b2c35b74b5b5c24d43b09542fce7fe4e461b291222cdc46171"},{"version":"dc211fd2cac1df57f0ef95c5ef1f65c48f2414a2cffb707c2e5546dd15640a60","signature":"01c1afb666ad23e839d19d712c451ecd053bee665a96bb352bb78df49884149a"},{"version":"bed57f8624b8a1487d260ec75d619effaa351cf0220aec8115d1bd74b7652f02","signature":"4de268d814cdf44767d1e48058071a2fbe94c2849a4f5746520a8e9eb98fbee5"},{"version":"ba080ea03f9691efdb0eeb58520e619ff0bb292287277e0b7fe338344af50fbd","signature":"4e94f55183fa720ca5616b7d67c84b54d0ee9f712dd9d9520beae30bd97c12fa"},{"version":"782b111cb2bc62a0f7f42f15773621ac731e4781cfd272010d57608b33dc2bd4","signature":"892d7aa74fbdea8ca3fb795575165f6b10347d1b99ae752ae85a16a5adbde957"},{"version":"285ced29c9ea33c1fa938c9326345e88f17cae4d3bef4fe3a281bf42d9e0280b","signature":"172d66a7e47b56fbeaea7503b6997a7336c6dcb7e2882306f0c4f07f83d84b67"},{"version":"5141a9369196bde48cd4679ecadd33483a4c1539c759113bba9ca432b10920d0","signature":"795057e39cf21e76c5e393ca04d6457002c3f6250588682d48c5404b24454c7f"},{"version":"39354abee942c5dcdd5f3454021e21ce871916ea17b426d3b6d557673fe4f5e8","signature":"d03764288f8f0f1ab2c1a3673444c1ebb16c4a2cd2870b648ae16b23ba7cc6a3"},{"version":"85ead12d3a84427868cedd8283c7f19a892efc18f52872dd550cc4b59e00c340","signature":"83078aca6f4e00a583d9f1b0f210e77ae151908164b929942ffc2d9877d91622"},{"version":"8bf39511307af01fbfe2f4b6f1f18f6774f1bec19d31f41b31e940316b007d49","signature":"58528afde29d23c1685bd6695842bd1e3cfa21ec865b0b6eef38967194db928f"},{"version":"f370d05100d4981aa6dca9a3295f46d302d4233db391f2d19c4b5eabc08b378e","signature":"43bacd11442c9e6fa8937f86dd117de45a4f95c07af457ac8c1cbb6f59ff0933"},{"version":"002536ecc1d4c8e1812111c8c2a477543d7d6bcbbe4b2be2b930f6c6be7947ef","signature":"2143bcca33532154509650778ad1cdd203c7bbde83c3e7672fce28536ed929f4"},{"version":"1a3fbde83b2278392f20578a1e3c0a79de4efd2ac7de8c80d02bfe08df15b6d6","signature":"76a945ec70ac02d5903b3a23e06356a9a6ac938e533c70184eed942c9aa269e8"},{"version":"e05e6536b56eff89dd4abd30daf6c25c4b0aa15a5ac8c641f54b7089091d2f2d","signature":"842cf9d01140ef2336d7de002ac27430b1ec1d3c1c9436779018ef076b29e86f"},{"version":"c5ba4b28a8e337860418ac81e1c1bf18a823f18ee78c5f048717283e9aeb40e0","signature":"d58f6e441744852cafb67094b531599849187ed18fe2afebc31f57d568275caa"},{"version":"06d1a2fe973302ed719ff140090fa2f531d704f68832ea682be0193c646be745","signature":"3fa9d72e04068888ad7487366554cd404a463b46ad625948fbd60772e3f5ea15"},{"version":"3f343a7dafdc1e0e1d513c0baebc02af8016a408c3ad0a07f7bd045ecba2cc43","signature":"60ddf25ad3e766a1ac14fc6cb1929515e3ba5b2a0b4d8fa3a896d31df1f5e755"},{"version":"89d288533f67fb6ca3c3205b560857a1434cbe46268ba883d028f2e6af3d6b69","signature":"4b3fd343fda39c552d8eafbc177b656b3e9858c666650d0b0ea4f00bc05742d9"},{"version":"92e03c0ea8c40052fc6f0cfd7bcfbce3cbe935e12d91f1431a84673f99d73285","signature":"852fd8e4c2d9d6b982f412f029ba7873b9624b6ff824a5b58bed54cba3e0d88e"},{"version":"417008fe97aadd6ecca73ba7c54b2de4022cdcd8baf64f530086e3c2744186ae","signature":"80a1748b01a95df654272e3068e37836032ed348f16bbdb14cbaafa024431fa4"},{"version":"676ba1ac83311f91ae05d5ef0a783ca50fa3a9291559873995a26f52fb79bd76","signature":"cdfba960793689c099c32e06a5926d4778b012403128db36ca50a48c39615ad0"},"2bcb1acd536e696b5e4405ab92e847eb7b7eaa121c8e80c96394c130f141919f",{"version":"c93094c9c8dcb350a16168454316f037122d0809bedf3913334633de2b938a1f","signature":"163ff71678629a16c8c96bdaf1856dd26aa2f52e78c1577bfb44200aa7223bfe"},{"version":"feb89562956dccdc18a52f4702c41e099517dbd66fcfd7ad1a2f26ab56fb1134","signature":"e52543db07301f43bccb5fb291bdd2c161ef63c1534a97c2c81a9c2515fe0407"},{"version":"cbb9a7f9cadbf434947c3e1ce90caefc811efd3b706a5f1845452b5092f84fb7","signature":"68dd3503c1f29a0548b3c9876b82fb0525487f52caf2ba37767a8b2865197e58"},{"version":"59b6aa9194a173dbdaf69739244d9327861ffa64529f446dbccafa188da516c1","signature":"89d6765b9d573bcff261e277f461f49584d63e7a9d653c63b9f1bf5012c909bf"},{"version":"b6a3778c3bd4622e3b872ff419da9c7d6320600c3e602e600a9832ff8a727772","signature":"5d3b66dc001352732caf728ac1f720cc69c929fd92a57062a66d07ad53d9521e"},{"version":"6385cb883840c92ed59e597d1e7ee96a1d40bcbf36d278c3ff16663b6c9878f5","signature":"f04980e8cac976e04fd0f2f29e5eccba5c12f490c006bd7b9cb24c831a6c0631"},{"version":"4eb5bb3918b82a68c1c4ef032bdc432cbebf567e7e09ef2f41ad76af163a6501","signature":"e0dddccdae3a53e537174aeabff85042a53c1f97e2ae230a71bed373a5235447"},{"version":"5b7528efd2ea0043dc5e95c81851ff20c84cfc562fae9f4af66e33e03ae52f96","signature":"2509ee619f10dee9c5154aefbc3ef4b7ab58a6e840170dab120b66f395f44e99"},{"version":"00f20f18c1e34a9d71a593f795628cdf51ffd020aa2b01a51186bae0f3c43590","signature":"d559b5f087996e5bb69710d07c61ad03646fd8ed5ca6caf5ec401f5332614bf5"},{"version":"d49fac8d9ce3c3f6186d2780cbb9fed0eabefee307f75893fafebedef3b5fab4","signature":"9d03270300fd5bebef3a83b29ec154b975295be1a4db0163e637c313ca904493"},{"version":"643a81ffb6b6cdfcce4e854db2e16d905f1afe49b7d7adde7a3d0b84ccf7c511","signature":"73eb92b801117259b9f276ba9ff83522a84ccbe64f5bd826ced928c4c9816dde"},{"version":"fe6286033739a7d90a08c3dc468e4e6801c12c130ecc908921a2965acf25d845","signature":"c23803dfca8d4f541ade54c535a4604142c99a68027e21c27ec47c16cfb2ad3c"},{"version":"ca82ab2dd22a6fc83ce078366c987e56728a45992fad8c9b9f7ac4ad9c29adee","signature":"6f92b5ac2f6eaa636e3762e1bf8ac4c47720bc5f167bb633dfc2722adfda9d64"},{"version":"8b08d6eba097b24505e5495a336f6fee1b07137f6e0442a2f1957e1383202c43","signature":"26ad24babf2028fe663038de50853e71531972269fdf064e2179a60b54514309"},{"version":"e300aec8c117f671cb13615312f749d0fb2111a47184134330d48a2da80fc934","signature":"14f179d5d86b3a6f2f14549b260bd8d9542cdb3473c01375addd3d99a444accf"},{"version":"a73fda0175437366ed4b8e2f84a502db5980a8049ec410c3628f16e144797563","signature":"3aa1e62307fdcfd8918596172c7f7b6b532743b6cff15f3c920c0ea84f24e759"},{"version":"2b3a9d33e0efa3a2ce25733ba4cf774d1b4e2f6507e5db24ec90cc5ddb5b1821","signature":"548e3ac785541b27e0eb1daa941c1510f61ae9db077a5b48b887ab0e804d93f8"},{"version":"da97e4870bac9e83e936b0043cdc3c7f042bc1db189842e00887bf362e86a891","signature":"b1d46e2a56509afbbc354a1a226dc833c4ed3fc7d21bf530f40663cfa57a2c6a"},{"version":"9999be72668f140c79696039ac1a109c08705c91b71204a036050c1aa3466813","signature":"b577a773b705499032e0f2166437e36462e67bf96d2c3a4f04078fc221d366ff"},"18d1bd2540f1feea5c97f9078da29aa96fd080b141272ecce0d091aae3d373e5","83e27bbd7304ea67f9afa1535f1d4fdb15866089f0d893c784cbb5b1c6fb3386","21522c0f405e58c8dd89cd97eb3d1aa9865ba017fde102d01f86ab50b44e5610","3cfb7c0c642b19fb75132154040bb7cd840f0002f9955b14154e69611b9b3f81","8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b","d16f1c460b1ca9158e030fdf3641e1de11135e0c7169d3e8cf17cc4cc35d5e64","a934063af84f8117b8ce51851c1af2b76efe960aa4c7b48d0343a1b15c01aedf","e3c5ad476eb2fca8505aee5bdfdf9bf11760df5d0f9545db23f12a5c4d72a718","462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","d0570ce419fb38287e7b39c910b468becb5b2278cf33b1000a3d3e82a46ecae2","3aca7f4260dad9dcc0a0333654cb3cde6664d34a553ec06c953bce11151764d7","a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988","b58f396fe4cfe5a0e4d594996bc8c1bfe25496fbc66cf169d41ac3c139418c77","45785e608b3d380c79e21957a6d1467e1206ac0281644e43e8ed6498808ace72","bece27602416508ba946868ad34d09997911016dbd6893fb884633017f74e2c5","2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a","82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46","b4966c503c08bbd9e834037a8ab60e5f53c5fd1092e8873c4a1c344806acdab2","d1c08287c9c6a9c60c53142c122e9c1909f5a159301b9b06b0eaf12ab547920b","2a440f32bf83a8ef4bac939a6a3b8b59e8b14a060032444c9bb3ba7605a4c403","cdcc132f207d097d7d3aa75615ab9a2e71d6a478162dde8b67f88ea19f3e54de","0d14fa22c41fdc7277e6f71473b20ebc07f40f00e38875142335d5b63cdfc9d2","c085e9aa62d1ae1375794c1fb927a445fa105fed891a7e24edbb1c3300f7384a","f315e1e65a1f80992f0509e84e4ae2df15ecd9ef73df975f7c98813b71e4c8da","5b9586e9b0b6322e5bfbd2c29bd3b8e21ab9d871f82346cb71020e3d84bae73e","3e70a7e67c2cb16f8cd49097360c0309fe9d1e3210ff9222e9dac1f8df9d4fb6","ab68d2a3e3e8767c3fba8f80de099a1cfc18c0de79e42cb02ae66e22dfe14a66","d96cc6598148bf1a98fb2e8dcf01c63a4b3558bdaec6ef35e087fd0562eb40ec",{"version":"0a91552c60552ca235929b4b61f0b9121a28179f9271e3cfed66f2c67bc76dcd","signature":"e1bb82b694aed888d65fe15910b80c0b0d4a90f25d07c5c0b5b2844529bf0972"},{"version":"607cabb4db3766d5474d07afafc2e65e9cce134c29617bfb195738ec951177c2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"736c14342c95cdf46bec3c40322a160d7f0bceba7a7eb1a32f5a5934ad9439c3","deab484053e0ffc3e81f612f28291d054f26db2a44e9e83ba2d37755f789b232","2ffd5793d20504e56283413ca9eb9b0e8d871c57c5cb39e5e0df4d685a14317b","88912298e9014ddc36061054b0a14076a02aa576072ec1626f60e0f6bac9116f","29ff006e9162a63aafca305a6d26ed01ee3148f6c704918fd8dfaa1a2406da29","ada1b7cb7b88c059ac5818af61c7da17c1c699def51afa4d597c121552e34211",{"version":"a2f274e1d29e4c0b66637fe965e30745ede683b4643a24b82a59d734eae399fb","affectsGlobalScope":true},"63daeca7642998df05f51ac891557e11323f273085258d1c866acb7bbc6b17c1",{"version":"4a04f8d2eaa94917389d4763c64731a485f6054bd578cf21a60a4a4cc246145e","affectsGlobalScope":true},"8f66b6c9aed8958a562a913aa37f1c4c4feab5bd8d4bf693b909d6f598dfff03","584d6d9e44560dd474d1da093e81f87295456c7db0acb4a5021056af1318b213","055658d153e441e8e1c32ad4f9163d4cd0db9050ced4081dccd3af22a7356e53","6495b2dff79813b07677f30da318bc9d7e567983d5662725ca08857f5912829f","99e43dd48dbe17d3ea455ee5b9b61c04133156ce8564fc5f7c13264e621251fe","da780b391d2bffaa7b40cff38ee5815dec7ef4604fa43c782191cc33bc32ffff","8fcdc1b508dce5264483bd3664112e353bc7ce8d0c5b7b48fb099dabab3cea9c","adb3a40db2048227ee4ebe4ea66da876542f2e3bdac5353582600e20ba93125f","d27cf84de2c103c8cff521c139cc6edd946182bffbc47d21d95a0ee9d3baa6d8","c889aac0319d4f720f0f2422342a6c7e41fc53aa66830d49cc7340f8c5979594","7cd4800e0e14d4863f157b7c58689f30fd176afb3ee4d71d0c115fe8dba320f0","537d0632e5a963e5592c7782542bfd8e9eb46e7764b7b8e98a62ed9bb0db7eb8","a9a3ef276dafbee91e79da7c773f58ff38bb6334b765fa0957145f05d243dd7e","e093e246587c564e58a709a4218b91216832cbb73ab84ccf7f53093da8958a7d","b1eb2133c35da51a65acd4113f768e8c7f449303cdd8dab0b0ffca454acfeaf2","37660d7ca9a44aae06920704fb48dbda2cf27fbea47fcbc63b014024e2d89a9b","61d4877dd902176fa85d87da19b561fd21f1caa9ea1c311600cf89222ecc2c57","8cb39f70ec69a01a60479dbc16842e707fcc4804bd5f90c79e370ce2781571ee","854a9b46a5f263bb77392578ded028ef0476cd79363bdc2e311656add637a9f0","e3e93bfa9b97d15bc0bcf68ae28c5d738bec491fa124c0fc5df7a3603d394c3f","7e771891adaa85b690266bc37bd6eb43bc57eecc4b54693ead36467e7369952a","a69c09dbea52352f479d3e7ac949fde3d17b195abe90b045d619f747b38d6d1a",{"version":"ca72190df0eb9b09d4b600821c8c7b6c9747b75a1c700c4d57dc0bb72abc074c","affectsGlobalScope":true},"21a167fec8f933752fb8157f06d28fab6817af3ad9b0bdb1908a10762391eab9",{"version":"bb65c6267c5d6676be61acbf6604cf0a4555ac4b505df58ac15c831fcbff4e3e","affectsGlobalScope":true},"374ca798f244e464346f14301dc2a8b4b111af1a83b49fffef5906c338a1f922","5a94487653355b56018122d92392beb2e5f4a6c63ba5cef83bbe1c99775ef713",{"version":"d5135ad93b33adcce80b18f8065087934cdc1730d63db58562edcf017e1aad9b","affectsGlobalScope":true},"82408ed3e959ddc60d3e9904481b5a8dc16469928257af22a3f7d1a3bc7fd8c4","dab86d9604fe40854ef3c0a6f9e8948873dc3509213418e5e457f410fd11200f","bb9c4ffa5e6290c6980b63c815cdd1625876dadb2efaf77edbe82984be93e55e","489532ff54b714f0e0939947a1c560e516d3ae93d51d639ab02e907a0e950114","f30bb836526d930a74593f7b0f5c1c46d10856415a8f69e5e2fc3db80371e362","14b5aa23c5d0ae1907bc696ac7b6915d88f7d85799cc0dc2dcf98fbce2c5a67c","5c439dafdc09abe4d6c260a96b822fa0ba5be7203c71a63ab1f1423cd9e838ea",{"version":"6b526a5ec4a401ca7c26cfe6a48e641d8f30af76673bad3b06a1b4504594a960","affectsGlobalScope":true},{"version":"816ad2e607a96de5bcac7d437f843f5afd8957f1fa5eefa6bba8e4ed7ca8fd84","affectsGlobalScope":true},"cec36af22f514322f870e81d30675c78df82ae8bf4863f5fd4e4424c040c678d","d903fafe96674bc0b2ac38a5be4a8fc07b14c2548d1cdb165a80ea24c44c0c54","5eec82ac21f84d83586c59a16b9b8502d34505d1393393556682fe7e7fde9ef2","04eb6578a588d6a46f50299b55f30e3a04ef27d0c5a46c57d8fcc211cd530faa","8d3c583a07e0c37e876908c2d5da575019f689df8d9fa4c081d99119d53dba22","2c828a5405191d006115ab34e191b8474bc6c86ffdc401d1a9864b1b6e088a58",{"version":"e630e5528e899219ae319e83bef54bf3bcb91b01d76861ecf881e8e614b167f0","affectsGlobalScope":true},"d076fede3cb042e7b13fc29442aaa03a57806bc51e2b26a67a01fbc66a7c0c12","7c013aa892414a7fdcfd861ae524a668eaa3ede8c7c0acafaf611948122c8d93","b0973c3cbcdc59b37bf477731d468696ecaf442593ec51bab497a613a580fe30",{"version":"4989e92ba5b69b182d2caaea6295af52b7dc73a4f7a2e336a676722884e7139d","affectsGlobalScope":true},{"version":"b3624aed92dab6da8484280d3cb3e2f4130ec3f4ef3f8201c95144ae9e898bb6","affectsGlobalScope":true},"5153a2fd150e46ce57bb3f8db1318d33f6ad3261ed70ceeff92281c0608c74a3","210d54cd652ec0fec8c8916e4af59bb341065576ecda039842f9ffb2e908507c","36b03690b628eab08703d63f04eaa89c5df202e5f1edf3989f13ad389cd2c091","0effadd232a20498b11308058e334d3339cc5bf8c4c858393e38d9d4c0013dcf","25846d43937c672bab7e8195f3d881f93495df712ee901860effc109918938cc","fd93cee2621ff42dabe57b7be402783fd1aa69ece755bcba1e0290547ae60513","1b952304137851e45bc009785de89ada562d9376177c97e37702e39e60c2f1ff","69ee23dd0d215b09907ad30d23f88b7790c93329d1faf31d7835552a10cf7cbf","44b8b584a338b190a59f4f6929d072431950c7bd92ec2694821c11bce180c8a5","23b89798789dffbd437c0c423f5d02d11f9736aea73d6abf16db4f812ff36eda","223c37f62ce09a3d99e77498acdee7b2705a4ae14552fbdb4093600cd9164f3f",{"version":"970a90f76d4d219ad60819d61f5994514087ba94c985647a3474a5a3d12714ed","affectsGlobalScope":true},"e10177274a35a9d07c825615340b2fcde2f610f53f3fb40269fd196b4288dda6","4c8525f256873c7ba3135338c647eaf0ca7115a1a2805ae2d0056629461186ce","3c13ef48634e7b5012fcf7e8fce7496352c2d779a7201389ca96a2a81ee4314d","5d0a25ec910fa36595f85a67ac992d7a53dd4064a1ba6aea1c9f14ab73a023f2",{"version":"f0900cd5d00fe1263ff41201fb8073dbeb984397e4af3b8002a5c207a30bdc33","affectsGlobalScope":true},{"version":"4c50342e1b65d3bee2ed4ab18f84842d5724ad11083bd666d8705dc7a6079d80","affectsGlobalScope":true},"06d7c42d256f0ce6afe1b2b6cfbc97ab391f29dadb00dd0ae8e8f23f5bc916c3","ec4bd1b200670fb567920db572d6701ed42a9641d09c4ff6869768c8f81b404c","e59a892d87e72733e2a9ca21611b9beb52977be2696c7ba4b216cbbb9a48f5aa",{"version":"da26af7362f53d122283bc69fed862b9a9fe27e01bc6a69d1d682e0e5a4df3e6","affectsGlobalScope":true},"8a300fa9b698845a1f9c41ecbe2c5966634582a8e2020d51abcace9b55aa959e",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"8dbe725f8d237e70310977afcfa011629804d101ebaa0266cafda6b61ad72236","9a2eaab4e54953c6b2ba21f7ac4c2593859da03917011c10a2acd8864e38e7b2","9076b1bdb9973b32aeba348ccd76ca33e2a55b5dd1b60168879e134fbf7ae4d0","3d9235d46b7be56163dc2a30ed53fed476d6734df973b8250c25b844ed0f6869","ad55743b325a084f366043b636de98563811d40edfbf6a0c04f4e0e3bd48d6df","027420c0b6ef9f2dce7e15401eb7df19db6a364a45af000942dee84e95116d6d","8e06d1b753b7b76bfa99e9db95ff93db8132d88742f9c58db1407f591d496703","1843c781e7d4eb2ba2421f93ced2251a1ad2ec1252c4529be7fd3219c477e284","a5e048c705437fea926307bb370661502ae1813f9dff9efa6858a87575aac8d0","61e8d97d995fc12d08b0db14799e91c0a849da510a3f488bbf2ab4ec463ee08e","1fb26be9109c83d34ea36c5acfd376012b0348842f201d5c32b53be4e3f1ef39","2212ea1fc1af917bbc8c7fe6963ce2fb5b54c3ad7df6cd6d5cacf70489ef8b1d","551d6ea124a8ce4a11b8c619363ed8d9db1dee681cb6f0853c66b56d3f98f6e1","3a074d4e6fd4cae678d749dad7a0242fa1bbdaaa7287f27a7506d20723aaa8d1","cc55be68e82167f4103dd4f42849327495793a8775b727c925ee9943d1087c08","3d40f5b15a927f911c1919ed8396f19e431f36412ba38b4af4dcd8abb69439a9","2098691825f5a139440990b374632ab5d5e4df0b06f7a38e6bc62af8afb4f6f0","478c8e7d8206bdf5392f3d459ca95af1b2b0648d61bbb37cb03b77ef8d1bbf3c","2c4f3dc8ac4e5e1d064ac0492dcef017de6207c39ed3cbe49103319d4c87b92b","ee945dca25076506bc4759830980719893a46e9d13aa6b4d44a2f5211e4321b1","aeddcad4a86c626f6ea06a0eb0b2c5b2c5cdba9180e23aae4f701f07b3ad96d5","3ca3f546f0bae658a7ecec0d228161284e207a0dbe2fcb33b67448e97bd7aa71","75586523002e24122788900b04255268ad1eed9bd3e4d97175a88076213ec835","f5b6ddf9710f3d99ab87075aa2d4ae08a86dd50d3f8aa38729a56976a7a78e3c","64f76320a3f54460bd7365f6d72080b398a74a469fde8907d1a3256b3eb35316","91c456b34eb50cdfb74a49776dc2221575b6eecc692265458f3fa638beffd3bc","cab1efe27467e8186479c7ebdb189f198b372394eaa8a827af4b773502d73408","7d2a0ba1297be385a89b5515b88cd31b4a1eeef5236f710166dc1b36b1741e1b",{"version":"20f895cbad339ce2a4cadad33443a6a77e1e82f50a7dffa0c8146f130b8e0c18","affectsGlobalScope":true},"a9fe3e46584350db7e958217addbfee684d99b144afa977d7815c3f0ed31d06e","1cc9877c91ddf56672ca1913afa1249a7bca193dc28b1abc1c499b1f8fcb0269","fb209266413113ffdada072e40c1476466f19e28b81dc46235944b9b2246e0f0","b28ca76a1d7c28bf8cc74145963a5e729213b44d30073567ff8a2a72dc5a911e","091f2e62d3933d5a1ca1978f94ce9f77dd2bd597a252a7c24ac0202144fb0cbe","f16409db6af984258ec916eca20eb67c55c1a66a0a16d3024ce29e2361b6885d","2e2d70810b96fc1b6bc93419458e8b613b2c742d99a222b92ec72de91bb643c3","94fd071c8b6dc94c5acd45ad1520147963792339b0f2a5241dbbd27486a91274","33dc8a7a2cb9b0d7a7424206d7e449c33a10941462e27fbe21c61526c021618c","6f86608fc7780f98895bb87ced7248a78d6564c507543580ac28312fb398d3b7",{"version":"08285e19d4027575064de0b9158afb91b457808e2f158ddd48f92018ea1ddb2e","affectsGlobalScope":true},"02050fd18f29365b2b11936af46de1d3659d39e6bac1c462f8c605a6d075e244","fb3feb0d05a342d3e56846b432c06bd8a315536ef21ee2fa26073db89db2281a","971fc1b434c33c0462ff6b6e0c7755a3fa60310cfe3015369c7f3b5cb150e4ac","03e72d13ef295bc46632b3057e065427fc6284392cdb46d4911112c095899913","a23a2df8704b33829f18d8a203959dc8b6425d13bea9f26c492a11c0869c1f2c","04d1d25b589fb6038eec3457761f8a54d183fb8936bdc30a412cf58e350a6898","1cd1d23361f852c0d55bc83fb91e891bb5781b2f7861441edd148d5e48065112","8f8df5e2dd49215a98017778de466eb879dfcc14853566efe60384fbec22a3be","5e788d1a14a9d5746affbca5fab6fa24239b775912ac8250940180e984f131d8","4ce46463b2009136a9ae86f428ffa75714d2d22d5b77569e35a3cd63c313b2cd","b6d2ddbb8240a795cd3593a06c762cea36dd91760b5b4a0be653c321d64d26e5","d3f9b1fca0388f5fc6323921b4537bc6663669c336199498b1b0bcd9c1795565","22a72f2282b69ef00bc7d6d755af01f04827e702da836983ab58b01078350878","b1ed006070e75bcd8000628405c24e4c22a90efa569720a250e106ac801cb675","c2dc385ed08b401d862fbe0a74975441d2fdb0944696d33c4ccf0b95dedb0fb0","ebbced2909f510df415bb5521f87262856d31c92cceba6d75adaa3c32e46a3cc","7c89007a3368f659d735304f7063d99372a34189d665bc5c19ea269209f60f2c","63819ae69945a979d4de73c85c655c1ee6fd771ac1be474d80d08a4a049e1c20","cf23e6ab563055cf4bb4ecf2336795b9627bf1cf90b3b527421e82862c0d4337","780f8ec3f1267ac08d8aaca0f36f97008948473499dd5a14df024c84008c44a2","eb5afe3d04295bc4ce6f2a3c6f63301a8ee21c8fd80f957b5eaafeaa03577577","ce61014a8b6496be8965bfdd371aee34ca6b1e45f9c34a743015049ce4728ce2","c1f263481be7a415e5e46ed5811795799b7c4201e835f2895477a1191d9add2c","a385060cd665bad7dd16cfd035f27a923723b345b57087de3d80664e9bdc5919","f93f8a5655444dbf4bb32b252b6594be4701a38d23b9ba3d5214a02c15df181b","dedd0f2e91f2dd25aafef5c75fd270a3bb8f6546d6877c9609957730fc262394","a47675b315365616bff3d2e3ad4d5eb4f45941aafed015ee10acbd37037f1ba0","aafeda6b6cd763afbfdf051700853091e276a71b9ac9f4ece448570f38e14015","7ccb53d9c8c6cb660fc24eef6629bf86b9f360f7b5933b123ff985fba25b6339","5213f16f0b65ecc2993d3ed483507c589d69bf727f22cd5d12d0e47d68b9e56a","63590056e51c8e24a7394c86324eec939dd895a8662ecbab24954d5471924da6","86e6b18f53b22391ebb1c86297f461478b570493bf973975b7295b8f7a684310","5de47e429ed189b068d204cb96be99b59c3f17d16a6e198496305a71f105c077","06c690a59357b39b6e22b6436321b9aea57d18f6979b732414aab4c7f7ecb918","5add7978d40f4be1ece62ebd32e4cf799687d8eb200c76ca4e1cf1847d82db97","5788d3cfeac5e77c3162bdae82b21f9d6dbce860dfcacf8ea85366195873b8cf","42e62b272597eaa3b7b7822c4c4ab96fb9897f16dcd0fdab846bd20812be7366","e0c3c1be52a3f99d053b0ec4661fb6df372cbfcfeff1628a6d95f2281ecd0f4d","f0b15b0a6132c40c951b2b91926b393a6477bc0836b439699e04a8131418c488","8be35ecef5d47b31f8c1de92ffe3cc5cd4d02f65ba6ad0d376352f589119a403","de39920c0772fb9805aa4739550f4b81b0b5efc86db81cbd9171a51d9348f46e","e5e34dac998589bd210f9e20704ded4b9b5e5f082ea36feb33d9cabd0adb5b2a","84c1be5aa9f5b821b955ab6b370522d023f82ef8ad11f0cc58f54d24fcdcaed0","2c138146755c5890ae2209bf9cf950be9d40288b5eb9d36a7c81b524be8aa5a6","89ba4fcac6e8a026bc77d11affa96378eda1f3fd99a7117de6bc34483019444b","00b194b550eb6c9d0b1a28362e0fea439db0d1a0e82c695a4aca63f01568d7f2","8a135c6d6ae079937afbc51048f0a268f0637c2776f6dd2219382d140ccf2fd2","ba875e33c11816dce55040d05b86580f6b58b00cca6f4c41282deef16a55e005","3c90bdf3685eb6725c6a6f7ea47af4bb41e7f7056462f29e4465fd8711f1826d","37b83c56f1027e58521409107bd3e0b6fa9790cbf7d5391a40c230a77a77fc7f","a50cb11f885b9b9c46db7014308f0c2cbd489061de58490d821eb00801b2117f","666db7ccd9500a4a6b845cfa30d63801a5ca37fc502a9e85b7f224381bfdfdb5","f8e75a851f779d87ae96ebac89409925df7bbaf1aa3bd3f56258b98a2c8f0af8","736a3775067c232b24446c86cf25ae3b248e10309d66fbb2e19afaa8ba785bfe","d416b77ec4d0f8cbfd517d8a59ca136bc57735450271a23c14a38a8f9db721a2","f4c9cfa0db2efeac963f5330207272d1aba30ed6a751ce80d0219507c9779749","87072f1e3a57191545e94311d5b5f3f743bd76de41e79e8ae17e0b2214f16243","812754b148ba99a547ee11145b68c35052cc9d5ca827288719d6fb57f009b8aa","89f4c4dd4fd3a1e3c0f77beac796aed80bb9718a9ae25263c7661f21a7311723","6d8ac440eb0bbf214fd29c3a48850e57e63c1cbc31abc9357bba3c882467566c","d058c9af57e5de80321e1c92a55fc2d5f70012a817c8916bd2ac8c44e18c1436","10ee4b41b1b97a5667e64cfde6d2b3b6d15ae17793aa67e2cfa86881f7fe9c71","20ff70ed2dc0cc6717a76264809bfbc1daf4c191e7d5c8d15b3ab489fa653c53","09269729f1952708f5b0d688839ec140628d1f539750a6d9a026330c65b1d231","5813f1ac1f61bf1b81bd41baf959360dd20310fa97322aaf4429434ee89ecc7e","8c1888cd02bc1f3c34bdbfe053c08a2f8d5ca5147be292f099148bfa95a195af","8407a0d9ada016e6c8dc317dd0c72dbb894c950ac3573787d9bd3986e6c51bd7","54366974e39593fdea03c3bf895e611a19b8ede534c5139df2c47daf4304837e","adee9f9a7594c93efdf2fcee7a10ca63a7ffca096c205148b72c9fc3bdd9b19a","616338e71bac435feb9b4c606e53d4fd9e5911aca773c37edfa5663fb57002ad","aef20777734d4bc23a58c26f68c42bcd3e56b82ddd231f993f0567531c752e0f","cc52c40b782d451ceca219cc62c3aacfc91e08ccea2a4bb839f4cbc538a8caff","31df0ed3b1fe57e17dea70384eea62bd607a7dc69b29ce6c4df6e5c456d87387","e3a6aaefbfde2999fd38ee76b43c82215f59b52f3e3f2accee1ef0f5c4ffa9cc","7f44474d11122bcb3ee5feaca842b84499c13a5fd97469cabcb2dc942c4bf043","bc9ddaee0c1385e0b7f00c6fce65b4c2de959ef52c59c5b91d1de3234a3f8a01","84473706d793d798fdfc39957ac33fe8648c061b4b7f0f7efb400ce35d6e5ca7","c8be5bb9c2285ac7507b21c426985afe72f2633ede6e9ad27bb66288aecfeb92","bff46197eaf1c08125258f3152bd3e4a9e13289f0bbc88f1e9ef7e175ca1f8c5","7204921408eaed62e573211f904c6e0c461721134959d5a002320f73ed4f84c2",{"version":"eab2c8a2cb84c2940c93111b98ff13d88e314d48c0f6fc0ed8943e573ec59431","affectsGlobalScope":true},"0f1978d39f8f326e675c9d12e3c528edc8b99bb751c71f32d9b283c53e0ee5dd",{"version":"2c6c2fe3b290d4e83434b474e15d6cdfdd9147102ae3d50c5e7dc8acf97cd486","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f745a4a7d3e173c51d54a0cdaced86ec2ad7e92b31b2f55b232a3dad32a55757","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f42bcb0b57bc4d87c2e7ee29b9e8573b342f3ffd66b67ee191e34e649223a762","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"58273ce311834d501cb5e356465485949b928bad127e2b594308c4697b140120","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"40a97b3b274c4b2885b135e5ccaaf289aa07dd26f3d2f616cc30984c4d939aba","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cc88d87ab48a8b04bc217612ea91c8c77e02633e53aca6b1d4ef12bb2ec7e9d7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f666c963eb9cede06852fc4b098eb35fd96cd1709096bc3fc39deecd887775f7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ea795d3a8c384fa0e45871797720a86937a40829df2121ee22bcff1789f8ee2f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"61924354678d0dccd7182741324d57859bca9c29f942dcf1fe16fd81d7639329","signature":"d8b6612ffd8100484208679301bd0742c4e34b5a579f28f28cfcbff582c167c0"},{"version":"7e5974860627a3d219b0624952633002bb0601d7984c2b5a6b33fc7a9bc2d7a0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"56b93a1bae0e1acf844c2e1c90b18f1e30d3c19e9b57bba39f7878040a9bdc9b","signature":"2929d8318c9e3e8bcbebddee4bbd55f377d2b18fcb058f51965cb85be0f24b12"},"d2e64a6f25013b099e83bfadb2c388d7bef3e8f3fdb25528225bbc841e7e7e3a","e01ea380015ed698c3c0e2ccd0db72f3fc3ef1abc4519f122aa1c1a8d419a505","9e2534be8a9338e750d24acc6076680d49b1643ae993c74510776a92af0c1604","c266ab6ddc3c29b4dd77a6f0d0eb4df7dab76d066e385171d88cb97c1120e747",{"version":"adfef42f90e895874d5d01ae1c765ab3fb893ed71226960d8f669dbc391554e8","affectsGlobalScope":true},{"version":"2e4beadc98cf82611bc639cb48784883493648b25e097a56a8390edf1fe01548","affectsGlobalScope":true},{"version":"0e9864c8c9393c22cba58ad144b7f2a190db89f649d0c064f22604fb9ddbaea0","affectsGlobalScope":true},{"version":"c67a7b7eec0175ea53343429d32897fcad406c663ba4b775eab8be8164bff91c","affectsGlobalScope":true},"64518924a9b6453b89e3adb27e5605a4a5ea302112172b49d32a4eb468fc02fb",{"version":"639e82ad014ff342469c80e3814d0a33fa98911296fa7941474c81bcc0f9b9ac","affectsGlobalScope":true},"d8aab31ba8e618cc3eea10b0945de81cb93b7e8150a013a482332263b9305322","5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","7adecb2c3238794c378d336a8182d4c3dd2c4fa6fa1785e2797a3db550edea62","dc12dc0e5aa06f4e1a7692149b78f89116af823b9e1f1e4eae140cd3e0e674e6","1bfc6565b90c8771615cd8cfcf9b36efc0275e5e83ac7d9181307e96eb495161","8a8a96898906f065f296665e411f51010b51372fa260d5373bf9f64356703190","7f82ef88bdb67d9a850dd1c7cd2d690f33e0f0acd208e3c9eba086f3670d4f73",{"version":"ccfd8774cd9b929f63ff7dcf657977eb0652e3547f1fcac1b3a1dc5db22d4d58","affectsGlobalScope":true}],"options":{"declaration":true,"emitDeclarationOnly":true,"esModuleInterop":true,"jsx":2,"noFallthroughCasesInSwitch":true,"noImplicitReturns":true,"noUncheckedIndexedAccess":true,"noUnusedLocals":true,"noUnusedParameters":true,"outDir":"./types","rootDir":"../src","skipLibCheck":true,"strict":true,"target":2},"fileIdsList":[[227],[129,227],[127,227],[124,125,126,127,128,131,132,133,134,135,136,137,138,227],[123,227],[130,227],[124,125,126,227],[124,125,227],[127,128,130,227],[125,227],[67,122,139,140,227],[130,227,383],[227,377,379],[227,377,379,380,381],[143,227,382],[181,227],[184,227],[185,190,218,227],[186,197,198,205,215,226,227],[186,187,197,205,227],[188,227],[189,190,198,206,227],[190,215,223,227],[191,193,197,205,227],[192,227],[193,194,227],[197,227],[195,197,227],[197,198,199,215,226,227],[197,198,199,212,215,218,227],[227,231],[193,200,205,215,226,227],[197,198,200,201,205,215,223,226,227],[200,202,215,223,226,227],[181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233],[197,203,227],[204,226,227],[193,197,205,215,227],[206,227],[207,227],[184,208,227],[209,225,227,231],[210,227],[211,227],[197,212,213,227],[212,214,227,229],[185,197,215,216,217,218,227],[185,215,217,227],[215,216,227],[218,227],[219,227],[197,221,222,227],[221,222,227],[190,205,215,223,227],[224,227],[205,225,227],[185,200,211,226,227],[190,227],[215,227,228],[227,229],[227,230],[185,190,197,199,208,215,226,227,229,231],[215,227,232],[69,227],[64,65,66,68,227],[69,140,227],[63,64,65,66,227],[197,200,202,215,223,226,227,232,234],[227,367,368],[227,367,368,369],[227,371],[160,197,227,375],[227,374],[161,227],[161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,227],[157,227],[155,156,227],[158,197,198,227],[153,154,157,158,227],[157,197,227],[152,153,154,155,156,157,158,159,227],[160,227,370],[149,150,227,355,371],[142,148,227],[143,147,227],[146,227],[145,227],[144,227],[57,227],[58,59,62,227],[58,227],[57,58,227],[58,59,60,227],[227,272],[179,180,227,235],[179,180,227,236],[177,178,227,238],[160,227,238],[160,179,180,227,237,238,239,240,241],[160,197,226,227],[160,177,179,197,227,237],[160,178,197,227,238],[227,261],[227,245,246,252,253,254,255,256,257,258,259,260,264,265,266,267,268,269,270,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299],[177,227,354],[227,354],[227,251],[227,242],[227,261,262,263],[177,227,242],[227,271,274],[177,227],[227,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352],[227,245],[227,246],[227,299],[160,227,242,243,354],[160,177,197,227,242,244,249,251,254,271,274,300,353],[227,247,248,249,250],[227,247],[177,227,247,354],[227,247,354],[160,227,242,271,273,354],[58,62,67,72,83,97,99,100,102,104,105,107,108,109,110,111,227],[56,67,82,84,227],[58,62,67,90,97,98,112,227],[67,80,94,227],[58,67,90,97,103,227],[62,67,82,90,97,227],[58,62,67,70,72,74,75,97,98,103,104,106,112,227],[58,62,67,227],[58,67,102,227],[62,67,72,73,74,75,76,79,95,96,114,227],[67,90,97,113,227],[55,67,90,97,227],[58,62,67,70,90,112,227],[67,90,97,227],[67,83,90,97,227],[67,83,227],[62,121,150,227],[60,77,121,141,150,227,355],[58,60,62,67,74,82,83,84,121,150,227],[58,60,67,74,121,150,227],[60,62,67,74,118,119,121,141,150,227],[58,60,62,121,141,150,227],[58,60,62,67,74,82,86,114,115,119,121,141,150,227],[67,90,227],[62,67,71,227],[60,67,227],[67,227],[67,74,227],[67,81,96,141,227],[56,58,62,67,77,84,227],[62,227],[58,62,77,227],[58,62,78,83,227],[58,62,77,78,227],[60,62,67,72,73,82,96,141,227],[56,58,60,62,67,70,87,90,92,93,94,227],[62,67,72,81,227],[62,67,72,82,227],[60,62,67,71,72,82,227],[60,67,73,227],[67,80,227],[58,62,67,72,90,97,227],[67,72,86,227],[62,67,82,106,116,227],[74,82,83,84,86,114,115,116,117,118,119,227],[58,60,86,227],[58,60,62,83,85,86,227],[60,62,70,227],[58,60,227],[67,101,227],[55,56,60,62,88,89,91,227],[56,58,60,62,90,227],[55,56,60,62,90,227],[56,58,60,62,67,82,86,114,115,121,141,149,150,227],[55,56,58,62,78,89,227],[58,62,67],[67,83],[58,62,67,90],[67],[58,67],[62,67],[62,67,74,95],[67,113],[67,90],[62,67,71],[60,67],[67,74],[56,58,62,67,77,84],[62],[58,62,77],[58,62],[60,62,71],[56,60,62,67,71,90],[62,71],[60],[58],[74,82,83,84,86,114,115,116,117,118,119],[60,62],[58,60],[64,67],[56,62],[58,60,62],[56,58,62,115,121,149,150],[56,58,62]],"referencedMap":[[144,1],[130,2],[129,1],[137,1],[134,1],[133,1],[128,3],[139,4],[124,5],[135,6],[127,7],[126,8],[136,1],[131,9],[138,1],[132,10],[125,1],[141,11],[123,1],[384,12],[377,1],[380,13],[382,14],[381,13],[379,6],[383,15],[378,2],[374,1],[181,16],[182,16],[184,17],[185,18],[186,19],[187,20],[188,21],[189,22],[190,23],[191,24],[192,25],[193,26],[194,26],[196,27],[195,28],[197,27],[198,29],[199,30],[183,31],[233,1],[200,32],[201,33],[202,34],[234,35],[203,36],[204,37],[205,38],[206,39],[207,40],[208,41],[209,42],[210,43],[211,44],[212,45],[213,45],[214,46],[215,47],[217,48],[216,49],[218,50],[219,51],[220,1],[221,52],[222,53],[223,54],[224,55],[225,56],[226,57],[227,58],[228,59],[229,60],[230,61],[231,62],[232,63],[65,1],[122,64],[70,64],[68,1],[69,65],[140,66],[63,1],[67,67],[66,1],[261,1],[235,68],[367,1],[368,1],[369,69],[370,70],[373,71],[376,72],[375,73],[162,74],[163,74],[164,1],[165,1],[166,74],[167,74],[168,74],[177,75],[172,1],[173,1],[174,1],[171,1],[175,1],[176,1],[169,1],[170,1],[161,1],[152,1],[158,76],[153,1],[159,1],[157,77],[156,78],[155,79],[154,80],[160,81],[143,1],[101,1],[64,1],[371,82],[372,83],[149,84],[142,1],[148,85],[147,86],[57,1],[146,87],[145,88],[58,89],[60,90],[59,91],[121,92],[77,91],[62,93],[61,91],[10,1],[12,1],[11,1],[2,1],[13,1],[14,1],[15,1],[16,1],[17,1],[18,1],[19,1],[20,1],[3,1],[4,1],[24,1],[21,1],[22,1],[23,1],[25,1],[26,1],[27,1],[5,1],[28,1],[29,1],[30,1],[31,1],[6,1],[35,1],[32,1],[33,1],[34,1],[36,1],[7,1],[37,1],[42,1],[43,1],[38,1],[39,1],[40,1],[41,1],[8,1],[47,1],[44,1],[45,1],[46,1],[48,1],[9,1],[49,1],[50,1],[51,1],[52,1],[53,1],[1,1],[54,1],[273,94],[272,1],[236,95],[237,96],[179,1],[180,1],[239,97],[240,98],[242,99],[178,100],[238,101],[241,102],[262,103],[300,104],[245,105],[246,106],[252,107],[253,107],[254,1],[255,1],[256,1],[257,1],[258,1],[259,108],[260,1],[264,109],[265,1],[266,1],[267,110],[268,1],[269,1],[270,1],[275,111],[276,1],[277,1],[278,106],[279,1],[280,106],[281,106],[282,1],[283,106],[284,1],[285,1],[286,1],[287,1],[288,112],[289,112],[290,1],[291,1],[292,1],[293,106],[294,1],[295,106],[296,106],[297,1],[298,1],[299,106],[353,113],[301,114],[302,115],[303,1],[304,1],[305,106],[306,1],[307,1],[308,1],[309,106],[310,1],[311,1],[312,1],[313,106],[315,1],[314,1],[316,106],[317,1],[318,112],[319,1],[320,112],[321,1],[322,1],[323,1],[324,1],[325,1],[326,1],[327,1],[328,1],[329,1],[330,1],[331,1],[332,106],[333,106],[334,106],[335,106],[336,106],[337,106],[338,1],[339,1],[340,1],[341,1],[342,1],[343,1],[344,106],[345,106],[346,106],[347,106],[348,106],[349,106],[350,106],[351,106],[352,116],[263,1],[244,108],[355,117],[354,118],[243,1],[247,1],[251,119],[248,120],[249,121],[250,122],[274,123],[271,110],[55,1],[112,124],[85,125],[113,126],[96,127],[104,128],[105,129],[107,130],[74,131],[103,132],[115,133],[114,134],[108,135],[109,136],[110,137],[111,138],[84,139],[151,140],[356,141],[357,142],[358,143],[359,144],[360,145],[361,146],[97,147],[72,148],[73,149],[80,150],[75,151],[106,150],[362,152],[83,153],[76,154],[78,155],[99,156],[79,157],[56,1],[363,158],[93,148],[95,159],[82,160],[116,161],[117,162],[118,163],[94,150],[81,164],[98,165],[100,166],[119,167],[120,168],[365,169],[87,170],[71,171],[364,171],[86,172],[102,173],[92,174],[88,154],[91,175],[89,176],[150,177],[366,1],[90,178]],"exportedModulesMap":[[144,1],[130,2],[129,1],[137,1],[134,1],[133,1],[128,3],[139,4],[124,5],[135,6],[127,7],[126,8],[136,1],[131,9],[138,1],[132,10],[125,1],[141,11],[123,1],[384,12],[377,1],[380,13],[382,14],[381,13],[379,6],[383,15],[378,2],[374,1],[181,16],[182,16],[184,17],[185,18],[186,19],[187,20],[188,21],[189,22],[190,23],[191,24],[192,25],[193,26],[194,26],[196,27],[195,28],[197,27],[198,29],[199,30],[183,31],[233,1],[200,32],[201,33],[202,34],[234,35],[203,36],[204,37],[205,38],[206,39],[207,40],[208,41],[209,42],[210,43],[211,44],[212,45],[213,45],[214,46],[215,47],[217,48],[216,49],[218,50],[219,51],[220,1],[221,52],[222,53],[223,54],[224,55],[225,56],[226,57],[227,58],[228,59],[229,60],[230,61],[231,62],[232,63],[65,1],[122,64],[70,64],[68,1],[69,65],[140,66],[63,1],[67,67],[66,1],[261,1],[235,68],[367,1],[368,1],[369,69],[370,70],[373,71],[376,72],[375,73],[162,74],[163,74],[164,1],[165,1],[166,74],[167,74],[168,74],[177,75],[172,1],[173,1],[174,1],[171,1],[175,1],[176,1],[169,1],[170,1],[161,1],[152,1],[158,76],[153,1],[159,1],[157,77],[156,78],[155,79],[154,80],[160,81],[143,1],[101,1],[64,1],[371,82],[372,83],[149,84],[142,1],[148,85],[147,86],[57,1],[146,87],[145,88],[58,89],[60,90],[59,91],[121,92],[77,91],[62,93],[61,91],[10,1],[12,1],[11,1],[2,1],[13,1],[14,1],[15,1],[16,1],[17,1],[18,1],[19,1],[20,1],[3,1],[4,1],[24,1],[21,1],[22,1],[23,1],[25,1],[26,1],[27,1],[5,1],[28,1],[29,1],[30,1],[31,1],[6,1],[35,1],[32,1],[33,1],[34,1],[36,1],[7,1],[37,1],[42,1],[43,1],[38,1],[39,1],[40,1],[41,1],[8,1],[47,1],[44,1],[45,1],[46,1],[48,1],[9,1],[49,1],[50,1],[51,1],[52,1],[53,1],[1,1],[54,1],[273,94],[272,1],[236,95],[237,96],[179,1],[180,1],[239,97],[240,98],[242,99],[178,100],[238,101],[241,102],[262,103],[300,104],[245,105],[246,106],[252,107],[253,107],[254,1],[255,1],[256,1],[257,1],[258,1],[259,108],[260,1],[264,109],[265,1],[266,1],[267,110],[268,1],[269,1],[270,1],[275,111],[276,1],[277,1],[278,106],[279,1],[280,106],[281,106],[282,1],[283,106],[284,1],[285,1],[286,1],[287,1],[288,112],[289,112],[290,1],[291,1],[292,1],[293,106],[294,1],[295,106],[296,106],[297,1],[298,1],[299,106],[353,113],[301,114],[302,115],[303,1],[304,1],[305,106],[306,1],[307,1],[308,1],[309,106],[310,1],[311,1],[312,1],[313,106],[315,1],[314,1],[316,106],[317,1],[318,112],[319,1],[320,112],[321,1],[322,1],[323,1],[324,1],[325,1],[326,1],[327,1],[328,1],[329,1],[330,1],[331,1],[332,106],[333,106],[334,106],[335,106],[336,106],[337,106],[338,1],[339,1],[340,1],[341,1],[342,1],[343,1],[344,106],[345,106],[346,106],[347,106],[348,106],[349,106],[350,106],[351,106],[352,116],[263,1],[244,108],[355,117],[354,118],[243,1],[247,1],[251,119],[248,120],[249,121],[250,122],[274,123],[271,110],[112,179],[85,180],[113,181],[96,182],[104,183],[105,184],[107,179],[74,179],[103,183],[115,185],[114,186],[108,182],[109,181],[110,182],[111,180],[84,180],[97,187],[72,188],[73,189],[80,182],[75,190],[106,182],[83,191],[76,192],[78,193],[99,194],[79,192],[93,195],[95,196],[82,184],[116,192],[117,197],[118,198],[81,182],[98,181],[100,199],[119,192],[120,200],[87,201],[71,201],[364,201],[86,202],[102,203],[92,204],[88,192],[91,205],[89,201],[150,206],[90,207]],"semanticDiagnosticsPerFile":[144,130,129,137,134,133,128,139,124,135,127,126,136,131,138,132,125,141,123,384,377,380,382,381,379,383,378,374,181,182,184,185,186,187,188,189,190,191,192,193,194,196,195,197,198,199,183,233,200,201,202,234,203,204,205,206,207,208,209,210,211,212,213,214,215,217,216,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,65,122,70,68,69,140,63,67,66,261,235,367,368,369,370,373,376,375,162,163,164,165,166,167,168,177,172,173,174,171,175,176,169,170,161,152,158,153,159,157,156,155,154,160,143,101,64,371,372,149,142,148,147,57,146,145,58,60,59,121,77,62,61,10,12,11,2,13,14,15,16,17,18,19,20,3,4,24,21,22,23,25,26,27,5,28,29,30,31,6,35,32,33,34,36,7,37,42,43,38,39,40,41,8,47,44,45,46,48,9,49,50,51,52,53,1,54,273,272,236,237,179,180,239,240,242,178,238,241,262,300,245,246,252,253,254,255,256,257,258,259,260,264,265,266,267,268,269,270,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,353,301,302,303,304,305,306,307,308,309,310,311,312,313,315,314,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,263,244,355,354,243,247,251,248,249,250,274,271,55,112,85,113,96,104,105,107,74,103,115,114,108,109,110,111,84,151,356,357,358,359,360,361,97,72,73,80,75,106,362,83,76,78,99,79,56,363,93,95,82,116,117,118,94,81,98,100,119,120,365,87,71,364,86,102,92,88,91,89,150,366,90]},"version":"4.9.5"} \ No newline at end of file diff --git a/dist/types/browser.d.ts b/dist/types/browser.d.ts new file mode 100644 index 00000000..75bb3d81 --- /dev/null +++ b/dist/types/browser.d.ts @@ -0,0 +1,15 @@ +export declare const browser: { + ie: boolean; + ie_version: any; + gecko: boolean; + gecko_version: number | false; + chrome: boolean; + chrome_version: number; + safari: boolean; + ios: boolean; + mac: boolean; + windows: boolean; + android: boolean; + webkit: boolean; + webkit_version: number; +}; diff --git a/dist/types/components/ChildNodeViews.d.ts b/dist/types/components/ChildNodeViews.d.ts new file mode 100644 index 00000000..4be0de57 --- /dev/null +++ b/dist/types/components/ChildNodeViews.d.ts @@ -0,0 +1,9 @@ +import { Node } from "prosemirror-model"; +import { Decoration, DecorationSource } from "prosemirror-view"; +import React, { MutableRefObject } from "react"; +export declare function wrapInDeco(reactNode: JSX.Element | string, deco: Decoration): React.DetailedReactHTMLElement, HTMLElement> | React.FunctionComponentElement; +export declare const ChildNodeViews: React.NamedExoticComponent<{ + getPos: MutableRefObject<() => number>; + node: Node | undefined; + innerDecorations: DecorationSource; +}>; diff --git a/dist/types/components/CursorWrapper.d.ts b/dist/types/components/CursorWrapper.d.ts new file mode 100644 index 00000000..250abd85 --- /dev/null +++ b/dist/types/components/CursorWrapper.d.ts @@ -0,0 +1,5 @@ +import React from "react"; +export declare const CursorWrapper: React.ForwardRefExoticComponent<{ + widget: import("../decorations/ReactWidgetType.js").ReactWidgetDecoration; + getPos: () => number; +} & React.HTMLAttributes & React.RefAttributes>; diff --git a/dist/types/components/DocNodeView.d.ts b/dist/types/components/DocNodeView.d.ts new file mode 100644 index 00000000..364b6f09 --- /dev/null +++ b/dist/types/components/DocNodeView.d.ts @@ -0,0 +1,20 @@ +import { Node } from "prosemirror-model"; +import { Decoration, DecorationSource } from "prosemirror-view"; +import React, { DetailedHTMLProps, HTMLAttributes, ReactElement } from "react"; +import { NodeViewDesc } from "../viewdesc.js"; +export type DocNodeViewProps = { + className?: string; + node: Node | undefined; + innerDeco: DecorationSource; + outerDeco: Decoration[]; + as?: ReactElement; + viewDesc?: NodeViewDesc; +} & Omit, HTMLDivElement>, "ref">; +export declare const DocNodeView: React.MemoExoticComponent> | undefined; + viewDesc?: NodeViewDesc | undefined; +} & Omit, HTMLDivElement>, "ref"> & React.RefAttributes>>; diff --git a/dist/types/components/LayoutGroup.d.ts b/dist/types/components/LayoutGroup.d.ts new file mode 100644 index 00000000..037ac0a3 --- /dev/null +++ b/dist/types/components/LayoutGroup.d.ts @@ -0,0 +1,12 @@ +import React from "react"; +export interface LayoutGroupProps { + children: React.ReactNode; +} +/** + * Provides a boundary for grouping layout effects. + * + * Descendant components can invoke the `useLayoutGroupEffect` hook to register + * effects that run after all descendants within the group have processed their + * regular layout effects. + */ +export declare function LayoutGroup({ children }: LayoutGroupProps): JSX.Element; diff --git a/dist/types/components/MarkView.d.ts b/dist/types/components/MarkView.d.ts new file mode 100644 index 00000000..f8fd5660 --- /dev/null +++ b/dist/types/components/MarkView.d.ts @@ -0,0 +1,9 @@ +import { Mark } from "prosemirror-model"; +import React, { MutableRefObject, ReactNode } from "react"; +type Props = { + mark: Mark; + getPos: MutableRefObject<() => number>; + children: ReactNode; +}; +export declare const MarkView: React.MemoExoticComponent>>; +export {}; diff --git a/dist/types/components/NativeWidgetView.d.ts b/dist/types/components/NativeWidgetView.d.ts new file mode 100644 index 00000000..8320d4f8 --- /dev/null +++ b/dist/types/components/NativeWidgetView.d.ts @@ -0,0 +1,8 @@ +import { Decoration } from "prosemirror-view"; +import { MutableRefObject } from "react"; +type Props = { + widget: Decoration; + getPos: MutableRefObject<() => number>; +}; +export declare function NativeWidgetView({ widget, getPos }: Props): JSX.Element; +export {}; diff --git a/dist/types/components/NodeView.d.ts b/dist/types/components/NodeView.d.ts new file mode 100644 index 00000000..ebaa583c --- /dev/null +++ b/dist/types/components/NodeView.d.ts @@ -0,0 +1,11 @@ +import { Node } from "prosemirror-model"; +import { Decoration, DecorationSource } from "prosemirror-view"; +import React, { MutableRefObject } from "react"; +type NodeViewProps = { + outerDeco: readonly Decoration[]; + getPos: MutableRefObject<() => number>; + node: Node; + innerDeco: DecorationSource; +}; +export declare const NodeView: React.NamedExoticComponent; +export {}; diff --git a/dist/types/components/NodeViewComponentProps.d.ts b/dist/types/components/NodeViewComponentProps.d.ts new file mode 100644 index 00000000..e30d4a9e --- /dev/null +++ b/dist/types/components/NodeViewComponentProps.d.ts @@ -0,0 +1,13 @@ +import { Node } from "prosemirror-model"; +import { Decoration, DecorationSource } from "prosemirror-view"; +import { HTMLAttributes, ReactNode } from "react"; +export type NodeViewComponentProps = { + nodeProps: { + decorations: readonly Decoration[]; + innerDecorations: DecorationSource; + node: Node; + children?: ReactNode | ReactNode[]; + isSelected: boolean; + getPos: () => number; + }; +} & HTMLAttributes; diff --git a/dist/types/components/OutputSpec.d.ts b/dist/types/components/OutputSpec.d.ts new file mode 100644 index 00000000..4f320188 --- /dev/null +++ b/dist/types/components/OutputSpec.d.ts @@ -0,0 +1,8 @@ +import { DOMOutputSpec } from "prosemirror-model"; +import React, { HTMLProps, ReactNode } from "react"; +type Props = HTMLProps & { + outputSpec: DOMOutputSpec; + children?: ReactNode; +}; +declare const ForwardedOutputSpec: React.MemoExoticComponent & React.RefAttributes>>; +export { ForwardedOutputSpec as OutputSpec }; diff --git a/dist/types/components/ProseMirror.d.ts b/dist/types/components/ProseMirror.d.ts new file mode 100644 index 00000000..b70429a1 --- /dev/null +++ b/dist/types/components/ProseMirror.d.ts @@ -0,0 +1,15 @@ +import { NodeViewConstructor } from "prosemirror-view"; +import { ForwardRefExoticComponent, ReactNode, RefAttributes } from "react"; +import { UseEditorOptions } from "../hooks/useEditor.js"; +import { NodeViewComponentProps } from "./NodeViewComponentProps.js"; +export type Props = Omit & { + className?: string; + children?: ReactNode; + nodeViews?: { + [nodeType: string]: ForwardRefExoticComponent>; + }; + customNodeViews?: { + [nodeType: string]: NodeViewConstructor; + }; +}; +export declare function ProseMirror(props: Props): JSX.Element; diff --git a/dist/types/components/ProseMirrorDoc.d.ts b/dist/types/components/ProseMirrorDoc.d.ts new file mode 100644 index 00000000..3dbd7b0d --- /dev/null +++ b/dist/types/components/ProseMirrorDoc.d.ts @@ -0,0 +1,10 @@ +import React from "react"; +import { DocNodeViewProps } from "./DocNodeView.js"; +type DocNodeViewContextValue = Omit & { + setMount: (mount: HTMLElement | null) => void; +}; +export declare const DocNodeViewContext: React.Context; +declare const ForwardedProseMirrorDoc: React.ForwardRefExoticComponent<{ + as?: React.ReactElement> | undefined; +} & Omit, HTMLDivElement>, "ref"> & React.RefAttributes>; +export { ForwardedProseMirrorDoc as ProseMirrorDoc }; diff --git a/dist/types/components/SeparatorHackView.d.ts b/dist/types/components/SeparatorHackView.d.ts new file mode 100644 index 00000000..06ea07ec --- /dev/null +++ b/dist/types/components/SeparatorHackView.d.ts @@ -0,0 +1,6 @@ +import { MutableRefObject } from "react"; +type Props = { + getPos: MutableRefObject<() => number>; +}; +export declare function SeparatorHackView({ getPos }: Props): JSX.Element | null; +export {}; diff --git a/dist/types/components/TextNodeView.d.ts b/dist/types/components/TextNodeView.d.ts new file mode 100644 index 00000000..6ae3369b --- /dev/null +++ b/dist/types/components/TextNodeView.d.ts @@ -0,0 +1,23 @@ +import { Node } from "prosemirror-model"; +import { Decoration, EditorView } from "prosemirror-view"; +import { Component, MutableRefObject } from "react"; +import { ViewDesc } from "../viewdesc.js"; +type Props = { + view: EditorView | null; + node: Node; + getPos: MutableRefObject<() => number>; + siblingsRef: MutableRefObject; + parentRef: MutableRefObject; + decorations: readonly Decoration[]; +}; +export declare class TextNodeView extends Component { + private viewDescRef; + private renderRef; + updateEffect(): void; + shouldComponentUpdate(nextProps: Props): boolean; + componentDidMount(): void; + componentDidUpdate(): void; + componentWillUnmount(): void; + render(): JSX.Element | null; +} +export {}; diff --git a/dist/types/components/TrailingHackView.d.ts b/dist/types/components/TrailingHackView.d.ts new file mode 100644 index 00000000..4c71ede1 --- /dev/null +++ b/dist/types/components/TrailingHackView.d.ts @@ -0,0 +1,6 @@ +import { MutableRefObject } from "react"; +type Props = { + getPos: MutableRefObject<() => number>; +}; +export declare function TrailingHackView({ getPos }: Props): JSX.Element; +export {}; diff --git a/dist/types/components/WidgetView.d.ts b/dist/types/components/WidgetView.d.ts new file mode 100644 index 00000000..40904b59 --- /dev/null +++ b/dist/types/components/WidgetView.d.ts @@ -0,0 +1,8 @@ +import { MutableRefObject } from "react"; +import { ReactWidgetDecoration } from "../decorations/ReactWidgetType.js"; +type Props = { + widget: ReactWidgetDecoration; + getPos: MutableRefObject<() => number>; +}; +export declare function WidgetView({ widget, getPos }: Props): JSX.Element; +export {}; diff --git a/dist/types/components/WidgetViewComponentProps.d.ts b/dist/types/components/WidgetViewComponentProps.d.ts new file mode 100644 index 00000000..053c278d --- /dev/null +++ b/dist/types/components/WidgetViewComponentProps.d.ts @@ -0,0 +1,6 @@ +import { HTMLAttributes } from "react"; +import { ReactWidgetDecoration } from "../decorations/ReactWidgetType.js"; +export type WidgetViewComponentProps = { + widget: ReactWidgetDecoration; + getPos: () => number; +} & HTMLAttributes; diff --git a/dist/types/components/__tests__/ProseMirror.composition.test.d.ts b/dist/types/components/__tests__/ProseMirror.composition.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/components/__tests__/ProseMirror.composition.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/components/__tests__/ProseMirror.domchange.test.d.ts b/dist/types/components/__tests__/ProseMirror.domchange.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/components/__tests__/ProseMirror.domchange.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/components/__tests__/ProseMirror.draw-decoration.test.d.ts b/dist/types/components/__tests__/ProseMirror.draw-decoration.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/components/__tests__/ProseMirror.draw-decoration.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/components/__tests__/ProseMirror.draw.test.d.ts b/dist/types/components/__tests__/ProseMirror.draw.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/components/__tests__/ProseMirror.draw.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/components/__tests__/ProseMirror.node-view.test.d.ts b/dist/types/components/__tests__/ProseMirror.node-view.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/components/__tests__/ProseMirror.node-view.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/components/__tests__/ProseMirror.selection.test.d.ts b/dist/types/components/__tests__/ProseMirror.selection.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/components/__tests__/ProseMirror.selection.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/components/__tests__/ProseMirror.test.d.ts b/dist/types/components/__tests__/ProseMirror.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/components/__tests__/ProseMirror.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/contexts/ChildDescriptorsContext.d.ts b/dist/types/contexts/ChildDescriptorsContext.d.ts new file mode 100644 index 00000000..7c67c436 --- /dev/null +++ b/dist/types/contexts/ChildDescriptorsContext.d.ts @@ -0,0 +1,6 @@ +import { MutableRefObject } from "react"; +import { ViewDesc } from "../viewdesc.js"; +export declare const ChildDescriptorsContext: import("react").Context<{ + parentRef: MutableRefObject; + siblingsRef: MutableRefObject; +}>; diff --git a/dist/types/contexts/EditorContext.d.ts b/dist/types/contexts/EditorContext.d.ts new file mode 100644 index 00000000..33464f46 --- /dev/null +++ b/dist/types/contexts/EditorContext.d.ts @@ -0,0 +1,14 @@ +import type { DOMEventMap, EditorView } from "prosemirror-view"; +import type { EventHandler } from "../plugins/componentEventListeners"; +export interface EditorContextValue { + view: EditorView | null; + registerEventListener(eventType: EventType, handler: EventHandler): void; + unregisterEventListener(eventType: EventType, handler: EventHandler): void; +} +/** + * Provides the EditorView, as well as the current + * EditorState. Should not be consumed directly; instead + * see `useEditorState`, `useEditorViewEvent`, and + * `useEditorViewLayoutEffect`. + */ +export declare const EditorContext: import("react").Context; diff --git a/dist/types/contexts/EditorStateContext.d.ts b/dist/types/contexts/EditorStateContext.d.ts new file mode 100644 index 00000000..53c00559 --- /dev/null +++ b/dist/types/contexts/EditorStateContext.d.ts @@ -0,0 +1,2 @@ +import { EditorState } from "prosemirror-state"; +export declare const EditorStateContext: import("react").Context; diff --git a/dist/types/contexts/LayoutGroupContext.d.ts b/dist/types/contexts/LayoutGroupContext.d.ts new file mode 100644 index 00000000..f1db05ac --- /dev/null +++ b/dist/types/contexts/LayoutGroupContext.d.ts @@ -0,0 +1,5 @@ +import type { EffectCallback } from "react"; +export interface LayoutGroupContextValue { + (effect: EffectCallback): ReturnType; +} +export declare const LayoutGroupContext: import("react").Context; diff --git a/dist/types/contexts/NodeViewContext.d.ts b/dist/types/contexts/NodeViewContext.d.ts new file mode 100644 index 00000000..f411d453 --- /dev/null +++ b/dist/types/contexts/NodeViewContext.d.ts @@ -0,0 +1,6 @@ +import { ForwardRefExoticComponent, RefAttributes } from "react"; +import { NodeViewComponentProps } from "../components/NodeViewComponentProps.js"; +export type NodeViewContextValue = { + nodeViews: Record>>; +}; +export declare const NodeViewContext: import("react").Context; diff --git a/dist/types/contexts/StopEventContext.d.ts b/dist/types/contexts/StopEventContext.d.ts new file mode 100644 index 00000000..640c7e5e --- /dev/null +++ b/dist/types/contexts/StopEventContext.d.ts @@ -0,0 +1,3 @@ +type StopEventContextValue = (stopEvent: (event: Event) => boolean | undefined) => void; +export declare const StopEventContext: import("react").Context; +export {}; diff --git a/dist/types/contexts/__tests__/DeferredLayoutEffects.test.d.ts b/dist/types/contexts/__tests__/DeferredLayoutEffects.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/contexts/__tests__/DeferredLayoutEffects.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/decorations/ReactWidgetType.d.ts b/dist/types/decorations/ReactWidgetType.d.ts new file mode 100644 index 00000000..1c220596 --- /dev/null +++ b/dist/types/decorations/ReactWidgetType.d.ts @@ -0,0 +1,39 @@ +import { Mark } from "prosemirror-model"; +import { Mappable } from "prosemirror-transform"; +import { Decoration } from "prosemirror-view"; +import { ForwardRefExoticComponent, RefAttributes } from "react"; +import { WidgetViewComponentProps } from "../components/WidgetViewComponentProps.js"; +import { DOMNode } from "../dom.js"; +export interface DecorationType { + spec: unknown; + map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null; + valid(node: Node, span: Decoration): boolean; + eq(other: DecorationType): boolean; + destroy(dom: DOMNode): void; +} +export type DecorationWithType = Decoration & { + type: DecorationType; +}; +type ReactWidgetSpec = { + side?: number; + marks?: readonly Mark[]; + stopEvent?: (event: Event) => boolean; + ignoreSelection?: boolean; + key?: string; +}; +export declare class ReactWidgetType implements DecorationType { + Component: ForwardRefExoticComponent & WidgetViewComponentProps>; + side: number; + spec: ReactWidgetSpec; + constructor(Component: ForwardRefExoticComponent & WidgetViewComponentProps>, spec?: ReactWidgetSpec); + map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null; + valid(): boolean; + eq(other: DecorationType): boolean; + destroy(): void; +} +export declare function widget(pos: number, component: ForwardRefExoticComponent & WidgetViewComponentProps>, spec?: ReactWidgetSpec): Decoration; +export interface ReactWidgetDecoration extends Decoration { + type: ReactWidgetType; + inline: false; +} +export {}; diff --git a/dist/types/decorations/computeDocDeco.d.ts b/dist/types/decorations/computeDocDeco.d.ts new file mode 100644 index 00000000..175dae7a --- /dev/null +++ b/dist/types/decorations/computeDocDeco.d.ts @@ -0,0 +1,13 @@ +import { Decoration, EditorView } from "prosemirror-view"; +/** + * Produces the outer decorations for the doc node, based + * on the attributes editor prop. + * + * The return value of this function is memoized; if it is to + * return an equivalent value to the last time it was called for + * a given EditorView, it will return exactly that previous value. + * + * This makes it safe to call in a React render function, even + * if its result is used in a dependencies array for a hook. + */ +export declare function computeDocDeco(view: EditorView): [Decoration]; diff --git a/dist/types/decorations/internalTypes.d.ts b/dist/types/decorations/internalTypes.d.ts new file mode 100644 index 00000000..82071639 --- /dev/null +++ b/dist/types/decorations/internalTypes.d.ts @@ -0,0 +1,16 @@ +import { Node } from "prosemirror-model"; +import { Mapping } from "prosemirror-transform"; +import { Decoration, DecorationSet, DecorationSource } from "prosemirror-view"; +export interface InternalDecorationSource { + map: (mapping: Mapping, node: Node) => DecorationSource; + locals(node: Node): readonly Decoration[]; + forChild(offset: number, child: Node): DecorationSource; + eq(other: DecorationSource): boolean; + forEachSet(f: (set: DecorationSet) => void): void; +} +export interface InternalDecorationSet extends InternalDecorationSource { + localsInner(node: Node): readonly Decoration[]; +} +export interface InternalDecoration extends Decoration { + copy(from: number, to: number): Decoration; +} diff --git a/dist/types/decorations/iterDeco.d.ts b/dist/types/decorations/iterDeco.d.ts new file mode 100644 index 00000000..634581d3 --- /dev/null +++ b/dist/types/decorations/iterDeco.d.ts @@ -0,0 +1,3 @@ +import { Node } from "prosemirror-model"; +import { Decoration, DecorationSource } from "prosemirror-view"; +export declare function iterDeco(parent: Node, deco: DecorationSource, onWidget: (widget: Decoration, isNative: boolean, offset: number, index: number, insideNode: boolean) => void, onNode: (node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, offset: number, index: number) => void): void; diff --git a/dist/types/decorations/viewDecorations.d.ts b/dist/types/decorations/viewDecorations.d.ts new file mode 100644 index 00000000..ab163bd3 --- /dev/null +++ b/dist/types/decorations/viewDecorations.d.ts @@ -0,0 +1,13 @@ +import { Decoration, DecorationSource, EditorView } from "prosemirror-view"; +/** + * Produces the DecorationSource for the current state, based + * on the decorations editor prop. + * + * The return value of this function is memoized; if it is to + * return an equivalent value to the last time it was called for + * a given EditorView, it will return exactly that previous value. + * + * This makes it safe to call in a React render function, even + * if its result is used in a dependencies array for a hook. + */ +export declare function viewDecorations(view: EditorView, cursorWrapper: Decoration | null): DecorationSource; diff --git a/dist/types/dom.d.ts b/dist/types/dom.d.ts new file mode 100644 index 00000000..fc905c46 --- /dev/null +++ b/dist/types/dom.d.ts @@ -0,0 +1,22 @@ +export type DOMNode = InstanceType; +export type DOMSelection = InstanceType; +export type DOMSelectionRange = { + focusNode: DOMNode | null; + focusOffset: number; + anchorNode: DOMNode | null; + anchorOffset: number; +}; +export declare const domIndex: (node: Node) => number; +export declare const parentNode: (node: Node) => Node | null; +export declare const textRange: (node: Text, from?: number, to?: number) => Range; +export declare const isEquivalentPosition: (node: Node, off: number, targetNode: Node, targetOff: number) => boolean; +export declare function nodeSize(node: Node): number; +export declare function isOnEdge(node: Node, offset: number, parent: Node): boolean; +export declare function hasBlockDesc(dom: Node): boolean | null | undefined; +export declare const selectionCollapsed: (domSel: DOMSelectionRange) => boolean | null; +export declare function keyEvent(keyCode: number, key: string): KeyboardEvent; +export declare function deepActiveElement(doc: Document): Element | null; +export declare function caretFromPoint(doc: Document, x: number, y: number): { + node: Node; + offset: number; +} | undefined; diff --git a/dist/types/hooks/__tests__/useEditorViewLayoutEffect.test.d.ts b/dist/types/hooks/__tests__/useEditorViewLayoutEffect.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/hooks/__tests__/useEditorViewLayoutEffect.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/hooks/useComponentEventListeners.d.ts b/dist/types/hooks/useComponentEventListeners.d.ts new file mode 100644 index 00000000..b84db7e6 --- /dev/null +++ b/dist/types/hooks/useComponentEventListeners.d.ts @@ -0,0 +1,33 @@ +import type { DOMEventMap } from "prosemirror-view"; +import { EventHandler } from "../plugins/componentEventListeners.js"; +/** + * Produces a plugin that can be used with ProseMirror to handle DOM + * events at the EditorView.dom element. + * + * - `reactEventsPlugin` is a ProseMirror plugin for handling DOM events + * at the EditorView.dom element. It should be passed to `useEditorView`, + * along with any other plugins. + * + * - `registerEventListener` and `unregisterEventListener` should be + * passed to `EditorContext.Provider`. + * + * @privateRemarks + * + * This hook uses a combination of mutable and immutable updates to give + * us precise control over when we re-create the ProseMirror plugin. + * + * The plugin has a mutable reference to the set of handlers for each + * event type, but the set of event types is static. This means that we + * need to produce a new ProseMirror plugin whenever a new event type is + * registered. We avoid producing a new ProseMirrer plugin in any other + * scenario to avoid the performance overhead of reconfiguring the plugins + * in the EditorView. + * + * To accomplish this, we shallowly clone the registry whenever a new event + * type is registered. + */ +export declare function useComponentEventListeners(): { + registerEventListener: (eventType: keyof DOMEventMap, handler: EventHandler) => void; + unregisterEventListener: (eventType: keyof DOMEventMap, handler: EventHandler) => void; + componentEventListenersPlugin: import("prosemirror-state").Plugin; +}; diff --git a/dist/types/hooks/useEditor.d.ts b/dist/types/hooks/useEditor.d.ts new file mode 100644 index 00000000..c4b91b6f --- /dev/null +++ b/dist/types/hooks/useEditor.d.ts @@ -0,0 +1,67 @@ +import { EditorState, Plugin, Transaction } from "prosemirror-state"; +import { Decoration, DirectEditorProps, EditorProps, EditorView } from "prosemirror-view"; +import { DOMNode } from "../dom.js"; +import { NodeViewDesc } from "../viewdesc.js"; +export declare class ReactEditorView extends EditorView { + private shouldUpdatePluginViews; + private oldProps; + private _props; + constructor(place: null | DOMNode | ((editor: HTMLElement) => void) | { + mount: HTMLElement; + }, props: DirectEditorProps & { + docView: NodeViewDesc; + }); + /** + * Whether the EditorView's updateStateInner method thinks that the + * docView needs to be blown away and redrawn. + * + * @privateremarks + * + * When ProseMirror View detects that the EditorState has been reconfigured + * to provide new custom node views, it calls an internal function that + * we can't override in order to recreate the entire editor DOM. + * + * This property mimics that check, so that we can replace the EditorView + * with another of our own, preventing ProseMirror View from taking over + * DOM management responsibility. + */ + get needsRedraw(): boolean; + /** + * Like setProps, but without executing any side effects. + * Safe to use in a component render method. + */ + pureSetProps(props: Partial): void; + /** + * Triggers any side effects that have been queued by previous + * calls to pureSetProps. + */ + runPendingEffects(): void; + update(props: DirectEditorProps): void; + updatePluginViews(prevState?: EditorState): void; + destroy(): void; +} +export interface UseEditorOptions extends EditorProps { + defaultState?: EditorState; + state?: EditorState; + plugins?: Plugin[]; + dispatchTransaction?(this: EditorView, tr: Transaction): void; +} +/** + * Creates, mounts, and manages a ProseMirror `EditorView`. + * + * All state and props updates are executed in a layout effect. + * To ensure that the EditorState and EditorView are never out of + * sync, it's important that the EditorView produced by this hook + * is only accessed through the `useEditorViewEvent` and + * `useEditorViewLayoutEffect` hooks. + */ +export declare function useEditor(mount: T | null, options: UseEditorOptions): { + editor: { + view: EditorView | null; + registerEventListener: (eventType: keyof import("prosemirror-view").DOMEventMap, handler: import("../plugins/componentEventListeners.js").EventHandler) => void; + unregisterEventListener: (eventType: keyof import("prosemirror-view").DOMEventMap, handler: import("../plugins/componentEventListeners.js").EventHandler) => void; + cursorWrapper: Decoration | null; + docViewDescRef: import("react").MutableRefObject; + }; + state: EditorState; +}; diff --git a/dist/types/hooks/useEditorEffect.d.ts b/dist/types/hooks/useEditorEffect.d.ts new file mode 100644 index 00000000..cb025826 --- /dev/null +++ b/dist/types/hooks/useEditorEffect.d.ts @@ -0,0 +1,17 @@ +import type { EditorView } from "prosemirror-view"; +import type { DependencyList } from "react"; +/** + * Registers a layout effect to run after the EditorView has + * been updated with the latest EditorState and Decorations. + * + * Effects can take an EditorView instance as an argument. + * This hook should be used to execute layout effects that + * depend on the EditorView, such as for positioning DOM + * nodes based on ProseMirror positions. + * + * Layout effects registered with this hook still fire + * synchronously after all DOM mutations, but they do so + * _after_ the EditorView has been updated, even when the + * EditorView lives in an ancestor component. + */ +export declare function useEditorEffect(effect: (editorView: EditorView) => void | (() => void), dependencies?: DependencyList): void; diff --git a/dist/types/hooks/useEditorEventCallback.d.ts b/dist/types/hooks/useEditorEventCallback.d.ts new file mode 100644 index 00000000..8945b5d1 --- /dev/null +++ b/dist/types/hooks/useEditorEventCallback.d.ts @@ -0,0 +1,15 @@ +import type { EditorView } from "prosemirror-view"; +/** + * Returns a stable function reference to be used as an + * event handler callback. + * + * The callback will be called with the EditorView instance + * as its first argument. + * + * This hook is dependent on both the + * `EditorViewContext.Provider` and the + * `DeferredLayoutEffectProvider`. It can only be used in a + * component that is mounted as a child of both of these + * providers. + */ +export declare function useEditorEventCallback(callback: (view: EditorView, ...args: T) => R): (...args: T) => R | undefined; diff --git a/dist/types/hooks/useEditorEventListener.d.ts b/dist/types/hooks/useEditorEventListener.d.ts new file mode 100644 index 00000000..a98b94f4 --- /dev/null +++ b/dist/types/hooks/useEditorEventListener.d.ts @@ -0,0 +1,8 @@ +import type { DOMEventMap } from "prosemirror-view"; +import type { EventHandler } from "../plugins/componentEventListeners.js"; +/** + * Attaches an event listener at the `EditorView`'s DOM node. See + * [the ProseMirror docs](https://prosemirror.net/docs/ref/#view.EditorProps.handleDOMEvents) + * for more details. + */ +export declare function useEditorEventListener(eventType: EventType, handler: EventHandler): void; diff --git a/dist/types/hooks/useEditorState.d.ts b/dist/types/hooks/useEditorState.d.ts new file mode 100644 index 00000000..76ccde67 --- /dev/null +++ b/dist/types/hooks/useEditorState.d.ts @@ -0,0 +1,5 @@ +import type { EditorState } from "prosemirror-state"; +/** + * Provides access to the current EditorState value. + */ +export declare function useEditorState(): EditorState; diff --git a/dist/types/hooks/useForceUpdate.d.ts b/dist/types/hooks/useForceUpdate.d.ts new file mode 100644 index 00000000..69fab706 --- /dev/null +++ b/dist/types/hooks/useForceUpdate.d.ts @@ -0,0 +1,5 @@ +/** + * Provides a function that forces an update of the + * component. + */ +export declare function useForceUpdate(): () => void; diff --git a/dist/types/hooks/useLayoutGroupEffect.d.ts b/dist/types/hooks/useLayoutGroupEffect.d.ts new file mode 100644 index 00000000..206e21ff --- /dev/null +++ b/dist/types/hooks/useLayoutGroupEffect.d.ts @@ -0,0 +1,3 @@ +import type { DependencyList, EffectCallback } from "react"; +/** Registers a layout effect to run at the nearest `LayoutGroup` boundary. */ +export declare function useLayoutGroupEffect(effect: EffectCallback, deps?: DependencyList): void; diff --git a/dist/types/hooks/useNodeViewDescriptor.d.ts b/dist/types/hooks/useNodeViewDescriptor.d.ts new file mode 100644 index 00000000..eb3f6196 --- /dev/null +++ b/dist/types/hooks/useNodeViewDescriptor.d.ts @@ -0,0 +1,10 @@ +import { Node } from "prosemirror-model"; +import { Decoration, DecorationSource } from "prosemirror-view"; +import { MutableRefObject } from "react"; +import { NodeViewDesc, ViewDesc } from "../viewdesc.js"; +export declare function useNodeViewDescriptor(node: Node | undefined, getPos: () => number, domRef: undefined | MutableRefObject, nodeDomRef: MutableRefObject, innerDecorations: DecorationSource, outerDecorations: readonly Decoration[], viewDesc?: NodeViewDesc, contentDOMRef?: MutableRefObject): { + hasContentDOM: boolean; + childDescriptors: MutableRefObject; + nodeViewDescRef: MutableRefObject; + setStopEvent: (newStopEvent: (event: Event) => boolean | undefined) => void; +}; diff --git a/dist/types/hooks/useReactKeys.d.ts b/dist/types/hooks/useReactKeys.d.ts new file mode 100644 index 00000000..66ad7622 --- /dev/null +++ b/dist/types/hooks/useReactKeys.d.ts @@ -0,0 +1,5 @@ +export declare function useReactKeys(): { + posToKey: Map; + keyToPos: Map; + posToNode: Map; +} | null | undefined; diff --git a/dist/types/hooks/useStopEvent.d.ts b/dist/types/hooks/useStopEvent.d.ts new file mode 100644 index 00000000..74333fd5 --- /dev/null +++ b/dist/types/hooks/useStopEvent.d.ts @@ -0,0 +1,2 @@ +import { EditorView } from "prosemirror-view"; +export declare function useStopEvent(stopEvent: (view: EditorView, event: Event) => boolean): void; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts new file mode 100644 index 00000000..11dd31a4 --- /dev/null +++ b/dist/types/index.d.ts @@ -0,0 +1,11 @@ +export { ProseMirror } from "./components/ProseMirror.js"; +export { ProseMirrorDoc } from "./components/ProseMirrorDoc.js"; +export { useEditorEffect } from "./hooks/useEditorEffect.js"; +export { useEditorEventCallback } from "./hooks/useEditorEventCallback.js"; +export { useEditorEventListener } from "./hooks/useEditorEventListener.js"; +export { useEditorState } from "./hooks/useEditorState.js"; +export { useStopEvent } from "./hooks/useStopEvent.js"; +export { reactKeys } from "./plugins/reactKeys.js"; +export { widget } from "./decorations/ReactWidgetType.js"; +export type { NodeViewComponentProps } from "./components/NodeViewComponentProps.js"; +export type { WidgetViewComponentProps } from "./components/WidgetViewComponentProps.js"; diff --git a/dist/types/plugins/__tests__/reactKeys.test.d.ts b/dist/types/plugins/__tests__/reactKeys.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/types/plugins/__tests__/reactKeys.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/types/plugins/beforeInputPlugin.d.ts b/dist/types/plugins/beforeInputPlugin.d.ts new file mode 100644 index 00000000..9c857085 --- /dev/null +++ b/dist/types/plugins/beforeInputPlugin.d.ts @@ -0,0 +1,3 @@ +import { Plugin } from "prosemirror-state"; +import { Decoration } from "prosemirror-view"; +export declare function beforeInputPlugin(setCursorWrapper: (deco: Decoration | null) => void): Plugin; diff --git a/dist/types/plugins/componentEventListeners.d.ts b/dist/types/plugins/componentEventListeners.d.ts new file mode 100644 index 00000000..e93b160b --- /dev/null +++ b/dist/types/plugins/componentEventListeners.d.ts @@ -0,0 +1,4 @@ +import { Plugin } from "prosemirror-state"; +import { DOMEventMap, EditorView } from "prosemirror-view"; +export type EventHandler = (this: Plugin, view: EditorView, event: DOMEventMap[EventType]) => boolean | void; +export declare function componentEventListeners(eventHandlerRegistry: Map>): Plugin; diff --git a/dist/types/plugins/componentEventListenersPlugin.d.ts b/dist/types/plugins/componentEventListenersPlugin.d.ts new file mode 100644 index 00000000..60d8f479 --- /dev/null +++ b/dist/types/plugins/componentEventListenersPlugin.d.ts @@ -0,0 +1,4 @@ +import { Plugin } from "prosemirror-state"; +import { DOMEventMap, EditorView } from "prosemirror-view"; +export type EventHandler = (this: Plugin, view: EditorView, event: DOMEventMap[EventType]) => boolean | void; +export declare function createComponentEventListenersPlugin(eventHandlerRegistry: Map>): Plugin; diff --git a/dist/types/plugins/reactKeys.d.ts b/dist/types/plugins/reactKeys.d.ts new file mode 100644 index 00000000..d638f4e7 --- /dev/null +++ b/dist/types/plugins/reactKeys.d.ts @@ -0,0 +1,19 @@ +import { Node } from "prosemirror-model"; +import { Plugin, PluginKey } from "prosemirror-state"; +export declare function createNodeKey(): string; +export declare const reactKeysPluginKey: PluginKey<{ + posToKey: Map; + keyToPos: Map; + posToNode: Map; +}>; +/** + * Tracks a unique key for each (non-text) node in the + * document, identified by its current position. Keys are + * (mostly) stable across transaction applications. The + * key for a given node can be accessed by that node's + * current position in the document, and vice versa. + */ +export declare function reactKeys(): Plugin<{ + posToKey: Map; + keyToPos: Map; +}>; diff --git a/dist/types/props.d.ts b/dist/types/props.d.ts new file mode 100644 index 00000000..192a70bb --- /dev/null +++ b/dist/types/props.d.ts @@ -0,0 +1,1174 @@ +import { HTMLProps } from "react"; +export declare function kebabCaseToCamelCase(str: string): string; +/** + * Converts a CSS style string to an object + * that can be passed to a React component's + * `style` prop. + */ +export declare function cssToStyles(css: string): Record; +/** + * Merges two sets of React props. Class names + * will be concatenated and style objects + * will be merged. + */ +export declare function mergeReactProps(a: HTMLProps, b: HTMLProps): { + className: string; + style: { + accentColor?: import("csstype").Property.AccentColor | undefined; + alignContent?: import("csstype").Property.AlignContent | undefined; + alignItems?: import("csstype").Property.AlignItems | undefined; + alignSelf?: import("csstype").Property.AlignSelf | undefined; + alignTracks?: import("csstype").Property.AlignTracks | undefined; + animationComposition?: import("csstype").Property.AnimationComposition | undefined; + animationDelay?: import("csstype").Property.AnimationDelay | undefined; + animationDirection?: import("csstype").Property.AnimationDirection | undefined; + animationDuration?: import("csstype").Property.AnimationDuration | undefined; + animationFillMode?: import("csstype").Property.AnimationFillMode | undefined; + animationIterationCount?: import("csstype").Property.AnimationIterationCount | undefined; + animationName?: import("csstype").Property.AnimationName | undefined; + animationPlayState?: import("csstype").Property.AnimationPlayState | undefined; + animationTimeline?: import("csstype").Property.AnimationTimeline | undefined; + animationTimingFunction?: import("csstype").Property.AnimationTimingFunction | undefined; + appearance?: import("csstype").Property.Appearance | undefined; + aspectRatio?: import("csstype").Property.AspectRatio | undefined; + backdropFilter?: import("csstype").Property.BackdropFilter | undefined; + backfaceVisibility?: import("csstype").Property.BackfaceVisibility | undefined; + backgroundAttachment?: import("csstype").Property.BackgroundAttachment | undefined; + backgroundBlendMode?: import("csstype").Property.BackgroundBlendMode | undefined; + backgroundClip?: import("csstype").Property.BackgroundClip | undefined; + backgroundColor?: import("csstype").Property.BackgroundColor | undefined; + backgroundImage?: import("csstype").Property.BackgroundImage | undefined; + backgroundOrigin?: import("csstype").Property.BackgroundOrigin | undefined; + backgroundPositionX?: string | number | undefined; + backgroundPositionY?: string | number | undefined; + backgroundRepeat?: import("csstype").Property.BackgroundRepeat | undefined; + backgroundSize?: string | number | undefined; + blockOverflow?: import("csstype").Property.BlockOverflow | undefined; + blockSize?: string | number | undefined; + borderBlockColor?: import("csstype").Property.BorderBlockColor | undefined; + borderBlockEndColor?: import("csstype").Property.BorderBlockEndColor | undefined; + borderBlockEndStyle?: import("csstype").Property.BorderBlockEndStyle | undefined; + borderBlockEndWidth?: import("csstype").Property.BorderBlockEndWidth | undefined; + borderBlockStartColor?: import("csstype").Property.BorderBlockStartColor | undefined; + borderBlockStartStyle?: import("csstype").Property.BorderBlockStartStyle | undefined; + borderBlockStartWidth?: import("csstype").Property.BorderBlockStartWidth | undefined; + borderBlockStyle?: import("csstype").Property.BorderBlockStyle | undefined; + borderBlockWidth?: import("csstype").Property.BorderBlockWidth | undefined; + borderBottomColor?: import("csstype").Property.BorderBottomColor | undefined; + borderBottomLeftRadius?: string | number | undefined; + borderBottomRightRadius?: string | number | undefined; + borderBottomStyle?: import("csstype").Property.BorderBottomStyle | undefined; + borderBottomWidth?: import("csstype").Property.BorderBottomWidth | undefined; + borderCollapse?: import("csstype").Property.BorderCollapse | undefined; + borderEndEndRadius?: string | number | undefined; + borderEndStartRadius?: string | number | undefined; + borderImageOutset?: string | number | undefined; + borderImageRepeat?: import("csstype").Property.BorderImageRepeat | undefined; + borderImageSlice?: import("csstype").Property.BorderImageSlice | undefined; + borderImageSource?: import("csstype").Property.BorderImageSource | undefined; + borderImageWidth?: string | number | undefined; + borderInlineColor?: import("csstype").Property.BorderInlineColor | undefined; + borderInlineEndColor?: import("csstype").Property.BorderInlineEndColor | undefined; + borderInlineEndStyle?: import("csstype").Property.BorderInlineEndStyle | undefined; + borderInlineEndWidth?: import("csstype").Property.BorderInlineEndWidth | undefined; + borderInlineStartColor?: import("csstype").Property.BorderInlineStartColor | undefined; + borderInlineStartStyle?: import("csstype").Property.BorderInlineStartStyle | undefined; + borderInlineStartWidth?: import("csstype").Property.BorderInlineStartWidth | undefined; + borderInlineStyle?: import("csstype").Property.BorderInlineStyle | undefined; + borderInlineWidth?: import("csstype").Property.BorderInlineWidth | undefined; + borderLeftColor?: import("csstype").Property.BorderLeftColor | undefined; + borderLeftStyle?: import("csstype").Property.BorderLeftStyle | undefined; + borderLeftWidth?: import("csstype").Property.BorderLeftWidth | undefined; + borderRightColor?: import("csstype").Property.BorderRightColor | undefined; + borderRightStyle?: import("csstype").Property.BorderRightStyle | undefined; + borderRightWidth?: import("csstype").Property.BorderRightWidth | undefined; + borderSpacing?: string | number | undefined; + borderStartEndRadius?: string | number | undefined; + borderStartStartRadius?: string | number | undefined; + borderTopColor?: import("csstype").Property.BorderTopColor | undefined; + borderTopLeftRadius?: string | number | undefined; + borderTopRightRadius?: string | number | undefined; + borderTopStyle?: import("csstype").Property.BorderTopStyle | undefined; + borderTopWidth?: import("csstype").Property.BorderTopWidth | undefined; + bottom?: string | number | undefined; + boxDecorationBreak?: import("csstype").Property.BoxDecorationBreak | undefined; + boxShadow?: import("csstype").Property.BoxShadow | undefined; + boxSizing?: import("csstype").Property.BoxSizing | undefined; + breakAfter?: import("csstype").Property.BreakAfter | undefined; + breakBefore?: import("csstype").Property.BreakBefore | undefined; + breakInside?: import("csstype").Property.BreakInside | undefined; + captionSide?: import("csstype").Property.CaptionSide | undefined; + caretColor?: import("csstype").Property.CaretColor | undefined; + clear?: import("csstype").Property.Clear | undefined; + clipPath?: import("csstype").Property.ClipPath | undefined; + color?: import("csstype").Property.Color | undefined; + colorAdjust?: import("csstype").Property.PrintColorAdjust | undefined; + colorScheme?: import("csstype").Property.ColorScheme | undefined; + columnCount?: import("csstype").Property.ColumnCount | undefined; + columnFill?: import("csstype").Property.ColumnFill | undefined; + columnGap?: string | number | undefined; + columnRuleColor?: import("csstype").Property.ColumnRuleColor | undefined; + columnRuleStyle?: import("csstype").Property.ColumnRuleStyle | undefined; + columnRuleWidth?: string | number | undefined; + columnSpan?: import("csstype").Property.ColumnSpan | undefined; + columnWidth?: import("csstype").Property.ColumnWidth | undefined; + contain?: import("csstype").Property.Contain | undefined; + content?: import("csstype").Property.Content | undefined; + contentVisibility?: import("csstype").Property.ContentVisibility | undefined; + counterIncrement?: import("csstype").Property.CounterIncrement | undefined; + counterReset?: import("csstype").Property.CounterReset | undefined; + counterSet?: import("csstype").Property.CounterSet | undefined; + cursor?: import("csstype").Property.Cursor | undefined; + direction?: import("csstype").Property.Direction | undefined; + display?: import("csstype").Property.Display | undefined; + emptyCells?: import("csstype").Property.EmptyCells | undefined; + filter?: import("csstype").Property.Filter | undefined; + flexBasis?: string | number | undefined; + flexDirection?: import("csstype").Property.FlexDirection | undefined; + flexGrow?: import("csstype").Property.FlexGrow | undefined; + flexShrink?: import("csstype").Property.FlexShrink | undefined; + flexWrap?: import("csstype").Property.FlexWrap | undefined; + float?: import("csstype").Property.Float | undefined; + fontFamily?: import("csstype").Property.FontFamily | undefined; + fontFeatureSettings?: import("csstype").Property.FontFeatureSettings | undefined; + fontKerning?: import("csstype").Property.FontKerning | undefined; + fontLanguageOverride?: import("csstype").Property.FontLanguageOverride | undefined; + fontOpticalSizing?: import("csstype").Property.FontOpticalSizing | undefined; + fontSize?: string | number | undefined; + fontSizeAdjust?: import("csstype").Property.FontSizeAdjust | undefined; + fontSmooth?: import("csstype").Property.FontSmooth | undefined; + fontStretch?: import("csstype").Property.FontStretch | undefined; + fontStyle?: import("csstype").Property.FontStyle | undefined; + fontSynthesis?: import("csstype").Property.FontSynthesis | undefined; + fontVariant?: import("csstype").Property.FontVariant | undefined; + fontVariantAlternates?: import("csstype").Property.FontVariantAlternates | undefined; + fontVariantCaps?: import("csstype").Property.FontVariantCaps | undefined; + fontVariantEastAsian?: import("csstype").Property.FontVariantEastAsian | undefined; + fontVariantLigatures?: import("csstype").Property.FontVariantLigatures | undefined; + fontVariantNumeric?: import("csstype").Property.FontVariantNumeric | undefined; + fontVariantPosition?: import("csstype").Property.FontVariantPosition | undefined; + fontVariationSettings?: import("csstype").Property.FontVariationSettings | undefined; + fontWeight?: import("csstype").Property.FontWeight | undefined; + forcedColorAdjust?: import("csstype").Property.ForcedColorAdjust | undefined; + gridAutoColumns?: string | number | undefined; + gridAutoFlow?: import("csstype").Property.GridAutoFlow | undefined; + gridAutoRows?: string | number | undefined; + gridColumnEnd?: import("csstype").Property.GridColumnEnd | undefined; + gridColumnStart?: import("csstype").Property.GridColumnStart | undefined; + gridRowEnd?: import("csstype").Property.GridRowEnd | undefined; + gridRowStart?: import("csstype").Property.GridRowStart | undefined; + gridTemplateAreas?: import("csstype").Property.GridTemplateAreas | undefined; + gridTemplateColumns?: string | number | undefined; + gridTemplateRows?: string | number | undefined; + hangingPunctuation?: import("csstype").Property.HangingPunctuation | undefined; + height?: string | number | undefined; + hyphenateCharacter?: import("csstype").Property.HyphenateCharacter | undefined; + hyphens?: import("csstype").Property.Hyphens | undefined; + imageOrientation?: import("csstype").Property.ImageOrientation | undefined; + imageRendering?: import("csstype").Property.ImageRendering | undefined; + imageResolution?: import("csstype").Property.ImageResolution | undefined; + initialLetter?: import("csstype").Property.InitialLetter | undefined; + inlineSize?: string | number | undefined; + inputSecurity?: import("csstype").Property.InputSecurity | undefined; + inset?: string | number | undefined; + insetBlock?: string | number | undefined; + insetBlockEnd?: string | number | undefined; + insetBlockStart?: string | number | undefined; + insetInline?: string | number | undefined; + insetInlineEnd?: string | number | undefined; + insetInlineStart?: string | number | undefined; + isolation?: import("csstype").Property.Isolation | undefined; + justifyContent?: import("csstype").Property.JustifyContent | undefined; + justifyItems?: import("csstype").Property.JustifyItems | undefined; + justifySelf?: import("csstype").Property.JustifySelf | undefined; + justifyTracks?: import("csstype").Property.JustifyTracks | undefined; + left?: string | number | undefined; + letterSpacing?: import("csstype").Property.LetterSpacing | undefined; + lineBreak?: import("csstype").Property.LineBreak | undefined; + lineHeight?: string | number | undefined; + lineHeightStep?: import("csstype").Property.LineHeightStep | undefined; + listStyleImage?: import("csstype").Property.ListStyleImage | undefined; + listStylePosition?: import("csstype").Property.ListStylePosition | undefined; + listStyleType?: import("csstype").Property.ListStyleType | undefined; + marginBlock?: string | number | undefined; + marginBlockEnd?: string | number | undefined; + marginBlockStart?: string | number | undefined; + marginBottom?: string | number | undefined; + marginInline?: string | number | undefined; + marginInlineEnd?: string | number | undefined; + marginInlineStart?: string | number | undefined; + marginLeft?: string | number | undefined; + marginRight?: string | number | undefined; + marginTop?: string | number | undefined; + maskBorderMode?: import("csstype").Property.MaskBorderMode | undefined; + maskBorderOutset?: string | number | undefined; + maskBorderRepeat?: import("csstype").Property.MaskBorderRepeat | undefined; + maskBorderSlice?: import("csstype").Property.MaskBorderSlice | undefined; + maskBorderSource?: import("csstype").Property.MaskBorderSource | undefined; + maskBorderWidth?: string | number | undefined; + maskClip?: import("csstype").Property.MaskClip | undefined; + maskComposite?: import("csstype").Property.MaskComposite | undefined; + maskImage?: import("csstype").Property.MaskImage | undefined; + maskMode?: import("csstype").Property.MaskMode | undefined; + maskOrigin?: import("csstype").Property.MaskOrigin | undefined; + maskPosition?: string | number | undefined; + maskRepeat?: import("csstype").Property.MaskRepeat | undefined; + maskSize?: string | number | undefined; + maskType?: import("csstype").Property.MaskType | undefined; + mathDepth?: import("csstype").Property.MathDepth | undefined; + mathShift?: import("csstype").Property.MathShift | undefined; + mathStyle?: import("csstype").Property.MathStyle | undefined; + maxBlockSize?: string | number | undefined; + maxHeight?: string | number | undefined; + maxInlineSize?: string | number | undefined; + maxLines?: import("csstype").Property.MaxLines | undefined; + maxWidth?: string | number | undefined; + minBlockSize?: string | number | undefined; + minHeight?: string | number | undefined; + minInlineSize?: string | number | undefined; + minWidth?: string | number | undefined; + mixBlendMode?: import("csstype").Property.MixBlendMode | undefined; + motionDistance?: string | number | undefined; + motionPath?: import("csstype").Property.OffsetPath | undefined; + motionRotation?: import("csstype").Property.OffsetRotate | undefined; + objectFit?: import("csstype").Property.ObjectFit | undefined; + objectPosition?: string | number | undefined; + offsetAnchor?: string | number | undefined; + offsetDistance?: string | number | undefined; + offsetPath?: import("csstype").Property.OffsetPath | undefined; + offsetRotate?: import("csstype").Property.OffsetRotate | undefined; + offsetRotation?: import("csstype").Property.OffsetRotate | undefined; + opacity?: import("csstype").Property.Opacity | undefined; + order?: import("csstype").Property.Order | undefined; + orphans?: import("csstype").Property.Orphans | undefined; + outlineColor?: import("csstype").Property.OutlineColor | undefined; + outlineOffset?: import("csstype").Property.OutlineOffset | undefined; + outlineStyle?: import("csstype").Property.OutlineStyle | undefined; + outlineWidth?: import("csstype").Property.OutlineWidth | undefined; + overflowAnchor?: import("csstype").Property.OverflowAnchor | undefined; + overflowBlock?: import("csstype").Property.OverflowBlock | undefined; + overflowClipBox?: import("csstype").Property.OverflowClipBox | undefined; + overflowClipMargin?: string | number | undefined; + overflowInline?: import("csstype").Property.OverflowInline | undefined; + overflowWrap?: import("csstype").Property.OverflowWrap | undefined; + overflowX?: import("csstype").Property.OverflowX | undefined; + overflowY?: import("csstype").Property.OverflowY | undefined; + overscrollBehaviorBlock?: import("csstype").Property.OverscrollBehaviorBlock | undefined; + overscrollBehaviorInline?: import("csstype").Property.OverscrollBehaviorInline | undefined; + overscrollBehaviorX?: import("csstype").Property.OverscrollBehaviorX | undefined; + overscrollBehaviorY?: import("csstype").Property.OverscrollBehaviorY | undefined; + paddingBlock?: string | number | undefined; + paddingBlockEnd?: string | number | undefined; + paddingBlockStart?: string | number | undefined; + paddingBottom?: string | number | undefined; + paddingInline?: string | number | undefined; + paddingInlineEnd?: string | number | undefined; + paddingInlineStart?: string | number | undefined; + paddingLeft?: string | number | undefined; + paddingRight?: string | number | undefined; + paddingTop?: string | number | undefined; + pageBreakAfter?: import("csstype").Property.PageBreakAfter | undefined; + pageBreakBefore?: import("csstype").Property.PageBreakBefore | undefined; + pageBreakInside?: import("csstype").Property.PageBreakInside | undefined; + paintOrder?: import("csstype").Property.PaintOrder | undefined; + perspective?: import("csstype").Property.Perspective | undefined; + perspectiveOrigin?: string | number | undefined; + placeContent?: import("csstype").Property.PlaceContent | undefined; + pointerEvents?: import("csstype").Property.PointerEvents | undefined; + position?: import("csstype").Property.Position | undefined; + printColorAdjust?: import("csstype").Property.PrintColorAdjust | undefined; + quotes?: import("csstype").Property.Quotes | undefined; + resize?: import("csstype").Property.Resize | undefined; + right?: string | number | undefined; + rotate?: import("csstype").Property.Rotate | undefined; + rowGap?: string | number | undefined; + rubyAlign?: import("csstype").Property.RubyAlign | undefined; + rubyMerge?: import("csstype").Property.RubyMerge | undefined; + rubyPosition?: import("csstype").Property.RubyPosition | undefined; + scale?: import("csstype").Property.Scale | undefined; + scrollBehavior?: import("csstype").Property.ScrollBehavior | undefined; + scrollMargin?: string | number | undefined; + scrollMarginBlock?: string | number | undefined; + scrollMarginBlockEnd?: import("csstype").Property.ScrollMarginBlockEnd | undefined; + scrollMarginBlockStart?: import("csstype").Property.ScrollMarginBlockStart | undefined; + scrollMarginBottom?: import("csstype").Property.ScrollMarginBottom | undefined; + scrollMarginInline?: string | number | undefined; + scrollMarginInlineEnd?: import("csstype").Property.ScrollMarginInlineEnd | undefined; + scrollMarginInlineStart?: import("csstype").Property.ScrollMarginInlineStart | undefined; + scrollMarginLeft?: import("csstype").Property.ScrollMarginLeft | undefined; + scrollMarginRight?: import("csstype").Property.ScrollMarginRight | undefined; + scrollMarginTop?: import("csstype").Property.ScrollMarginTop | undefined; + scrollPadding?: string | number | undefined; + scrollPaddingBlock?: string | number | undefined; + scrollPaddingBlockEnd?: string | number | undefined; + scrollPaddingBlockStart?: string | number | undefined; + scrollPaddingBottom?: string | number | undefined; + scrollPaddingInline?: string | number | undefined; + scrollPaddingInlineEnd?: string | number | undefined; + scrollPaddingInlineStart?: string | number | undefined; + scrollPaddingLeft?: string | number | undefined; + scrollPaddingRight?: string | number | undefined; + scrollPaddingTop?: string | number | undefined; + scrollSnapAlign?: import("csstype").Property.ScrollSnapAlign | undefined; + scrollSnapMargin?: string | number | undefined; + scrollSnapMarginBottom?: import("csstype").Property.ScrollMarginBottom | undefined; + scrollSnapMarginLeft?: import("csstype").Property.ScrollMarginLeft | undefined; + scrollSnapMarginRight?: import("csstype").Property.ScrollMarginRight | undefined; + scrollSnapMarginTop?: import("csstype").Property.ScrollMarginTop | undefined; + scrollSnapStop?: import("csstype").Property.ScrollSnapStop | undefined; + scrollSnapType?: import("csstype").Property.ScrollSnapType | undefined; + scrollbarColor?: import("csstype").Property.ScrollbarColor | undefined; + scrollbarGutter?: import("csstype").Property.ScrollbarGutter | undefined; + scrollbarWidth?: import("csstype").Property.ScrollbarWidth | undefined; + shapeImageThreshold?: import("csstype").Property.ShapeImageThreshold | undefined; + shapeMargin?: string | number | undefined; + shapeOutside?: import("csstype").Property.ShapeOutside | undefined; + tabSize?: string | number | undefined; + tableLayout?: import("csstype").Property.TableLayout | undefined; + textAlign?: import("csstype").Property.TextAlign | undefined; + textAlignLast?: import("csstype").Property.TextAlignLast | undefined; + textCombineUpright?: import("csstype").Property.TextCombineUpright | undefined; + textDecorationColor?: import("csstype").Property.TextDecorationColor | undefined; + textDecorationLine?: import("csstype").Property.TextDecorationLine | undefined; + textDecorationSkip?: import("csstype").Property.TextDecorationSkip | undefined; + textDecorationSkipInk?: import("csstype").Property.TextDecorationSkipInk | undefined; + textDecorationStyle?: import("csstype").Property.TextDecorationStyle | undefined; + textDecorationThickness?: string | number | undefined; + textEmphasisColor?: import("csstype").Property.TextEmphasisColor | undefined; + textEmphasisPosition?: import("csstype").Property.TextEmphasisPosition | undefined; + textEmphasisStyle?: import("csstype").Property.TextEmphasisStyle | undefined; + textIndent?: string | number | undefined; + textJustify?: import("csstype").Property.TextJustify | undefined; + textOrientation?: import("csstype").Property.TextOrientation | undefined; + textOverflow?: import("csstype").Property.TextOverflow | undefined; + textRendering?: import("csstype").Property.TextRendering | undefined; + textShadow?: import("csstype").Property.TextShadow | undefined; + textSizeAdjust?: import("csstype").Property.TextSizeAdjust | undefined; + textTransform?: import("csstype").Property.TextTransform | undefined; + textUnderlineOffset?: string | number | undefined; + textUnderlinePosition?: import("csstype").Property.TextUnderlinePosition | undefined; + top?: string | number | undefined; + touchAction?: import("csstype").Property.TouchAction | undefined; + transform?: import("csstype").Property.Transform | undefined; + transformBox?: import("csstype").Property.TransformBox | undefined; + transformOrigin?: string | number | undefined; + transformStyle?: import("csstype").Property.TransformStyle | undefined; + transitionDelay?: import("csstype").Property.TransitionDelay | undefined; + transitionDuration?: import("csstype").Property.TransitionDuration | undefined; + transitionProperty?: import("csstype").Property.TransitionProperty | undefined; + transitionTimingFunction?: import("csstype").Property.TransitionTimingFunction | undefined; + translate?: string | number | undefined; + unicodeBidi?: import("csstype").Property.UnicodeBidi | undefined; + userSelect?: import("csstype").Property.UserSelect | undefined; + verticalAlign?: string | number | undefined; + visibility?: import("csstype").Property.Visibility | undefined; + whiteSpace?: import("csstype").Property.WhiteSpace | undefined; + widows?: import("csstype").Property.Widows | undefined; + width?: string | number | undefined; + willChange?: import("csstype").Property.WillChange | undefined; + wordBreak?: import("csstype").Property.WordBreak | undefined; + wordSpacing?: import("csstype").Property.WordSpacing | undefined; + wordWrap?: import("csstype").Property.WordWrap | undefined; + writingMode?: import("csstype").Property.WritingMode | undefined; + zIndex?: import("csstype").Property.ZIndex | undefined; + zoom?: import("csstype").Property.Zoom | undefined; + all?: import("csstype").Globals | undefined; + animation?: import("csstype").Property.Animation | undefined; + background?: string | number | undefined; + backgroundPosition?: string | number | undefined; + border?: string | number | undefined; + borderBlock?: string | number | undefined; + borderBlockEnd?: string | number | undefined; + borderBlockStart?: string | number | undefined; + borderBottom?: string | number | undefined; + borderColor?: import("csstype").Property.BorderColor | undefined; + borderImage?: import("csstype").Property.BorderImage | undefined; + borderInline?: string | number | undefined; + borderInlineEnd?: string | number | undefined; + borderInlineStart?: string | number | undefined; + borderLeft?: string | number | undefined; + borderRadius?: string | number | undefined; + borderRight?: string | number | undefined; + borderStyle?: import("csstype").Property.BorderStyle | undefined; + borderTop?: string | number | undefined; + borderWidth?: string | number | undefined; + columnRule?: string | number | undefined; + columns?: string | number | undefined; + flex?: string | number | undefined; + flexFlow?: import("csstype").Property.FlexFlow | undefined; + font?: import("csstype").Property.Font | undefined; + gap?: string | number | undefined; + grid?: import("csstype").Property.Grid | undefined; + gridArea?: import("csstype").Property.GridArea | undefined; + gridColumn?: import("csstype").Property.GridColumn | undefined; + gridRow?: import("csstype").Property.GridRow | undefined; + gridTemplate?: import("csstype").Property.GridTemplate | undefined; + lineClamp?: import("csstype").Property.LineClamp | undefined; + listStyle?: import("csstype").Property.ListStyle | undefined; + margin?: string | number | undefined; + mask?: string | number | undefined; + maskBorder?: import("csstype").Property.MaskBorder | undefined; + motion?: string | number | undefined; + offset?: string | number | undefined; + outline?: string | number | undefined; + overflow?: import("csstype").Property.Overflow | undefined; + overscrollBehavior?: import("csstype").Property.OverscrollBehavior | undefined; + padding?: string | number | undefined; + placeItems?: import("csstype").Property.PlaceItems | undefined; + placeSelf?: import("csstype").Property.PlaceSelf | undefined; + textDecoration?: string | number | undefined; + textEmphasis?: import("csstype").Property.TextEmphasis | undefined; + transition?: import("csstype").Property.Transition | undefined; + MozAnimationDelay?: import("csstype").Property.AnimationDelay | undefined; + MozAnimationDirection?: import("csstype").Property.AnimationDirection | undefined; + MozAnimationDuration?: import("csstype").Property.AnimationDuration | undefined; + MozAnimationFillMode?: import("csstype").Property.AnimationFillMode | undefined; + MozAnimationIterationCount?: import("csstype").Property.AnimationIterationCount | undefined; + MozAnimationName?: import("csstype").Property.AnimationName | undefined; + MozAnimationPlayState?: import("csstype").Property.AnimationPlayState | undefined; + MozAnimationTimingFunction?: import("csstype").Property.AnimationTimingFunction | undefined; + MozAppearance?: import("csstype").Property.MozAppearance | undefined; + MozBackfaceVisibility?: import("csstype").Property.BackfaceVisibility | undefined; + MozBorderBottomColors?: import("csstype").Property.MozBorderBottomColors | undefined; + MozBorderEndColor?: import("csstype").Property.BorderInlineEndColor | undefined; + MozBorderEndStyle?: import("csstype").Property.BorderInlineEndStyle | undefined; + MozBorderEndWidth?: import("csstype").Property.BorderInlineEndWidth | undefined; + MozBorderLeftColors?: import("csstype").Property.MozBorderLeftColors | undefined; + MozBorderRightColors?: import("csstype").Property.MozBorderRightColors | undefined; + MozBorderStartColor?: import("csstype").Property.BorderInlineStartColor | undefined; + MozBorderStartStyle?: import("csstype").Property.BorderInlineStartStyle | undefined; + MozBorderTopColors?: import("csstype").Property.MozBorderTopColors | undefined; + MozBoxSizing?: import("csstype").Property.BoxSizing | undefined; + MozColumnCount?: import("csstype").Property.ColumnCount | undefined; + MozColumnFill?: import("csstype").Property.ColumnFill | undefined; + MozColumnRuleColor?: import("csstype").Property.ColumnRuleColor | undefined; + MozColumnRuleStyle?: import("csstype").Property.ColumnRuleStyle | undefined; + MozColumnRuleWidth?: string | number | undefined; + MozColumnWidth?: import("csstype").Property.ColumnWidth | undefined; + MozContextProperties?: import("csstype").Property.MozContextProperties | undefined; + MozFontFeatureSettings?: import("csstype").Property.FontFeatureSettings | undefined; + MozFontLanguageOverride?: import("csstype").Property.FontLanguageOverride | undefined; + MozHyphens?: import("csstype").Property.Hyphens | undefined; + MozImageRegion?: import("csstype").Property.MozImageRegion | undefined; + MozMarginEnd?: string | number | undefined; + MozMarginStart?: string | number | undefined; + MozOrient?: import("csstype").Property.MozOrient | undefined; + MozOsxFontSmoothing?: import("csstype").Property.FontSmooth | undefined; + MozPaddingEnd?: string | number | undefined; + MozPaddingStart?: string | number | undefined; + MozPerspective?: import("csstype").Property.Perspective | undefined; + MozPerspectiveOrigin?: string | number | undefined; + MozStackSizing?: import("csstype").Property.MozStackSizing | undefined; + MozTabSize?: string | number | undefined; + MozTextBlink?: import("csstype").Property.MozTextBlink | undefined; + MozTextSizeAdjust?: import("csstype").Property.TextSizeAdjust | undefined; + MozTransformOrigin?: string | number | undefined; + MozTransformStyle?: import("csstype").Property.TransformStyle | undefined; + MozTransitionDelay?: import("csstype").Property.TransitionDelay | undefined; + MozTransitionDuration?: import("csstype").Property.TransitionDuration | undefined; + MozTransitionProperty?: import("csstype").Property.TransitionProperty | undefined; + MozTransitionTimingFunction?: import("csstype").Property.TransitionTimingFunction | undefined; + MozUserFocus?: import("csstype").Property.MozUserFocus | undefined; + MozUserModify?: import("csstype").Property.MozUserModify | undefined; + MozUserSelect?: import("csstype").Property.UserSelect | undefined; + MozWindowDragging?: import("csstype").Property.MozWindowDragging | undefined; + MozWindowShadow?: import("csstype").Property.MozWindowShadow | undefined; + msAccelerator?: import("csstype").Property.MsAccelerator | undefined; + msBlockProgression?: import("csstype").Property.MsBlockProgression | undefined; + msContentZoomChaining?: import("csstype").Property.MsContentZoomChaining | undefined; + msContentZoomLimitMax?: import("csstype").Property.MsContentZoomLimitMax | undefined; + msContentZoomLimitMin?: import("csstype").Property.MsContentZoomLimitMin | undefined; + msContentZoomSnapPoints?: import("csstype").Property.MsContentZoomSnapPoints | undefined; + msContentZoomSnapType?: import("csstype").Property.MsContentZoomSnapType | undefined; + msContentZooming?: import("csstype").Property.MsContentZooming | undefined; + msFilter?: import("csstype").Property.MsFilter | undefined; + msFlexDirection?: import("csstype").Property.FlexDirection | undefined; + msFlexPositive?: import("csstype").Property.FlexGrow | undefined; + msFlowFrom?: import("csstype").Property.MsFlowFrom | undefined; + msFlowInto?: import("csstype").Property.MsFlowInto | undefined; + msGridColumns?: string | number | undefined; + msGridRows?: string | number | undefined; + msHighContrastAdjust?: import("csstype").Property.MsHighContrastAdjust | undefined; + msHyphenateLimitChars?: import("csstype").Property.MsHyphenateLimitChars | undefined; + msHyphenateLimitLines?: import("csstype").Property.MsHyphenateLimitLines | undefined; + msHyphenateLimitZone?: string | number | undefined; + msHyphens?: import("csstype").Property.Hyphens | undefined; + msImeAlign?: import("csstype").Property.MsImeAlign | undefined; + msLineBreak?: import("csstype").Property.LineBreak | undefined; + msOrder?: import("csstype").Property.Order | undefined; + msOverflowStyle?: import("csstype").Property.MsOverflowStyle | undefined; + msOverflowX?: import("csstype").Property.OverflowX | undefined; + msOverflowY?: import("csstype").Property.OverflowY | undefined; + msScrollChaining?: import("csstype").Property.MsScrollChaining | undefined; + msScrollLimitXMax?: import("csstype").Property.MsScrollLimitXMax | undefined; + msScrollLimitXMin?: import("csstype").Property.MsScrollLimitXMin | undefined; + msScrollLimitYMax?: import("csstype").Property.MsScrollLimitYMax | undefined; + msScrollLimitYMin?: import("csstype").Property.MsScrollLimitYMin | undefined; + msScrollRails?: import("csstype").Property.MsScrollRails | undefined; + msScrollSnapPointsX?: import("csstype").Property.MsScrollSnapPointsX | undefined; + msScrollSnapPointsY?: import("csstype").Property.MsScrollSnapPointsY | undefined; + msScrollSnapType?: import("csstype").Property.MsScrollSnapType | undefined; + msScrollTranslation?: import("csstype").Property.MsScrollTranslation | undefined; + msScrollbar3dlightColor?: import("csstype").Property.MsScrollbar3dlightColor | undefined; + msScrollbarArrowColor?: import("csstype").Property.MsScrollbarArrowColor | undefined; + msScrollbarBaseColor?: import("csstype").Property.MsScrollbarBaseColor | undefined; + msScrollbarDarkshadowColor?: import("csstype").Property.MsScrollbarDarkshadowColor | undefined; + msScrollbarFaceColor?: import("csstype").Property.MsScrollbarFaceColor | undefined; + msScrollbarHighlightColor?: import("csstype").Property.MsScrollbarHighlightColor | undefined; + msScrollbarShadowColor?: import("csstype").Property.MsScrollbarShadowColor | undefined; + msScrollbarTrackColor?: import("csstype").Property.MsScrollbarTrackColor | undefined; + msTextAutospace?: import("csstype").Property.MsTextAutospace | undefined; + msTextCombineHorizontal?: import("csstype").Property.TextCombineUpright | undefined; + msTextOverflow?: import("csstype").Property.TextOverflow | undefined; + msTouchAction?: import("csstype").Property.TouchAction | undefined; + msTouchSelect?: import("csstype").Property.MsTouchSelect | undefined; + msTransform?: import("csstype").Property.Transform | undefined; + msTransformOrigin?: string | number | undefined; + msTransitionDelay?: import("csstype").Property.TransitionDelay | undefined; + msTransitionDuration?: import("csstype").Property.TransitionDuration | undefined; + msTransitionProperty?: import("csstype").Property.TransitionProperty | undefined; + msTransitionTimingFunction?: import("csstype").Property.TransitionTimingFunction | undefined; + msUserSelect?: import("csstype").Property.MsUserSelect | undefined; + msWordBreak?: import("csstype").Property.WordBreak | undefined; + msWrapFlow?: import("csstype").Property.MsWrapFlow | undefined; + msWrapMargin?: import("csstype").Property.MsWrapMargin | undefined; + msWrapThrough?: import("csstype").Property.MsWrapThrough | undefined; + msWritingMode?: import("csstype").Property.WritingMode | undefined; + WebkitAlignContent?: import("csstype").Property.AlignContent | undefined; + WebkitAlignItems?: import("csstype").Property.AlignItems | undefined; + WebkitAlignSelf?: import("csstype").Property.AlignSelf | undefined; + WebkitAnimationDelay?: import("csstype").Property.AnimationDelay | undefined; + WebkitAnimationDirection?: import("csstype").Property.AnimationDirection | undefined; + WebkitAnimationDuration?: import("csstype").Property.AnimationDuration | undefined; + WebkitAnimationFillMode?: import("csstype").Property.AnimationFillMode | undefined; + WebkitAnimationIterationCount?: import("csstype").Property.AnimationIterationCount | undefined; + WebkitAnimationName?: import("csstype").Property.AnimationName | undefined; + WebkitAnimationPlayState?: import("csstype").Property.AnimationPlayState | undefined; + WebkitAnimationTimingFunction?: import("csstype").Property.AnimationTimingFunction | undefined; + WebkitAppearance?: import("csstype").Property.WebkitAppearance | undefined; + WebkitBackdropFilter?: import("csstype").Property.BackdropFilter | undefined; + WebkitBackfaceVisibility?: import("csstype").Property.BackfaceVisibility | undefined; + WebkitBackgroundClip?: import("csstype").Property.BackgroundClip | undefined; + WebkitBackgroundOrigin?: import("csstype").Property.BackgroundOrigin | undefined; + WebkitBackgroundSize?: string | number | undefined; + WebkitBorderBeforeColor?: import("csstype").Property.WebkitBorderBeforeColor | undefined; + WebkitBorderBeforeStyle?: import("csstype").Property.WebkitBorderBeforeStyle | undefined; + WebkitBorderBeforeWidth?: string | number | undefined; + WebkitBorderBottomLeftRadius?: string | number | undefined; + WebkitBorderBottomRightRadius?: string | number | undefined; + WebkitBorderImageSlice?: import("csstype").Property.BorderImageSlice | undefined; + WebkitBorderTopLeftRadius?: string | number | undefined; + WebkitBorderTopRightRadius?: string | number | undefined; + WebkitBoxDecorationBreak?: import("csstype").Property.BoxDecorationBreak | undefined; + WebkitBoxReflect?: string | number | undefined; + WebkitBoxShadow?: import("csstype").Property.BoxShadow | undefined; + WebkitBoxSizing?: import("csstype").Property.BoxSizing | undefined; + WebkitClipPath?: import("csstype").Property.ClipPath | undefined; + WebkitColumnCount?: import("csstype").Property.ColumnCount | undefined; + WebkitColumnFill?: import("csstype").Property.ColumnFill | undefined; + WebkitColumnRuleColor?: import("csstype").Property.ColumnRuleColor | undefined; + WebkitColumnRuleStyle?: import("csstype").Property.ColumnRuleStyle | undefined; + WebkitColumnRuleWidth?: string | number | undefined; + WebkitColumnSpan?: import("csstype").Property.ColumnSpan | undefined; + WebkitColumnWidth?: import("csstype").Property.ColumnWidth | undefined; + WebkitFilter?: import("csstype").Property.Filter | undefined; + WebkitFlexBasis?: string | number | undefined; + WebkitFlexDirection?: import("csstype").Property.FlexDirection | undefined; + WebkitFlexGrow?: import("csstype").Property.FlexGrow | undefined; + WebkitFlexShrink?: import("csstype").Property.FlexShrink | undefined; + WebkitFlexWrap?: import("csstype").Property.FlexWrap | undefined; + WebkitFontFeatureSettings?: import("csstype").Property.FontFeatureSettings | undefined; + WebkitFontKerning?: import("csstype").Property.FontKerning | undefined; + WebkitFontSmoothing?: import("csstype").Property.FontSmooth | undefined; + WebkitFontVariantLigatures?: import("csstype").Property.FontVariantLigatures | undefined; + WebkitHyphenateCharacter?: import("csstype").Property.HyphenateCharacter | undefined; + WebkitHyphens?: import("csstype").Property.Hyphens | undefined; + WebkitInitialLetter?: import("csstype").Property.InitialLetter | undefined; + WebkitJustifyContent?: import("csstype").Property.JustifyContent | undefined; + WebkitLineBreak?: import("csstype").Property.LineBreak | undefined; + WebkitLineClamp?: import("csstype").Property.WebkitLineClamp | undefined; + WebkitMarginEnd?: string | number | undefined; + WebkitMarginStart?: string | number | undefined; + WebkitMaskAttachment?: import("csstype").Property.WebkitMaskAttachment | undefined; + WebkitMaskBoxImageOutset?: string | number | undefined; + WebkitMaskBoxImageRepeat?: import("csstype").Property.MaskBorderRepeat | undefined; + WebkitMaskBoxImageSlice?: import("csstype").Property.MaskBorderSlice | undefined; + WebkitMaskBoxImageSource?: import("csstype").Property.MaskBorderSource | undefined; + WebkitMaskBoxImageWidth?: string | number | undefined; + WebkitMaskClip?: import("csstype").Property.WebkitMaskClip | undefined; + WebkitMaskComposite?: import("csstype").Property.WebkitMaskComposite | undefined; + WebkitMaskImage?: import("csstype").Property.WebkitMaskImage | undefined; + WebkitMaskOrigin?: import("csstype").Property.WebkitMaskOrigin | undefined; + WebkitMaskPosition?: string | number | undefined; + WebkitMaskPositionX?: string | number | undefined; + WebkitMaskPositionY?: string | number | undefined; + WebkitMaskRepeat?: import("csstype").Property.WebkitMaskRepeat | undefined; + WebkitMaskRepeatX?: import("csstype").Property.WebkitMaskRepeatX | undefined; + WebkitMaskRepeatY?: import("csstype").Property.WebkitMaskRepeatY | undefined; + WebkitMaskSize?: string | number | undefined; + WebkitMaxInlineSize?: string | number | undefined; + WebkitOrder?: import("csstype").Property.Order | undefined; + WebkitOverflowScrolling?: import("csstype").Property.WebkitOverflowScrolling | undefined; + WebkitPaddingEnd?: string | number | undefined; + WebkitPaddingStart?: string | number | undefined; + WebkitPerspective?: import("csstype").Property.Perspective | undefined; + WebkitPerspectiveOrigin?: string | number | undefined; + WebkitPrintColorAdjust?: import("csstype").Property.PrintColorAdjust | undefined; + WebkitRubyPosition?: import("csstype").Property.RubyPosition | undefined; + WebkitScrollSnapType?: import("csstype").Property.ScrollSnapType | undefined; + WebkitShapeMargin?: string | number | undefined; + WebkitTapHighlightColor?: import("csstype").Property.WebkitTapHighlightColor | undefined; + WebkitTextCombine?: import("csstype").Property.TextCombineUpright | undefined; + WebkitTextDecorationColor?: import("csstype").Property.TextDecorationColor | undefined; + WebkitTextDecorationLine?: import("csstype").Property.TextDecorationLine | undefined; + WebkitTextDecorationSkip?: import("csstype").Property.TextDecorationSkip | undefined; + WebkitTextDecorationStyle?: import("csstype").Property.TextDecorationStyle | undefined; + WebkitTextEmphasisColor?: import("csstype").Property.TextEmphasisColor | undefined; + WebkitTextEmphasisPosition?: import("csstype").Property.TextEmphasisPosition | undefined; + WebkitTextEmphasisStyle?: import("csstype").Property.TextEmphasisStyle | undefined; + WebkitTextFillColor?: import("csstype").Property.WebkitTextFillColor | undefined; + WebkitTextOrientation?: import("csstype").Property.TextOrientation | undefined; + WebkitTextSizeAdjust?: import("csstype").Property.TextSizeAdjust | undefined; + WebkitTextStrokeColor?: import("csstype").Property.WebkitTextStrokeColor | undefined; + WebkitTextStrokeWidth?: import("csstype").Property.WebkitTextStrokeWidth | undefined; + WebkitTextUnderlinePosition?: import("csstype").Property.TextUnderlinePosition | undefined; + WebkitTouchCallout?: import("csstype").Property.WebkitTouchCallout | undefined; + WebkitTransform?: import("csstype").Property.Transform | undefined; + WebkitTransformOrigin?: string | number | undefined; + WebkitTransformStyle?: import("csstype").Property.TransformStyle | undefined; + WebkitTransitionDelay?: import("csstype").Property.TransitionDelay | undefined; + WebkitTransitionDuration?: import("csstype").Property.TransitionDuration | undefined; + WebkitTransitionProperty?: import("csstype").Property.TransitionProperty | undefined; + WebkitTransitionTimingFunction?: import("csstype").Property.TransitionTimingFunction | undefined; + WebkitUserModify?: import("csstype").Property.WebkitUserModify | undefined; + WebkitUserSelect?: import("csstype").Property.UserSelect | undefined; + WebkitWritingMode?: import("csstype").Property.WritingMode | undefined; + MozAnimation?: import("csstype").Property.Animation | undefined; + MozBorderImage?: import("csstype").Property.BorderImage | undefined; + MozColumnRule?: string | number | undefined; + MozColumns?: string | number | undefined; + MozTransition?: import("csstype").Property.Transition | undefined; + msContentZoomLimit?: import("csstype").Property.MsContentZoomLimit | undefined; + msContentZoomSnap?: import("csstype").Property.MsContentZoomSnap | undefined; + msFlex?: string | number | undefined; + msScrollLimit?: import("csstype").Property.MsScrollLimit | undefined; + msScrollSnapX?: import("csstype").Property.MsScrollSnapX | undefined; + msScrollSnapY?: import("csstype").Property.MsScrollSnapY | undefined; + msTransition?: import("csstype").Property.Transition | undefined; + WebkitAnimation?: import("csstype").Property.Animation | undefined; + WebkitBorderBefore?: string | number | undefined; + WebkitBorderImage?: import("csstype").Property.BorderImage | undefined; + WebkitBorderRadius?: string | number | undefined; + WebkitColumnRule?: string | number | undefined; + WebkitColumns?: string | number | undefined; + WebkitFlex?: string | number | undefined; + WebkitFlexFlow?: import("csstype").Property.FlexFlow | undefined; + WebkitMask?: string | number | undefined; + WebkitMaskBoxImage?: import("csstype").Property.MaskBorder | undefined; + WebkitTextEmphasis?: import("csstype").Property.TextEmphasis | undefined; + WebkitTextStroke?: string | number | undefined; + WebkitTransition?: import("csstype").Property.Transition | undefined; + azimuth?: import("csstype").Property.Azimuth | undefined; + boxAlign?: import("csstype").Property.BoxAlign | undefined; + boxDirection?: import("csstype").Property.BoxDirection | undefined; + boxFlex?: import("csstype").Property.BoxFlex | undefined; + boxFlexGroup?: import("csstype").Property.BoxFlexGroup | undefined; + boxLines?: import("csstype").Property.BoxLines | undefined; + boxOrdinalGroup?: import("csstype").Property.BoxOrdinalGroup | undefined; + boxOrient?: import("csstype").Property.BoxOrient | undefined; + boxPack?: import("csstype").Property.BoxPack | undefined; + clip?: import("csstype").Property.Clip | undefined; + gridColumnGap?: string | number | undefined; + gridGap?: string | number | undefined; + gridRowGap?: string | number | undefined; + imeMode?: import("csstype").Property.ImeMode | undefined; + offsetBlock?: string | number | undefined; + offsetBlockEnd?: string | number | undefined; + offsetBlockStart?: string | number | undefined; + offsetInline?: string | number | undefined; + offsetInlineEnd?: string | number | undefined; + offsetInlineStart?: string | number | undefined; + scrollSnapCoordinate?: string | number | undefined; + scrollSnapDestination?: string | number | undefined; + scrollSnapPointsX?: import("csstype").Property.ScrollSnapPointsX | undefined; + scrollSnapPointsY?: import("csstype").Property.ScrollSnapPointsY | undefined; + scrollSnapTypeX?: import("csstype").Property.ScrollSnapTypeX | undefined; + scrollSnapTypeY?: import("csstype").Property.ScrollSnapTypeY | undefined; + KhtmlBoxAlign?: import("csstype").Property.BoxAlign | undefined; + KhtmlBoxDirection?: import("csstype").Property.BoxDirection | undefined; + KhtmlBoxFlex?: import("csstype").Property.BoxFlex | undefined; + KhtmlBoxFlexGroup?: import("csstype").Property.BoxFlexGroup | undefined; + KhtmlBoxLines?: import("csstype").Property.BoxLines | undefined; + KhtmlBoxOrdinalGroup?: import("csstype").Property.BoxOrdinalGroup | undefined; + KhtmlBoxOrient?: import("csstype").Property.BoxOrient | undefined; + KhtmlBoxPack?: import("csstype").Property.BoxPack | undefined; + KhtmlLineBreak?: import("csstype").Property.LineBreak | undefined; + KhtmlOpacity?: import("csstype").Property.Opacity | undefined; + KhtmlUserSelect?: import("csstype").Property.UserSelect | undefined; + MozBackgroundClip?: import("csstype").Property.BackgroundClip | undefined; + MozBackgroundInlinePolicy?: import("csstype").Property.BoxDecorationBreak | undefined; + MozBackgroundOrigin?: import("csstype").Property.BackgroundOrigin | undefined; + MozBackgroundSize?: string | number | undefined; + MozBinding?: import("csstype").Property.MozBinding | undefined; + MozBorderRadius?: string | number | undefined; + MozBorderRadiusBottomleft?: string | number | undefined; + MozBorderRadiusBottomright?: string | number | undefined; + MozBorderRadiusTopleft?: string | number | undefined; + MozBorderRadiusTopright?: string | number | undefined; + MozBoxAlign?: import("csstype").Property.BoxAlign | undefined; + MozBoxDirection?: import("csstype").Property.BoxDirection | undefined; + MozBoxFlex?: import("csstype").Property.BoxFlex | undefined; + MozBoxOrdinalGroup?: import("csstype").Property.BoxOrdinalGroup | undefined; + MozBoxOrient?: import("csstype").Property.BoxOrient | undefined; + MozBoxPack?: import("csstype").Property.BoxPack | undefined; + MozBoxShadow?: import("csstype").Property.BoxShadow | undefined; + MozFloatEdge?: import("csstype").Property.MozFloatEdge | undefined; + MozForceBrokenImageIcon?: import("csstype").Property.MozForceBrokenImageIcon | undefined; + MozOpacity?: import("csstype").Property.Opacity | undefined; + MozOutline?: string | number | undefined; + MozOutlineColor?: import("csstype").Property.OutlineColor | undefined; + MozOutlineRadius?: string | number | undefined; + MozOutlineRadiusBottomleft?: string | number | undefined; + MozOutlineRadiusBottomright?: string | number | undefined; + MozOutlineRadiusTopleft?: string | number | undefined; + MozOutlineRadiusTopright?: string | number | undefined; + MozOutlineStyle?: import("csstype").Property.OutlineStyle | undefined; + MozOutlineWidth?: import("csstype").Property.OutlineWidth | undefined; + MozTextAlignLast?: import("csstype").Property.TextAlignLast | undefined; + MozTextDecorationColor?: import("csstype").Property.TextDecorationColor | undefined; + MozTextDecorationLine?: import("csstype").Property.TextDecorationLine | undefined; + MozTextDecorationStyle?: import("csstype").Property.TextDecorationStyle | undefined; + MozUserInput?: import("csstype").Property.MozUserInput | undefined; + msImeMode?: import("csstype").Property.ImeMode | undefined; + OAnimation?: import("csstype").Property.Animation | undefined; + OAnimationDelay?: import("csstype").Property.AnimationDelay | undefined; + OAnimationDirection?: import("csstype").Property.AnimationDirection | undefined; + OAnimationDuration?: import("csstype").Property.AnimationDuration | undefined; + OAnimationFillMode?: import("csstype").Property.AnimationFillMode | undefined; + OAnimationIterationCount?: import("csstype").Property.AnimationIterationCount | undefined; + OAnimationName?: import("csstype").Property.AnimationName | undefined; + OAnimationPlayState?: import("csstype").Property.AnimationPlayState | undefined; + OAnimationTimingFunction?: import("csstype").Property.AnimationTimingFunction | undefined; + OBackgroundSize?: string | number | undefined; + OBorderImage?: import("csstype").Property.BorderImage | undefined; + OObjectFit?: import("csstype").Property.ObjectFit | undefined; + OObjectPosition?: string | number | undefined; + OTabSize?: string | number | undefined; + OTextOverflow?: import("csstype").Property.TextOverflow | undefined; + OTransform?: import("csstype").Property.Transform | undefined; + OTransformOrigin?: string | number | undefined; + OTransition?: import("csstype").Property.Transition | undefined; + OTransitionDelay?: import("csstype").Property.TransitionDelay | undefined; + OTransitionDuration?: import("csstype").Property.TransitionDuration | undefined; + OTransitionProperty?: import("csstype").Property.TransitionProperty | undefined; + OTransitionTimingFunction?: import("csstype").Property.TransitionTimingFunction | undefined; + WebkitBoxAlign?: import("csstype").Property.BoxAlign | undefined; + WebkitBoxDirection?: import("csstype").Property.BoxDirection | undefined; + WebkitBoxFlex?: import("csstype").Property.BoxFlex | undefined; + WebkitBoxFlexGroup?: import("csstype").Property.BoxFlexGroup | undefined; + WebkitBoxLines?: import("csstype").Property.BoxLines | undefined; + WebkitBoxOrdinalGroup?: import("csstype").Property.BoxOrdinalGroup | undefined; + WebkitBoxOrient?: import("csstype").Property.BoxOrient | undefined; + WebkitBoxPack?: import("csstype").Property.BoxPack | undefined; + WebkitScrollSnapPointsX?: import("csstype").Property.ScrollSnapPointsX | undefined; + WebkitScrollSnapPointsY?: import("csstype").Property.ScrollSnapPointsY | undefined; + alignmentBaseline?: import("csstype").Property.AlignmentBaseline | undefined; + baselineShift?: string | number | undefined; + clipRule?: import("csstype").Property.ClipRule | undefined; + colorInterpolation?: import("csstype").Property.ColorInterpolation | undefined; + colorRendering?: import("csstype").Property.ColorRendering | undefined; + dominantBaseline?: import("csstype").Property.DominantBaseline | undefined; + fill?: import("csstype").Property.Fill | undefined; + fillOpacity?: import("csstype").Property.FillOpacity | undefined; + fillRule?: import("csstype").Property.FillRule | undefined; + floodColor?: import("csstype").Property.FloodColor | undefined; + floodOpacity?: import("csstype").Property.FloodOpacity | undefined; + glyphOrientationVertical?: import("csstype").Property.GlyphOrientationVertical | undefined; + lightingColor?: import("csstype").Property.LightingColor | undefined; + marker?: import("csstype").Property.Marker | undefined; + markerEnd?: import("csstype").Property.MarkerEnd | undefined; + markerMid?: import("csstype").Property.MarkerMid | undefined; + markerStart?: import("csstype").Property.MarkerStart | undefined; + shapeRendering?: import("csstype").Property.ShapeRendering | undefined; + stopColor?: import("csstype").Property.StopColor | undefined; + stopOpacity?: import("csstype").Property.StopOpacity | undefined; + stroke?: import("csstype").Property.Stroke | undefined; + strokeDasharray?: string | number | undefined; + strokeDashoffset?: string | number | undefined; + strokeLinecap?: import("csstype").Property.StrokeLinecap | undefined; + strokeLinejoin?: import("csstype").Property.StrokeLinejoin | undefined; + strokeMiterlimit?: import("csstype").Property.StrokeMiterlimit | undefined; + strokeOpacity?: import("csstype").Property.StrokeOpacity | undefined; + strokeWidth?: string | number | undefined; + textAnchor?: import("csstype").Property.TextAnchor | undefined; + vectorEffect?: import("csstype").Property.VectorEffect | undefined; + }; + accept?: string | undefined; + acceptCharset?: string | undefined; + action?: string | undefined; + allowFullScreen?: boolean | undefined; + allowTransparency?: boolean | undefined; + alt?: string | undefined; + as?: string | undefined; + async?: boolean | undefined; + autoComplete?: string | undefined; + autoFocus?: boolean | undefined; + autoPlay?: boolean | undefined; + capture?: boolean | "user" | "environment" | undefined; + cellPadding?: string | number | undefined; + cellSpacing?: string | number | undefined; + charSet?: string | undefined; + challenge?: string | undefined; + checked?: boolean | undefined; + cite?: string | undefined; + classID?: string | undefined; + cols?: number | undefined; + colSpan?: number | undefined; + content?: string | undefined; + controls?: boolean | undefined; + coords?: string | undefined; + crossOrigin?: "" | "anonymous" | "use-credentials" | undefined; + data?: string | undefined; + dateTime?: string | undefined; + default?: boolean | undefined; + defer?: boolean | undefined; + disabled?: boolean | undefined; + download?: any; + encType?: string | undefined; + form?: string | undefined; + formAction?: string | undefined; + formEncType?: string | undefined; + formMethod?: string | undefined; + formNoValidate?: boolean | undefined; + formTarget?: string | undefined; + frameBorder?: string | number | undefined; + headers?: string | undefined; + height?: string | number | undefined; + high?: number | undefined; + href?: string | undefined; + hrefLang?: string | undefined; + htmlFor?: string | undefined; + httpEquiv?: string | undefined; + integrity?: string | undefined; + keyParams?: string | undefined; + keyType?: string | undefined; + kind?: string | undefined; + label?: string | undefined; + list?: string | undefined; + loop?: boolean | undefined; + low?: number | undefined; + manifest?: string | undefined; + marginHeight?: number | undefined; + marginWidth?: number | undefined; + max?: string | number | undefined; + maxLength?: number | undefined; + media?: string | undefined; + mediaGroup?: string | undefined; + method?: string | undefined; + min?: string | number | undefined; + minLength?: number | undefined; + multiple?: boolean | undefined; + muted?: boolean | undefined; + name?: string | undefined; + noValidate?: boolean | undefined; + open?: boolean | undefined; + optimum?: number | undefined; + pattern?: string | undefined; + placeholder?: string | undefined; + playsInline?: boolean | undefined; + poster?: string | undefined; + preload?: string | undefined; + readOnly?: boolean | undefined; + rel?: string | undefined; + required?: boolean | undefined; + reversed?: boolean | undefined; + rows?: number | undefined; + rowSpan?: number | undefined; + sandbox?: string | undefined; + scope?: string | undefined; + scoped?: boolean | undefined; + scrolling?: string | undefined; + seamless?: boolean | undefined; + selected?: boolean | undefined; + shape?: string | undefined; + size?: number | undefined; + sizes?: string | undefined; + span?: number | undefined; + src?: string | undefined; + srcDoc?: string | undefined; + srcLang?: string | undefined; + srcSet?: string | undefined; + start?: number | undefined; + step?: string | number | undefined; + summary?: string | undefined; + target?: string | undefined; + type?: string | undefined; + useMap?: string | undefined; + value?: string | number | readonly string[] | undefined; + width?: string | number | undefined; + wmode?: string | undefined; + wrap?: string | undefined; + defaultChecked?: boolean | undefined; + defaultValue?: string | number | readonly string[] | undefined; + suppressContentEditableWarning?: boolean | undefined; + suppressHydrationWarning?: boolean | undefined; + accessKey?: string | undefined; + contentEditable?: (boolean | "true" | "false") | "inherit" | undefined; + contextMenu?: string | undefined; + dir?: string | undefined; + draggable?: (boolean | "true" | "false") | undefined; + hidden?: boolean | undefined; + id?: string | undefined; + lang?: string | undefined; + nonce?: string | undefined; + slot?: string | undefined; + spellCheck?: (boolean | "true" | "false") | undefined; + tabIndex?: number | undefined; + title?: string | undefined; + translate?: "yes" | "no" | undefined; + radioGroup?: string | undefined; + role?: import("react").AriaRole | undefined; + about?: string | undefined; + datatype?: string | undefined; + inlist?: any; + prefix?: string | undefined; + property?: string | undefined; + resource?: string | undefined; + typeof?: string | undefined; + vocab?: string | undefined; + autoCapitalize?: string | undefined; + autoCorrect?: string | undefined; + autoSave?: string | undefined; + color?: string | undefined; + itemProp?: string | undefined; + itemScope?: boolean | undefined; + itemType?: string | undefined; + itemID?: string | undefined; + itemRef?: string | undefined; + results?: number | undefined; + security?: string | undefined; + unselectable?: "on" | "off" | undefined; + inputMode?: "text" | "none" | "search" | "email" | "tel" | "url" | "numeric" | "decimal" | undefined; + is?: string | undefined; + 'aria-activedescendant'?: string | undefined; + 'aria-atomic'?: (boolean | "true" | "false") | undefined; + 'aria-autocomplete'?: "list" | "none" | "both" | "inline" | undefined; + 'aria-busy'?: (boolean | "true" | "false") | undefined; + 'aria-checked'?: boolean | "true" | "false" | "mixed" | undefined; + 'aria-colcount'?: number | undefined; + 'aria-colindex'?: number | undefined; + 'aria-colspan'?: number | undefined; + 'aria-controls'?: string | undefined; + 'aria-current'?: boolean | "time" | "step" | "date" | "true" | "false" | "page" | "location" | undefined; + 'aria-describedby'?: string | undefined; + 'aria-details'?: string | undefined; + 'aria-disabled'?: (boolean | "true" | "false") | undefined; + 'aria-dropeffect'?: "link" | "none" | "copy" | "move" | "execute" | "popup" | undefined; + 'aria-errormessage'?: string | undefined; + 'aria-expanded'?: (boolean | "true" | "false") | undefined; + 'aria-flowto'?: string | undefined; + 'aria-grabbed'?: (boolean | "true" | "false") | undefined; + 'aria-haspopup'?: boolean | "dialog" | "menu" | "grid" | "listbox" | "tree" | "true" | "false" | undefined; + 'aria-hidden'?: (boolean | "true" | "false") | undefined; + 'aria-invalid'?: boolean | "true" | "false" | "grammar" | "spelling" | undefined; + 'aria-keyshortcuts'?: string | undefined; + 'aria-label'?: string | undefined; + 'aria-labelledby'?: string | undefined; + 'aria-level'?: number | undefined; + 'aria-live'?: "off" | "assertive" | "polite" | undefined; + 'aria-modal'?: (boolean | "true" | "false") | undefined; + 'aria-multiline'?: (boolean | "true" | "false") | undefined; + 'aria-multiselectable'?: (boolean | "true" | "false") | undefined; + 'aria-orientation'?: "horizontal" | "vertical" | undefined; + 'aria-owns'?: string | undefined; + 'aria-placeholder'?: string | undefined; + 'aria-posinset'?: number | undefined; + 'aria-pressed'?: boolean | "true" | "false" | "mixed" | undefined; + 'aria-readonly'?: (boolean | "true" | "false") | undefined; + 'aria-relevant'?: "text" | "all" | "additions" | "additions removals" | "additions text" | "removals" | "removals additions" | "removals text" | "text additions" | "text removals" | undefined; + 'aria-required'?: (boolean | "true" | "false") | undefined; + 'aria-roledescription'?: string | undefined; + 'aria-rowcount'?: number | undefined; + 'aria-rowindex'?: number | undefined; + 'aria-rowspan'?: number | undefined; + 'aria-selected'?: (boolean | "true" | "false") | undefined; + 'aria-setsize'?: number | undefined; + 'aria-sort'?: "none" | "ascending" | "descending" | "other" | undefined; + 'aria-valuemax'?: number | undefined; + 'aria-valuemin'?: number | undefined; + 'aria-valuenow'?: number | undefined; + 'aria-valuetext'?: string | undefined; + children?: import("react").ReactNode; + dangerouslySetInnerHTML?: { + __html: string; + } | undefined; + onCopy?: import("react").ClipboardEventHandler | undefined; + onCopyCapture?: import("react").ClipboardEventHandler | undefined; + onCut?: import("react").ClipboardEventHandler | undefined; + onCutCapture?: import("react").ClipboardEventHandler | undefined; + onPaste?: import("react").ClipboardEventHandler | undefined; + onPasteCapture?: import("react").ClipboardEventHandler | undefined; + onCompositionEnd?: import("react").CompositionEventHandler | undefined; + onCompositionEndCapture?: import("react").CompositionEventHandler | undefined; + onCompositionStart?: import("react").CompositionEventHandler | undefined; + onCompositionStartCapture?: import("react").CompositionEventHandler | undefined; + onCompositionUpdate?: import("react").CompositionEventHandler | undefined; + onCompositionUpdateCapture?: import("react").CompositionEventHandler | undefined; + onFocus?: import("react").FocusEventHandler | undefined; + onFocusCapture?: import("react").FocusEventHandler | undefined; + onBlur?: import("react").FocusEventHandler | undefined; + onBlurCapture?: import("react").FocusEventHandler | undefined; + onChange?: import("react").FormEventHandler | undefined; + onChangeCapture?: import("react").FormEventHandler | undefined; + onBeforeInput?: import("react").FormEventHandler | undefined; + onBeforeInputCapture?: import("react").FormEventHandler | undefined; + onInput?: import("react").FormEventHandler | undefined; + onInputCapture?: import("react").FormEventHandler | undefined; + onReset?: import("react").FormEventHandler | undefined; + onResetCapture?: import("react").FormEventHandler | undefined; + onSubmit?: import("react").FormEventHandler | undefined; + onSubmitCapture?: import("react").FormEventHandler | undefined; + onInvalid?: import("react").FormEventHandler | undefined; + onInvalidCapture?: import("react").FormEventHandler | undefined; + onLoad?: import("react").ReactEventHandler | undefined; + onLoadCapture?: import("react").ReactEventHandler | undefined; + onError?: import("react").ReactEventHandler | undefined; + onErrorCapture?: import("react").ReactEventHandler | undefined; + onKeyDown?: import("react").KeyboardEventHandler | undefined; + onKeyDownCapture?: import("react").KeyboardEventHandler | undefined; + onKeyPress?: import("react").KeyboardEventHandler | undefined; + onKeyPressCapture?: import("react").KeyboardEventHandler | undefined; + onKeyUp?: import("react").KeyboardEventHandler | undefined; + onKeyUpCapture?: import("react").KeyboardEventHandler | undefined; + onAbort?: import("react").ReactEventHandler | undefined; + onAbortCapture?: import("react").ReactEventHandler | undefined; + onCanPlay?: import("react").ReactEventHandler | undefined; + onCanPlayCapture?: import("react").ReactEventHandler | undefined; + onCanPlayThrough?: import("react").ReactEventHandler | undefined; + onCanPlayThroughCapture?: import("react").ReactEventHandler | undefined; + onDurationChange?: import("react").ReactEventHandler | undefined; + onDurationChangeCapture?: import("react").ReactEventHandler | undefined; + onEmptied?: import("react").ReactEventHandler | undefined; + onEmptiedCapture?: import("react").ReactEventHandler | undefined; + onEncrypted?: import("react").ReactEventHandler | undefined; + onEncryptedCapture?: import("react").ReactEventHandler | undefined; + onEnded?: import("react").ReactEventHandler | undefined; + onEndedCapture?: import("react").ReactEventHandler | undefined; + onLoadedData?: import("react").ReactEventHandler | undefined; + onLoadedDataCapture?: import("react").ReactEventHandler | undefined; + onLoadedMetadata?: import("react").ReactEventHandler | undefined; + onLoadedMetadataCapture?: import("react").ReactEventHandler | undefined; + onLoadStart?: import("react").ReactEventHandler | undefined; + onLoadStartCapture?: import("react").ReactEventHandler | undefined; + onPause?: import("react").ReactEventHandler | undefined; + onPauseCapture?: import("react").ReactEventHandler | undefined; + onPlay?: import("react").ReactEventHandler | undefined; + onPlayCapture?: import("react").ReactEventHandler | undefined; + onPlaying?: import("react").ReactEventHandler | undefined; + onPlayingCapture?: import("react").ReactEventHandler | undefined; + onProgress?: import("react").ReactEventHandler | undefined; + onProgressCapture?: import("react").ReactEventHandler | undefined; + onRateChange?: import("react").ReactEventHandler | undefined; + onRateChangeCapture?: import("react").ReactEventHandler | undefined; + onResize?: import("react").ReactEventHandler | undefined; + onResizeCapture?: import("react").ReactEventHandler | undefined; + onSeeked?: import("react").ReactEventHandler | undefined; + onSeekedCapture?: import("react").ReactEventHandler | undefined; + onSeeking?: import("react").ReactEventHandler | undefined; + onSeekingCapture?: import("react").ReactEventHandler | undefined; + onStalled?: import("react").ReactEventHandler | undefined; + onStalledCapture?: import("react").ReactEventHandler | undefined; + onSuspend?: import("react").ReactEventHandler | undefined; + onSuspendCapture?: import("react").ReactEventHandler | undefined; + onTimeUpdate?: import("react").ReactEventHandler | undefined; + onTimeUpdateCapture?: import("react").ReactEventHandler | undefined; + onVolumeChange?: import("react").ReactEventHandler | undefined; + onVolumeChangeCapture?: import("react").ReactEventHandler | undefined; + onWaiting?: import("react").ReactEventHandler | undefined; + onWaitingCapture?: import("react").ReactEventHandler | undefined; + onAuxClick?: import("react").MouseEventHandler | undefined; + onAuxClickCapture?: import("react").MouseEventHandler | undefined; + onClick?: import("react").MouseEventHandler | undefined; + onClickCapture?: import("react").MouseEventHandler | undefined; + onContextMenu?: import("react").MouseEventHandler | undefined; + onContextMenuCapture?: import("react").MouseEventHandler | undefined; + onDoubleClick?: import("react").MouseEventHandler | undefined; + onDoubleClickCapture?: import("react").MouseEventHandler | undefined; + onDrag?: import("react").DragEventHandler | undefined; + onDragCapture?: import("react").DragEventHandler | undefined; + onDragEnd?: import("react").DragEventHandler | undefined; + onDragEndCapture?: import("react").DragEventHandler | undefined; + onDragEnter?: import("react").DragEventHandler | undefined; + onDragEnterCapture?: import("react").DragEventHandler | undefined; + onDragExit?: import("react").DragEventHandler | undefined; + onDragExitCapture?: import("react").DragEventHandler | undefined; + onDragLeave?: import("react").DragEventHandler | undefined; + onDragLeaveCapture?: import("react").DragEventHandler | undefined; + onDragOver?: import("react").DragEventHandler | undefined; + onDragOverCapture?: import("react").DragEventHandler | undefined; + onDragStart?: import("react").DragEventHandler | undefined; + onDragStartCapture?: import("react").DragEventHandler | undefined; + onDrop?: import("react").DragEventHandler | undefined; + onDropCapture?: import("react").DragEventHandler | undefined; + onMouseDown?: import("react").MouseEventHandler | undefined; + onMouseDownCapture?: import("react").MouseEventHandler | undefined; + onMouseEnter?: import("react").MouseEventHandler | undefined; + onMouseLeave?: import("react").MouseEventHandler | undefined; + onMouseMove?: import("react").MouseEventHandler | undefined; + onMouseMoveCapture?: import("react").MouseEventHandler | undefined; + onMouseOut?: import("react").MouseEventHandler | undefined; + onMouseOutCapture?: import("react").MouseEventHandler | undefined; + onMouseOver?: import("react").MouseEventHandler | undefined; + onMouseOverCapture?: import("react").MouseEventHandler | undefined; + onMouseUp?: import("react").MouseEventHandler | undefined; + onMouseUpCapture?: import("react").MouseEventHandler | undefined; + onSelect?: import("react").ReactEventHandler | undefined; + onSelectCapture?: import("react").ReactEventHandler | undefined; + onTouchCancel?: import("react").TouchEventHandler | undefined; + onTouchCancelCapture?: import("react").TouchEventHandler | undefined; + onTouchEnd?: import("react").TouchEventHandler | undefined; + onTouchEndCapture?: import("react").TouchEventHandler | undefined; + onTouchMove?: import("react").TouchEventHandler | undefined; + onTouchMoveCapture?: import("react").TouchEventHandler | undefined; + onTouchStart?: import("react").TouchEventHandler | undefined; + onTouchStartCapture?: import("react").TouchEventHandler | undefined; + onPointerDown?: import("react").PointerEventHandler | undefined; + onPointerDownCapture?: import("react").PointerEventHandler | undefined; + onPointerMove?: import("react").PointerEventHandler | undefined; + onPointerMoveCapture?: import("react").PointerEventHandler | undefined; + onPointerUp?: import("react").PointerEventHandler | undefined; + onPointerUpCapture?: import("react").PointerEventHandler | undefined; + onPointerCancel?: import("react").PointerEventHandler | undefined; + onPointerCancelCapture?: import("react").PointerEventHandler | undefined; + onPointerEnter?: import("react").PointerEventHandler | undefined; + onPointerEnterCapture?: import("react").PointerEventHandler | undefined; + onPointerLeave?: import("react").PointerEventHandler | undefined; + onPointerLeaveCapture?: import("react").PointerEventHandler | undefined; + onPointerOver?: import("react").PointerEventHandler | undefined; + onPointerOverCapture?: import("react").PointerEventHandler | undefined; + onPointerOut?: import("react").PointerEventHandler | undefined; + onPointerOutCapture?: import("react").PointerEventHandler | undefined; + onGotPointerCapture?: import("react").PointerEventHandler | undefined; + onGotPointerCaptureCapture?: import("react").PointerEventHandler | undefined; + onLostPointerCapture?: import("react").PointerEventHandler | undefined; + onLostPointerCaptureCapture?: import("react").PointerEventHandler | undefined; + onScroll?: import("react").UIEventHandler | undefined; + onScrollCapture?: import("react").UIEventHandler | undefined; + onWheel?: import("react").WheelEventHandler | undefined; + onWheelCapture?: import("react").WheelEventHandler | undefined; + onAnimationStart?: import("react").AnimationEventHandler | undefined; + onAnimationStartCapture?: import("react").AnimationEventHandler | undefined; + onAnimationEnd?: import("react").AnimationEventHandler | undefined; + onAnimationEndCapture?: import("react").AnimationEventHandler | undefined; + onAnimationIteration?: import("react").AnimationEventHandler | undefined; + onAnimationIterationCapture?: import("react").AnimationEventHandler | undefined; + onTransitionEnd?: import("react").TransitionEventHandler | undefined; + onTransitionEndCapture?: import("react").TransitionEventHandler | undefined; + ref?: import("react").LegacyRef | undefined; + key?: import("react").Key | null | undefined; +}; +/** + * Given a record of HTML attributes, returns tho + * equivalent React props. + */ +export declare function htmlAttrsToReactProps(attrs: Record): HTMLProps; diff --git a/dist/types/selection/SelectionDOMObserver.d.ts b/dist/types/selection/SelectionDOMObserver.d.ts new file mode 100644 index 00000000..f9812245 --- /dev/null +++ b/dist/types/selection/SelectionDOMObserver.d.ts @@ -0,0 +1,34 @@ +import { EditorView } from "prosemirror-view"; +import { DOMSelectionRange } from "../dom.js"; +declare class SelectionState { + anchorNode: Node | null; + anchorOffset: number; + focusNode: Node | null; + focusOffset: number; + set(sel: DOMSelectionRange): void; + clear(): void; + eq(sel: DOMSelectionRange): boolean; +} +export declare class SelectionDOMObserver { + readonly view: EditorView; + flushingSoon: number; + currentSelection: SelectionState; + suppressingSelectionUpdates: boolean; + constructor(view: EditorView); + connectSelection(): void; + disconnectSelection(): void; + stop(): void; + start(): void; + suppressSelectionUpdates(): void; + setCurSelection(): void; + ignoreSelectionChange(sel: DOMSelectionRange): true | undefined; + registerMutation(): void; + flushSoon(): void; + updateSelection(): void; + selectionToDOM(): void; + flush(): void; + selectionChanged(sel: DOMSelectionRange): boolean; + forceFlush(): void; + onSelectionChange(): void; +} +export {}; diff --git a/dist/types/selection/hasFocusAndSelection.d.ts b/dist/types/selection/hasFocusAndSelection.d.ts new file mode 100644 index 00000000..025fe0e0 --- /dev/null +++ b/dist/types/selection/hasFocusAndSelection.d.ts @@ -0,0 +1,3 @@ +import { EditorView } from "prosemirror-view"; +export declare function hasFocusAndSelection(view: EditorView): boolean; +export declare function hasSelection(view: EditorView): boolean; diff --git a/dist/types/selection/selectionFromDOM.d.ts b/dist/types/selection/selectionFromDOM.d.ts new file mode 100644 index 00000000..0758b958 --- /dev/null +++ b/dist/types/selection/selectionFromDOM.d.ts @@ -0,0 +1,4 @@ +import { ResolvedPos } from "prosemirror-model"; +import { EditorView } from "prosemirror-view"; +export declare function selectionBetween(view: EditorView, $anchor: ResolvedPos, $head: ResolvedPos, bias?: number): import("prosemirror-state").Selection; +export declare function selectionFromDOM(view: EditorView, origin?: string | null): import("prosemirror-state").Selection | null; diff --git a/dist/types/selection/selectionToDOM.d.ts b/dist/types/selection/selectionToDOM.d.ts new file mode 100644 index 00000000..a7e74271 --- /dev/null +++ b/dist/types/selection/selectionToDOM.d.ts @@ -0,0 +1,9 @@ +import { Selection } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; +export declare const isEquivalentPosition: (node: Node, off: number, targetNode: Node, targetOff: number) => boolean; +export declare function hasBlockDesc(dom: Node): boolean | null | undefined; +export declare const domIndex: (node: Node) => number; +export declare function nodeSize(node: Node): number; +export declare function syncNodeSelection(view: EditorView, sel: Selection): void; +export declare function hasSelection(view: EditorView): boolean; +export declare function selectionToDOM(view: EditorView, force?: boolean): void; diff --git a/dist/types/testing/editorViewTestHelpers.d.ts b/dist/types/testing/editorViewTestHelpers.d.ts new file mode 100644 index 00000000..b04f3314 --- /dev/null +++ b/dist/types/testing/editorViewTestHelpers.d.ts @@ -0,0 +1,23 @@ +import { Node } from "prosemirror-model"; +import { doc } from "prosemirror-test-builder"; +import { EditorView as EditorViewT } from "prosemirror-view"; +import { Props } from "../components/ProseMirror.js"; +import { DOMNode } from "../dom.js"; +declare module "expect" { + interface AsymmetricMatchers { + toEqualNode(actual: Node): void; + } + interface Matchers { + toEqualNode(actual: Node): R; + } +} +export declare function tempEditor({ doc: startDoc, selection, controlled, plugins, ...props }: { + doc?: ReturnType; + selection?: Selection; + controlled?: boolean; +} & Omit): { + view: EditorViewT; + rerender: (props?: Omit) => void; + unmount: () => void; +}; +export declare function findTextNode(node: DOMNode, text: string): Text; diff --git a/dist/types/testing/setupProseMirrorView.d.ts b/dist/types/testing/setupProseMirrorView.d.ts new file mode 100644 index 00000000..3264b4c4 --- /dev/null +++ b/dist/types/testing/setupProseMirrorView.d.ts @@ -0,0 +1,2 @@ +export declare function setupProseMirrorView(): void; +export declare function teardownProseMirrorView(): void; diff --git a/dist/types/viewdesc.d.ts b/dist/types/viewdesc.d.ts new file mode 100644 index 00000000..d43a9ac8 --- /dev/null +++ b/dist/types/viewdesc.d.ts @@ -0,0 +1,130 @@ +import { Mark, Node, TagParseRule } from "prosemirror-model"; +import { Decoration, DecorationSource, EditorView } from "prosemirror-view"; +import { DOMNode } from "./dom.js"; +export declare class ViewDesc { + parent: ViewDesc | undefined; + children: ViewDesc[]; + pos: number; + dom: DOMNode; + contentDOM: HTMLElement | null; + dirty: number; + node: Node | null; + constructor(parent: ViewDesc | undefined, children: ViewDesc[], pos: number, dom: DOMNode, contentDOM: HTMLElement | null); + matchesWidget(_widget: Decoration): boolean; + matchesMark(_mark: Mark): boolean; + matchesNode(_node: Node, _outerDeco: readonly Decoration[], _innerDeco: DecorationSource): boolean; + matchesHack(nodeName: string): boolean; + parseRule(): Omit | null; + stopEvent(event: Event): boolean; + get size(): number; + get border(): number; + destroy(): void; + posBeforeChild(child: ViewDesc): number; + get posBefore(): number; + get posAtStart(): number; + get posAfter(): number; + get posAtEnd(): number; + localPosFromDOM(dom: DOMNode, offset: number, bias: number): number; + nearestDesc(dom: DOMNode): ViewDesc | undefined; + nearestDesc(dom: DOMNode, onlyNodes: true): NodeViewDesc | undefined; + getDesc(dom: DOMNode): ViewDesc | undefined; + posFromDOM(dom: DOMNode, offset: number, bias: number): number; + descAt(pos: number): ViewDesc | undefined; + domFromPos(pos: number, side: number): { + node: DOMNode; + offset: number; + atom?: number; + }; + parseRange(from: number, to: number, base?: number): { + node: DOMNode; + from: number; + to: number; + fromOffset: number; + toOffset: number; + }; + emptyChildAt(side: number): boolean; + domAfterPos(pos: number): DOMNode; + setSelection(anchor: number, head: number, root: Document | ShadowRoot, force?: boolean): void; + ignoreMutation(mutation: MutationRecord): boolean; + get contentLost(): boolean | null; + markDirty(from: number, to: number): void; + markParentsDirty(): void; + get domAtom(): boolean; + get ignoreForCoords(): boolean; +} +export declare class WidgetViewDesc extends ViewDesc { + widget: Decoration; + constructor(parent: ViewDesc | undefined, pos: number, widget: Decoration, dom: DOMNode); + matchesWidget(widget: Decoration): any; + parseRule(): { + ignore: boolean; + }; + stopEvent(event: Event): any; + ignoreMutation(mutation: MutationRecord): any; + get domAtom(): boolean; + get side(): number; +} +export declare class CompositionViewDesc extends ViewDesc { + textDOM: Text; + text: string; + constructor(parent: ViewDesc | undefined, pos: number, dom: DOMNode, textDOM: Text, text: string); + get size(): number; + localPosFromDOM(dom: DOMNode, offset: number): number; + domFromPos(pos: number): { + node: Text; + offset: number; + }; + ignoreMutation(mut: MutationRecord): boolean; +} +export declare class MarkViewDesc extends ViewDesc { + mark: Mark; + constructor(parent: ViewDesc | undefined, children: ViewDesc[], pos: number, mark: Mark, dom: DOMNode, contentDOM: HTMLElement); + parseRule(): { + mark: string; + attrs: import("prosemirror-model").Attrs; + contentElement: HTMLElement; + } | null; + matchesMark(mark: Mark): boolean; + markDirty(from: number, to: number): void; +} +export declare class NodeViewDesc extends ViewDesc { + node: Node; + outerDeco: readonly Decoration[]; + innerDeco: DecorationSource; + nodeDOM: DOMNode; + stopEvent: (event: Event) => boolean; + constructor(parent: ViewDesc | undefined, children: ViewDesc[], pos: number, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, dom: DOMNode, contentDOM: HTMLElement | null, nodeDOM: DOMNode, stopEvent: (event: Event) => boolean); + updateOuterDeco(): void; + parseRule(): Omit | null; + matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource): boolean; + get size(): number; + get border(): 1 | 0; + update(_node: Node, _outerDeco: readonly Decoration[], _innerDeco: DecorationSource, _view: EditorView): boolean; + selectNode(): void; + deselectNode(): void; + get domAtom(): boolean; +} +export declare class TextViewDesc extends NodeViewDesc { + constructor(parent: ViewDesc | undefined, children: ViewDesc[], pos: number, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, dom: DOMNode, nodeDOM: DOMNode); + parseRule(): { + skip: any; + }; + update(_node: Node, _outerDeco: readonly Decoration[], _innerDeco: DecorationSource, _view: EditorView): boolean; + inParent(): boolean; + domFromPos(pos: number): { + node: globalThis.Node; + offset: number; + }; + localPosFromDOM(dom: DOMNode, offset: number, bias: number): number; + ignoreMutation(mutation: MutationRecord): boolean; + markDirty(from: number, to: number): void; + get domAtom(): boolean; +} +export declare class TrailingHackViewDesc extends ViewDesc { + parseRule(): { + ignore: boolean; + }; + matchesHack(nodeName: string): boolean; + get domAtom(): boolean; + get ignoreForCoords(): boolean; +}