@stesura/react-wordgard 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/LICENSE.txt +21 -0
- package/README.md +912 -2
- package/THIRD_PARTY_LICENSES.txt +93 -0
- package/dist/ReactWordgard.d.ts +13 -0
- package/dist/ReactWordgard.js +94 -0
- package/dist/changes.d.ts +14 -0
- package/dist/changes.js +163 -0
- package/dist/components/ChildNodeViews.d.ts +8 -0
- package/dist/components/ChildNodeViews.js +36 -0
- package/dist/components/DecorationRoot.d.ts +13 -0
- package/dist/components/DecorationRoot.js +31 -0
- package/dist/components/ErrorBoundary.d.ts +18 -0
- package/dist/components/ErrorBoundary.js +14 -0
- package/dist/components/SeparatorHackView.d.ts +2 -0
- package/dist/components/SeparatorHackView.js +4 -0
- package/dist/components/ShapeView.d.ts +12 -0
- package/dist/components/ShapeView.js +24 -0
- package/dist/components/TextNodeView.d.ts +5 -0
- package/dist/components/TextNodeView.js +4 -0
- package/dist/components/TrailingHackView.d.ts +2 -0
- package/dist/components/TrailingHackView.js +4 -0
- package/dist/components/WidgetView.d.ts +10 -0
- package/dist/components/WidgetView.js +32 -0
- package/dist/components/WordgardDoc.d.ts +9 -0
- package/dist/components/WordgardDoc.js +36 -0
- package/dist/components/WordgardEditor.d.ts +15 -0
- package/dist/components/WordgardEditor.js +17 -0
- package/dist/components/WordgardScope.d.ts +14 -0
- package/dist/components/WordgardScope.js +14 -0
- package/dist/components/WrapperView.d.ts +12 -0
- package/dist/components/WrapperView.js +23 -0
- package/dist/components/marks/MarkView.d.ts +11 -0
- package/dist/components/marks/MarkView.js +10 -0
- package/dist/components/marks/ReactMarkView.d.ts +10 -0
- package/dist/components/marks/ReactMarkView.js +30 -0
- package/dist/components/nodes/DefaultNodeView.d.ts +16 -0
- package/dist/components/nodes/DefaultNodeView.js +112 -0
- package/dist/components/nodes/DocNodeView.d.ts +7 -0
- package/dist/components/nodes/DocNodeView.js +5 -0
- package/dist/composition.d.ts +23 -0
- package/dist/composition.js +95 -0
- package/dist/contexts/DecorationContext.d.ts +15 -0
- package/dist/contexts/DecorationContext.js +35 -0
- package/dist/contexts/DecorationRootContext.d.ts +6 -0
- package/dist/contexts/DecorationRootContext.js +4 -0
- package/dist/contexts/EditorContext.d.ts +27 -0
- package/dist/contexts/EditorContext.js +87 -0
- package/dist/contexts/RenderStoreContext.d.ts +15 -0
- package/dist/contexts/RenderStoreContext.js +41 -0
- package/dist/contexts/editorEvents.d.ts +12 -0
- package/dist/contexts/editorEvents.js +92 -0
- package/dist/decorations/DecoIterator.d.ts +43 -0
- package/dist/decorations/DecoIterator.js +462 -0
- package/dist/decorations/ReactMarkType.d.ts +41 -0
- package/dist/decorations/ReactMarkType.js +47 -0
- package/dist/decorations/ReactShapeType.d.ts +65 -0
- package/dist/decorations/ReactShapeType.js +202 -0
- package/dist/decorations/ReactWidgetType.d.ts +29 -0
- package/dist/decorations/ReactWidgetType.js +67 -0
- package/dist/decorations/ReactWrapperType.d.ts +31 -0
- package/dist/decorations/ReactWrapperType.js +41 -0
- package/dist/decorations/iterDeco.d.ts +82 -0
- package/dist/decorations/iterDeco.js +406 -0
- package/dist/helpers/attributes.d.ts +2 -0
- package/dist/helpers/attributes.js +11 -0
- package/dist/helpers/index.d.ts +2 -0
- package/dist/helpers/index.js +2 -0
- package/dist/helpers/props.d.ts +8 -0
- package/dist/helpers/props.js +248 -0
- package/dist/helpers/refs.d.ts +6 -0
- package/dist/helpers/refs.js +20 -0
- package/dist/hooks/useEditorEffect.d.ts +10 -0
- package/dist/hooks/useEditorEffect.js +28 -0
- package/dist/hooks/useEditorEventCallback.d.ts +8 -0
- package/dist/hooks/useEditorEventCallback.js +17 -0
- package/dist/hooks/useEditorEventListener.d.ts +10 -0
- package/dist/hooks/useEditorEventListener.js +26 -0
- package/dist/hooks/useEditorLayoutEffect.d.ts +15 -0
- package/dist/hooks/useEditorLayoutEffect.js +34 -0
- package/dist/hooks/useEditorState.d.ts +6 -0
- package/dist/hooks/useEditorState.js +12 -0
- package/dist/hooks/useEditorStateSelector.d.ts +7 -0
- package/dist/hooks/useEditorStateSelector.js +11 -0
- package/dist/hooks/useIgnoreMutation.d.ts +6 -0
- package/dist/hooks/useIgnoreMutation.js +19 -0
- package/dist/hooks/useIsNodeSelected.d.ts +2 -0
- package/dist/hooks/useIsNodeSelected.js +5 -0
- package/dist/hooks/useMergedDOMRefs.d.ts +6 -0
- package/dist/hooks/useMergedDOMRefs.js +18 -0
- package/dist/hooks/useNodePos.d.ts +4 -0
- package/dist/hooks/useNodePos.js +7 -0
- package/dist/hooks/useSelectNode.d.ts +6 -0
- package/dist/hooks/useSelectNode.js +25 -0
- package/dist/hooks/useStopEvent.d.ts +8 -0
- package/dist/hooks/useStopEvent.js +21 -0
- package/dist/hooks/useWordgard.d.ts +2 -0
- package/dist/hooks/useWordgard.js +3 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/reactKeys.d.ts +39 -0
- package/dist/reactKeys.js +188 -0
- package/dist/tile.d.ts +32 -0
- package/dist/tile.js +1319 -0
- package/dist/viewRoots.d.ts +2 -0
- package/dist/viewRoots.js +7 -0
- package/dist/wordgardInternals.d.ts +211 -0
- package/dist/wordgardInternals.js +82 -0
- package/package.json +119 -4
- package/skills/react-wordgard/SKILL.md +525 -0
- package/skills/react-wordgard/references/wordgard.md +189 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Named after react-prosemirror's ProseMirrorDoc, rewritten for Wordgard. The portal commits first
|
|
2
|
+
// and builds the tiles; attaching the element then connects the editor, as Wordgard's own editor is
|
|
3
|
+
// built before it's connected.
|
|
4
|
+
import React, { useCallback, useInsertionEffect, useLayoutEffect, useSyncExternalStore, } from "react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
import { editorOf, useEditorContext } from "../contexts/EditorContext.js";
|
|
7
|
+
import { docPortal } from "../tile.js";
|
|
8
|
+
const DocPortal = ({ wg }) => {
|
|
9
|
+
const portal = docPortal(wg.contentDOM);
|
|
10
|
+
const element = useSyncExternalStore(portal.subscribe, portal.get);
|
|
11
|
+
// Insertion effects all run before any layout effect, so a second portal
|
|
12
|
+
// committed alongside this one has claimed the editor by the time `mount`
|
|
13
|
+
// builds the tiles. StrictMode doesn't run them twice.
|
|
14
|
+
useInsertionEffect(() => portal.claim(), [portal]);
|
|
15
|
+
useLayoutEffect(() => portal.committed(element));
|
|
16
|
+
useLayoutEffect(() => portal.mount(), [portal]);
|
|
17
|
+
return createPortal(element, wg.contentDOM);
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Puts the editor in the page, inside a `div` taking the props, and renders the document through a
|
|
21
|
+
* portal, in your React tree. It goes under its `<WordgardEditor>`, inside any other components,
|
|
22
|
+
* with other children before or after it. One per editor: a second throws.
|
|
23
|
+
*/
|
|
24
|
+
export const WordgardDoc = (props) => {
|
|
25
|
+
const { wg } = useEditorContext();
|
|
26
|
+
const attach = useCallback((dom) => {
|
|
27
|
+
if (!dom)
|
|
28
|
+
return;
|
|
29
|
+
dom.appendChild(wg.dom);
|
|
30
|
+
// Effects waiting for the editor to be in the page.
|
|
31
|
+
editorOf(wg).requestEffects();
|
|
32
|
+
}, [wg]);
|
|
33
|
+
return (React.createElement(React.Fragment, null,
|
|
34
|
+
React.createElement(DocPortal, { wg: wg }),
|
|
35
|
+
React.createElement("div", { ...props, ref: attach })));
|
|
36
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { Wordgard } from "wordgard/editor";
|
|
3
|
+
type Props = Omit<Wordgard.Spec, "parent"> & {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Wordgard editor from the spec (`doc`, `config`, `selection`, `state`, `scrollTo`) and
|
|
8
|
+
* provides it to its children. It draws no DOM of its own. The spec is read once, on mount: from
|
|
9
|
+
* then on the editor owns its state (see [Accessing the state outside the
|
|
10
|
+
* editor](https://github.com/stesura/react-wordgard#accessing-the-state-outside-the-editor)). In
|
|
11
|
+
* React's StrictMode (development only), it creates its editor twice and drops one, which is never
|
|
12
|
+
* put in the page.
|
|
13
|
+
*/
|
|
14
|
+
export declare const WordgardEditor: ({ children, ...spec }: Props) => React.JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Named after react-prosemirror's ProseMirror component, rewritten for Wordgard. The spec is read
|
|
2
|
+
// once, as react-prosemirror's `defaultState`: Wordgard's editor owns its state.
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { ReactWordgard } from "../ReactWordgard.js";
|
|
5
|
+
import { EditorContext, editorOf } from "../contexts/EditorContext.js";
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Wordgard editor from the spec (`doc`, `config`, `selection`, `state`, `scrollTo`) and
|
|
8
|
+
* provides it to its children. It draws no DOM of its own. The spec is read once, on mount: from
|
|
9
|
+
* then on the editor owns its state (see [Accessing the state outside the
|
|
10
|
+
* editor](https://github.com/stesura/react-wordgard#accessing-the-state-outside-the-editor)). In
|
|
11
|
+
* React's StrictMode (development only), it creates its editor twice and drops one, which is never
|
|
12
|
+
* put in the page.
|
|
13
|
+
*/
|
|
14
|
+
export const WordgardEditor = ({ children, ...spec }) => {
|
|
15
|
+
const [editor] = useState(() => editorOf(ReactWordgard.host(spec)));
|
|
16
|
+
return React.createElement(EditorContext.Provider, { value: editor }, children);
|
|
17
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { Wordgard } from "wordgard/editor";
|
|
3
|
+
type Props = {
|
|
4
|
+
wg: Wordgard;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Gives the components it holds `wg`, an editor a `WordgardEditor` made elsewhere, in place of the
|
|
9
|
+
* one around them: for a toolbar acting on whichever of a page's editors has focus. The hooks under
|
|
10
|
+
* it follow when `wg` changes: selectors subscribe to the new editor's state, callbacks and
|
|
11
|
+
* listeners get it, effects run again. Any other editor throws.
|
|
12
|
+
*/
|
|
13
|
+
export declare const WordgardScope: ({ wg, children }: Props) => React.JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { EditorContext, findEditor } from "../contexts/EditorContext.js";
|
|
3
|
+
/**
|
|
4
|
+
* Gives the components it holds `wg`, an editor a `WordgardEditor` made elsewhere, in place of the
|
|
5
|
+
* one around them: for a toolbar acting on whichever of a page's editors has focus. The hooks under
|
|
6
|
+
* it follow when `wg` changes: selectors subscribe to the new editor's state, callbacks and
|
|
7
|
+
* listeners get it, effects run again. Any other editor throws.
|
|
8
|
+
*/
|
|
9
|
+
export const WordgardScope = ({ wg, children }) => {
|
|
10
|
+
const editor = findEditor(wg);
|
|
11
|
+
if (!editor)
|
|
12
|
+
throw new Error("WordgardScope takes an editor a WordgardEditor made");
|
|
13
|
+
return React.createElement(EditorContext.Provider, { value: editor }, children);
|
|
14
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { Node } from "wordgard/doc";
|
|
3
|
+
import type { DecoElt } from "../wordgardInternals.js";
|
|
4
|
+
type Props = {
|
|
5
|
+
elt: DecoElt;
|
|
6
|
+
node: Node;
|
|
7
|
+
wrapped: (ref: (dom: Element | null) => void) => ReactNode;
|
|
8
|
+
selected: boolean;
|
|
9
|
+
outerRef?: ((dom: Element | null) => void) | undefined;
|
|
10
|
+
};
|
|
11
|
+
export declare const WrapperView: ({ elt, node, wrapped, selected, outerRef }: Props) => React.JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Like ShapeView, no effects: ReactDocTile builds the tiles over what it commits.
|
|
2
|
+
import React, { useMemo, useState } from "react";
|
|
3
|
+
import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
|
|
4
|
+
import { createWrapperHandle, nodePos, shapeAttributes } from "../decorations/ReactShapeType.js";
|
|
5
|
+
import { drawWrapper } from "../decorations/ReactWrapperType.js";
|
|
6
|
+
import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
|
|
7
|
+
import { attrsToRecord, componentProps } from "../helpers/index.js";
|
|
8
|
+
import { mergeRefs } from "../helpers/refs.js";
|
|
9
|
+
export const WrapperView = ({ elt, node, wrapped, selected, outerRef }) => {
|
|
10
|
+
const [{ handle, ref, wrappedRef }] = useState(createWrapperHandle);
|
|
11
|
+
const [handlers] = useState(() => createDecorationHandlers(nodePos(handle)));
|
|
12
|
+
const rootRef = useMemo(() => mergeRefs(ref, outerRef, decorationRoot(handlers)), [ref, outerRef, handlers]);
|
|
13
|
+
const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
|
|
14
|
+
const attrs = useMemo(() => attrsToRecord(shapeAttributes(elt)), [elt]);
|
|
15
|
+
const root = useMemo(() => ({ tagName: elt.tagName, attrs, ref: rootRef }), [elt.tagName, attrs, rootRef]);
|
|
16
|
+
return (React.createElement(DecorationContext.Provider, { value: decoration },
|
|
17
|
+
React.createElement(DecorationRootContext.Provider, { value: root }, drawWrapper(elt, {
|
|
18
|
+
attrs: componentProps(attrs),
|
|
19
|
+
nodeProps: { node, getPos: handlers.getPos },
|
|
20
|
+
children: wrapped(wrappedRef),
|
|
21
|
+
ref: rootRef,
|
|
22
|
+
}))));
|
|
23
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Mark } from "wordgard/doc";
|
|
3
|
+
import { type ChildView } from "../../decorations/iterDeco.js";
|
|
4
|
+
import type { DecoElt } from "../../wordgardInternals.js";
|
|
5
|
+
type Props = {
|
|
6
|
+
elt: DecoElt;
|
|
7
|
+
views: readonly ChildView[];
|
|
8
|
+
mark?: Mark | undefined;
|
|
9
|
+
};
|
|
10
|
+
export declare const MarkView: React.MemoExoticComponent<({ elt, views, mark }: Props) => React.JSX.Element>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Named after react-prosemirror's MarkView, rewritten for Wordgard: an element-shaped mark, the
|
|
2
|
+
// span of a text run's attribute-shaped marks, or a React mark. Memoised: iterDeco keeps a
|
|
3
|
+
// wrapper's unchanged views.
|
|
4
|
+
import React, { createElement, memo } from "react";
|
|
5
|
+
import { shapeOf } from "../../decorations/iterDeco.js";
|
|
6
|
+
import { attrsToRecord, htmlAttrsToReactProps } from "../../helpers/index.js";
|
|
7
|
+
import { renderChildViews } from "../ChildNodeViews.js";
|
|
8
|
+
import { viewRoot } from "../../viewRoots.js";
|
|
9
|
+
import { ReactMarkView } from "./ReactMarkView.js";
|
|
10
|
+
export const MarkView = memo(({ elt, views, mark }) => mark ? (React.createElement(ReactMarkView, { elt: elt, mark: mark }, renderChildViews(views))) : (createElement(elt.tagName, { ...htmlAttrsToReactProps(attrsToRecord(elt.attrs)), ref: viewRoot }, renderChildViews(views))), (a, b) => shapeOf(a.views) == shapeOf(b.views) && a.elt.eq(b.elt) && a.mark == b.mark);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { Mark } from "wordgard/doc";
|
|
3
|
+
import type { DecoElt } from "../../wordgardInternals.js";
|
|
4
|
+
type Props = {
|
|
5
|
+
elt: DecoElt;
|
|
6
|
+
mark: Mark;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
export declare const ReactMarkView: ({ elt, mark, children }: Props) => React.JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Named after react-prosemirror's ReactMarkView, rewritten for Wordgard. Like ShapeView, no
|
|
2
|
+
// effects: ReactDocTile builds the tiles over what it commits.
|
|
3
|
+
import React, { useMemo, useState } from "react";
|
|
4
|
+
import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../../contexts/DecorationContext.js";
|
|
5
|
+
import { drawMark } from "../../decorations/ReactMarkType.js";
|
|
6
|
+
import { createShapeHandle, shapeAttributes } from "../../decorations/ReactShapeType.js";
|
|
7
|
+
import { DecorationRootContext } from "../../contexts/DecorationRootContext.js";
|
|
8
|
+
import { attrsToRecord, componentProps } from "../../helpers/index.js";
|
|
9
|
+
import { mergeRefs } from "../../helpers/refs.js";
|
|
10
|
+
export const ReactMarkView = ({ elt, mark, children }) => {
|
|
11
|
+
const [{ handle, ref, contentDOMRef }] = useState(createShapeHandle);
|
|
12
|
+
const [handlers] = useState(() => createDecorationHandlers(() => {
|
|
13
|
+
const tile = handle.root?.isConnected ? handle.root.wgTile : undefined;
|
|
14
|
+
if (!tile)
|
|
15
|
+
throw new Error("This React mark isn't drawn");
|
|
16
|
+
return tile.posBefore;
|
|
17
|
+
}));
|
|
18
|
+
const rootRef = useMemo(() => mergeRefs(ref, decorationRoot(handlers)), [ref, handlers]);
|
|
19
|
+
// A mark is no node: never selected.
|
|
20
|
+
const decoration = useMemo(() => ({ handlers, selected: false }), [handlers]);
|
|
21
|
+
const attrs = useMemo(() => attrsToRecord(shapeAttributes(elt)), [elt]);
|
|
22
|
+
const root = useMemo(() => ({ tagName: elt.tagName, attrs, ref: rootRef }), [elt.tagName, attrs, rootRef]);
|
|
23
|
+
return (React.createElement(DecorationContext.Provider, { value: decoration },
|
|
24
|
+
React.createElement(DecorationRootContext.Provider, { value: root }, drawMark(elt, {
|
|
25
|
+
attrs: componentProps(attrs),
|
|
26
|
+
markProps: { mark, getPos: handlers.getPos, contentDOMRef },
|
|
27
|
+
children,
|
|
28
|
+
ref: rootRef,
|
|
29
|
+
}))));
|
|
30
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React, { Component } from "react";
|
|
2
|
+
import type { NodeCell } from "../../contexts/RenderStoreContext.js";
|
|
3
|
+
type Props = {
|
|
4
|
+
cell: NodeCell;
|
|
5
|
+
};
|
|
6
|
+
export declare class DefaultNodeView extends Component<Props> {
|
|
7
|
+
private drawn;
|
|
8
|
+
private readonly redraw;
|
|
9
|
+
shouldComponentUpdate(next: Props): boolean;
|
|
10
|
+
componentDidMount(): void;
|
|
11
|
+
componentDidUpdate(prev: Props): void;
|
|
12
|
+
componentWillUnmount(): void;
|
|
13
|
+
private listen;
|
|
14
|
+
render(): React.JSX.Element | null;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Named after react-prosemirror's DefaultNodeView, rewritten for Wordgard: a node renders as its
|
|
2
|
+
// decorated shape (buildNodeShape). Every Wordgard node type has a shape, so there's no missing
|
|
3
|
+
// `toDOM` to check for. Native widgets draw nothing: ReactDocTile puts their DOM in place.
|
|
4
|
+
import React, { Component, createElement } from "react";
|
|
5
|
+
import { Attributes, Elt } from "wordgard/doc";
|
|
6
|
+
import { isReactShape, shapeAttributes } from "../../decorations/ReactShapeType.js";
|
|
7
|
+
import { attributedWidget, isReactWidget } from "../../decorations/ReactWidgetType.js";
|
|
8
|
+
import { isReactWrapper } from "../../decorations/ReactWrapperType.js";
|
|
9
|
+
import { uneditable } from "../../decorations/iterDeco.js";
|
|
10
|
+
import { attrsToRecord, htmlAttrsToReactProps } from "../../helpers/index.js";
|
|
11
|
+
import { ChildNodeViews } from "../ChildNodeViews.js";
|
|
12
|
+
import { ShapeView } from "../ShapeView.js";
|
|
13
|
+
import { WidgetView } from "../WidgetView.js";
|
|
14
|
+
import { WrapperView } from "../WrapperView.js";
|
|
15
|
+
import { viewRoot } from "../../viewRoots.js";
|
|
16
|
+
const renderShape = (draw, shape, inEditable, key, ref) => {
|
|
17
|
+
const { node, content, selected } = draw;
|
|
18
|
+
const widget = attributedWidget(shape);
|
|
19
|
+
if (widget)
|
|
20
|
+
return (React.createElement(WidgetView, { key: key, widget: widget, attrs: shapeAttributes(shape), selected: selected, outerRef: ref }));
|
|
21
|
+
const { shape: elt, inEditable: inner } = uneditable(shape, inEditable);
|
|
22
|
+
// Wordgard fills a wrapper's hole with what it wraps, which a React shape
|
|
23
|
+
// would drop.
|
|
24
|
+
if (isReactShape(elt) && (elt.children.length > 1 || elt.children.some((child) => child !== 0)))
|
|
25
|
+
throw new Error("A React shape can't wrap: use createWrapper");
|
|
26
|
+
if (isReactShape(elt))
|
|
27
|
+
return (React.createElement(ShapeView, { key: key, elt: elt, node: node, content: content, selected: selected, outerRef: ref }));
|
|
28
|
+
if (isReactWrapper(elt))
|
|
29
|
+
return (React.createElement(WrapperView, { key: key, elt: elt, node: node, wrapped: (wrappedRef) => renderWrapped(draw, elt, inner, wrappedRef), selected: selected, outerRef: ref }));
|
|
30
|
+
const props = {
|
|
31
|
+
key,
|
|
32
|
+
ref,
|
|
33
|
+
suppressContentEditableWarning: true,
|
|
34
|
+
...htmlAttrsToReactProps(attrsToRecord(elt.attrs)),
|
|
35
|
+
};
|
|
36
|
+
return createElement(elt.tagName, props, ...elt.children.map((child, i) => {
|
|
37
|
+
if (child === 0)
|
|
38
|
+
return content;
|
|
39
|
+
if (typeof child == "string")
|
|
40
|
+
return child;
|
|
41
|
+
if (child instanceof Elt)
|
|
42
|
+
return renderShape(draw, child, inner, i);
|
|
43
|
+
return isReactWidget(child) ? (React.createElement(WidgetView, { key: i, widget: child, selected: selected })) : null;
|
|
44
|
+
}));
|
|
45
|
+
};
|
|
46
|
+
// What a React wrapper wraps: its hole holds one element or widget, whose root
|
|
47
|
+
// the wrapper's tiles are built down to. A native widget's DOM is placed after
|
|
48
|
+
// the commit, where React drew nothing: there'd be no telling where the
|
|
49
|
+
// component put its children.
|
|
50
|
+
const renderWrapped = (draw, wrapper, inEditable, ref) => {
|
|
51
|
+
const [child, ...rest] = wrapper.children;
|
|
52
|
+
if (child === 0)
|
|
53
|
+
throw new Error("A React wrapper isn't a shape: use createShape");
|
|
54
|
+
if (rest.length || child === undefined || typeof child == "string")
|
|
55
|
+
throw new Error("A React wrapper's hole holds one element or widget");
|
|
56
|
+
if (child instanceof Elt)
|
|
57
|
+
return renderShape(draw, child, inEditable, undefined, ref);
|
|
58
|
+
if (!isReactWidget(child))
|
|
59
|
+
throw new Error(`A React wrapper can't wrap a native widget: draw ${draw.node.name} with a React widget`);
|
|
60
|
+
return React.createElement(WidgetView, { widget: child, selected: draw.selected, outerRef: ref });
|
|
61
|
+
};
|
|
62
|
+
// Wordgard draws a node selection as a class on the node's shape: on its outer
|
|
63
|
+
// element (a widget-drawn node gets one around it), or inside a wrapper a point
|
|
64
|
+
// decoration added after it.
|
|
65
|
+
const isSelected = (shape) => (Attributes.get(shape.attrs, "class") ?? "").split(" ").includes("wg-selected-node") ||
|
|
66
|
+
shape.children.some((child) => child instanceof Elt && isSelected(child));
|
|
67
|
+
const draw = ({ node, shape, children }) => {
|
|
68
|
+
if (!(shape instanceof Elt))
|
|
69
|
+
return isReactWidget(shape) ? React.createElement(WidgetView, { widget: shape }) : null;
|
|
70
|
+
// Keyed, so React keeps the content where a shape moves its hole: the tile
|
|
71
|
+
// build counts on an element React kept keeping its content.
|
|
72
|
+
const content = node.isPlot && children ? React.createElement(ChildNodeViews, { key: "content", views: children }) : null;
|
|
73
|
+
return renderShape({ node, content, selected: isSelected(shape) }, shape, true, undefined, viewRoot);
|
|
74
|
+
};
|
|
75
|
+
// Renders when its cell gets a new view, not when its list renders again:
|
|
76
|
+
// Wordgard's documents share unchanged nodes between states, and their layouts
|
|
77
|
+
// are kept outside redrawn ranges. A class, so a node's listener costs no hook
|
|
78
|
+
// state and an update no effect (useSyncExternalStore made Bold all over 5,000
|
|
79
|
+
// paragraphs about 15% slower, and held more memory).
|
|
80
|
+
export class DefaultNodeView extends Component {
|
|
81
|
+
// What it drew last, to catch a view its cell got before it listened.
|
|
82
|
+
drawn = null;
|
|
83
|
+
redraw = () => this.forceUpdate();
|
|
84
|
+
shouldComponentUpdate(next) {
|
|
85
|
+
return next.cell != this.props.cell;
|
|
86
|
+
}
|
|
87
|
+
componentDidMount() {
|
|
88
|
+
this.listen();
|
|
89
|
+
}
|
|
90
|
+
componentDidUpdate(prev) {
|
|
91
|
+
if (prev.cell == this.props.cell)
|
|
92
|
+
return;
|
|
93
|
+
if (prev.cell.listener == this.redraw)
|
|
94
|
+
prev.cell.listener = null;
|
|
95
|
+
this.listen();
|
|
96
|
+
}
|
|
97
|
+
componentWillUnmount() {
|
|
98
|
+
const { cell } = this.props;
|
|
99
|
+
if (cell.listener == this.redraw)
|
|
100
|
+
cell.listener = null;
|
|
101
|
+
}
|
|
102
|
+
listen() {
|
|
103
|
+
const { cell } = this.props;
|
|
104
|
+
cell.listener = this.redraw;
|
|
105
|
+
if (cell.view != this.drawn)
|
|
106
|
+
this.forceUpdate();
|
|
107
|
+
}
|
|
108
|
+
render() {
|
|
109
|
+
this.drawn = this.props.cell.view;
|
|
110
|
+
return draw(this.drawn);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Named after react-prosemirror's DocNodeView, rewritten for Wordgard: renders straight into
|
|
2
|
+
// Wordgard's content element, React's container.
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { ChildNodeViews } from "../ChildNodeViews.js";
|
|
5
|
+
export const DocNodeView = ({ views }) => React.createElement(ChildNodeViews, { views: views });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Elt, type Mark, type Plot } from "wordgard/doc";
|
|
2
|
+
import { type CompositionLayout } from "./decorations/iterDeco.js";
|
|
3
|
+
import { type CompositionInfo } from "./wordgardInternals.js";
|
|
4
|
+
export type RenderedText = {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly text: string;
|
|
7
|
+
};
|
|
8
|
+
export type Rendered = {
|
|
9
|
+
readonly texts: WeakMap<Text, RenderedText>;
|
|
10
|
+
readonly wrappers: WeakMap<Element, string>;
|
|
11
|
+
};
|
|
12
|
+
export type CompositionView = {
|
|
13
|
+
readonly plot: Plot;
|
|
14
|
+
readonly path: ReadonlySet<Plot>;
|
|
15
|
+
readonly layout: CompositionLayout;
|
|
16
|
+
readonly target: Text | null;
|
|
17
|
+
readonly node: ChildNode | null;
|
|
18
|
+
readonly text: string;
|
|
19
|
+
readonly wrapCursor: Mark.Set | null;
|
|
20
|
+
readonly rendered: RenderedText | null;
|
|
21
|
+
};
|
|
22
|
+
export declare const compositionView: (doc: Plot, info: CompositionInfo, changes: readonly number[], rendered: Rendered) => CompositionView | null;
|
|
23
|
+
export declare const browserElt: (dom: Element) => Elt<never>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Keeps a composition alive through Wordgard's flush, as Wordgard's ContentUpdate.composition does:
|
|
2
|
+
// the browser's text node stays, in a tile of its own, and React must not write to it. When React
|
|
3
|
+
// drew it, React keeps rendering what it last committed there; when the browser made it, React
|
|
4
|
+
// draws nothing in its place.
|
|
5
|
+
import { Attributes, Elt } from "wordgard/doc";
|
|
6
|
+
import { cursorWrappers, } from "./decorations/iterDeco.js";
|
|
7
|
+
import { markOfTile, markTiles } from "./decorations/ReactMarkType.js";
|
|
8
|
+
import { TileFlag, getTileClasses } from "./wordgardInternals.js";
|
|
9
|
+
// The innermost plot holding [from, to), and the plots down to it.
|
|
10
|
+
const plotAround = (doc, from, to) => {
|
|
11
|
+
const path = new Set();
|
|
12
|
+
let plot = doc;
|
|
13
|
+
let start = 0;
|
|
14
|
+
for (;;) {
|
|
15
|
+
let pos = start;
|
|
16
|
+
let next = null;
|
|
17
|
+
for (const child of plot.content) {
|
|
18
|
+
if (child.isPlot && pos + 1 <= from && to <= pos + child.length - 1) {
|
|
19
|
+
next = { plot: child, start: pos + 1 };
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
pos += child.length;
|
|
23
|
+
}
|
|
24
|
+
if (!next)
|
|
25
|
+
return { plot, start, path };
|
|
26
|
+
path.add(next.plot);
|
|
27
|
+
({ plot, start } = next);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// An element React put there: one of its wrappers, a React mark's element, or
|
|
31
|
+
// what holds a plot's content.
|
|
32
|
+
const isReacts = (dom, rendered) => (dom instanceof Element && rendered.wrappers.has(dom)) ||
|
|
33
|
+
!!dom.wgTile?.isDoc ||
|
|
34
|
+
!!(dom.wgTile && (dom.wgTile.flags & TileFlag.PlotContent || markTiles.has(dom.wgTile)));
|
|
35
|
+
// The outermost node around `target` that React didn't put there.
|
|
36
|
+
const browserMade = (target, rendered) => {
|
|
37
|
+
let node = target;
|
|
38
|
+
while (node.parentNode instanceof Element && !isReacts(node.parentNode, rendered))
|
|
39
|
+
node = node.parentNode;
|
|
40
|
+
return node;
|
|
41
|
+
};
|
|
42
|
+
// The wrappers `node` sits in, outermost first, as the tiles over them say
|
|
43
|
+
// (ContentUpdate.composition walks up to the node), with React's keys. A React
|
|
44
|
+
// mark's elements are one wrapper, its root's.
|
|
45
|
+
const domWrappers = (node, rendered) => {
|
|
46
|
+
const wrappers = [];
|
|
47
|
+
for (let dom = node.parentNode; dom instanceof Element; dom = dom.parentNode) {
|
|
48
|
+
const tile = dom.wgTile;
|
|
49
|
+
if (!(tile instanceof getTileClasses().EltTile) || !(tile.flags & TileFlag.Wrapper))
|
|
50
|
+
break;
|
|
51
|
+
const mark = markOfTile.get(tile);
|
|
52
|
+
if (markTiles.has(tile) && !mark)
|
|
53
|
+
continue;
|
|
54
|
+
const key = rendered.wrappers.get(dom);
|
|
55
|
+
wrappers.unshift({ elt: tile.elt, spanning: tile.isSpanning, key, ...(mark && { mark }) });
|
|
56
|
+
}
|
|
57
|
+
return wrappers;
|
|
58
|
+
};
|
|
59
|
+
// Wordgard's separateChange fails, and the flush drops the composition, when a
|
|
60
|
+
// replaced range reaches across the composition's edges.
|
|
61
|
+
const separates = (changes, fromB, toB) => {
|
|
62
|
+
for (let i = 0, posB = 0; i < changes.length; i += 2) {
|
|
63
|
+
const len = changes[i] ?? 0;
|
|
64
|
+
const ins = changes[i + 1] ?? 0;
|
|
65
|
+
const endB = posB + (ins < 0 ? len : ins);
|
|
66
|
+
if (ins >= 0 && fromB < endB && toB > posB && (posB < fromB || endB > toB))
|
|
67
|
+
return false;
|
|
68
|
+
posB = endB;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
export const compositionView = (doc, info, changes, rendered) => {
|
|
73
|
+
if (!separates(changes, info.fromB, info.toB))
|
|
74
|
+
return null;
|
|
75
|
+
const { plot, start, path } = plotAround(doc, info.fromB, info.toB);
|
|
76
|
+
const node = info.target && browserMade(info.target, rendered);
|
|
77
|
+
return {
|
|
78
|
+
plot,
|
|
79
|
+
path,
|
|
80
|
+
layout: {
|
|
81
|
+
from: info.fromB - start,
|
|
82
|
+
to: info.toB - start,
|
|
83
|
+
wrappers: node ? domWrappers(node, rendered) : cursorWrappers(info.wrapCursor ?? []),
|
|
84
|
+
cursor: !info.target,
|
|
85
|
+
},
|
|
86
|
+
target: info.target,
|
|
87
|
+
node,
|
|
88
|
+
text: info.text,
|
|
89
|
+
wrapCursor: info.wrapCursor ?? null,
|
|
90
|
+
rendered: (info.target && node == info.target && rendered.texts.get(info.target)) || null,
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
// The tile Wordgard's ContentUpdate.composition makes for an element the
|
|
94
|
+
// browser put around the composed text: its tag and attributes, a content hole.
|
|
95
|
+
export const browserElt = (dom) => Elt.create(dom.nodeName.toLowerCase(), Attributes.read(Object.fromEntries(dom.getAttributeNames().map((name) => [name, dom.getAttribute(name)]))), [0]);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type DecorationHandlers = {
|
|
2
|
+
readonly getPos: () => number;
|
|
3
|
+
readonly stopEvent: Set<(event: Event) => boolean>;
|
|
4
|
+
readonly ignoreMutation: Set<(record: MutationRecord) => boolean>;
|
|
5
|
+
};
|
|
6
|
+
export type DecorationContextValue = {
|
|
7
|
+
readonly handlers: DecorationHandlers;
|
|
8
|
+
readonly selected: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare const DecorationContext: import("react").Context<DecorationContextValue | null>;
|
|
11
|
+
export declare const useDecorationContext: (hook: string) => DecorationContextValue;
|
|
12
|
+
export declare const createDecorationHandlers: (getPos: () => number) => DecorationHandlers;
|
|
13
|
+
export declare const decorationRoot: (handlers: DecorationHandlers) => (dom: Element | null) => void;
|
|
14
|
+
export declare const stopsEvent: (event: Event) => boolean;
|
|
15
|
+
export declare const ignoresMutation: (record: MutationRecord) => boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The React decoration a component is drawn in, for the decoration hooks. Replaces
|
|
2
|
+
// react-prosemirror's GetPosContext, SelectNodeContext, StopEventContext and IgnoreMutationContext.
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
export const DecorationContext = createContext(null);
|
|
5
|
+
export const useDecorationContext = (hook) => {
|
|
6
|
+
const context = useContext(DecorationContext);
|
|
7
|
+
if (!context)
|
|
8
|
+
throw new Error(`${hook} is called outside a React shape, wrapper, mark or widget`);
|
|
9
|
+
return context;
|
|
10
|
+
};
|
|
11
|
+
export const createDecorationHandlers = (getPos) => ({
|
|
12
|
+
getPos,
|
|
13
|
+
stopEvent: new Set(),
|
|
14
|
+
ignoreMutation: new Set(),
|
|
15
|
+
});
|
|
16
|
+
// The handlers of the decorations whose root elements they are.
|
|
17
|
+
const roots = new WeakMap();
|
|
18
|
+
export const decorationRoot = (handlers) => (dom) => {
|
|
19
|
+
if (dom)
|
|
20
|
+
roots.set(dom, handlers);
|
|
21
|
+
};
|
|
22
|
+
// Whether a decoration around `node`, innermost first, says yes. Only in
|
|
23
|
+
// `node`'s own editor: one nested in a decoration isn't its.
|
|
24
|
+
const someAround = (node, says) => {
|
|
25
|
+
for (let cur = node; cur && !cur.wgTile?.isDoc; cur = cur.parentNode) {
|
|
26
|
+
const handlers = cur instanceof Element ? roots.get(cur) : undefined;
|
|
27
|
+
if (handlers && says(handlers))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
};
|
|
32
|
+
// Whether a decoration around the event's target stops it (useStopEvent).
|
|
33
|
+
export const stopsEvent = (event) => someAround(event.target instanceof Node ? event.target : null, ({ stopEvent }) => [...stopEvent].some((stop) => stop(event)));
|
|
34
|
+
// Whether a decoration around the record's target claims it (useIgnoreMutation).
|
|
35
|
+
export const ignoresMutation = (record) => someAround(record.target, ({ ignoreMutation }) => [...ignoreMutation].some((ignore) => ignore(record)));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Wordgard } from "wordgard/editor";
|
|
2
|
+
import type { GardState } from "wordgard/state";
|
|
3
|
+
export type EditorStore = {
|
|
4
|
+
readonly getState: () => GardState;
|
|
5
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
6
|
+
};
|
|
7
|
+
export type Measure = () => (() => void) | null;
|
|
8
|
+
export type EditorContextValue = {
|
|
9
|
+
readonly wg: Wordgard;
|
|
10
|
+
readonly store: EditorStore;
|
|
11
|
+
readonly afterFlush: (effect: () => void) => void;
|
|
12
|
+
readonly cancel: (effect: () => void) => void;
|
|
13
|
+
readonly measures: Set<Measure>;
|
|
14
|
+
readonly requestMeasure: () => void;
|
|
15
|
+
};
|
|
16
|
+
type Editor = EditorContextValue & {
|
|
17
|
+
readonly publish: (state: GardState) => void;
|
|
18
|
+
readonly readMeasures: (wg: Wordgard) => void;
|
|
19
|
+
readonly runEffects: () => void;
|
|
20
|
+
readonly requestEffects: () => void;
|
|
21
|
+
flushing: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare const editorOf: (wg: Wordgard) => Editor;
|
|
24
|
+
export declare const findEditor: (wg: Wordgard) => EditorContextValue | undefined;
|
|
25
|
+
export declare const EditorContext: import("react").Context<EditorContextValue | null>;
|
|
26
|
+
export declare const useEditorContext: () => EditorContextValue;
|
|
27
|
+
export {};
|