@llui/lexical 0.1.0 → 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.
@@ -15,7 +15,7 @@
15
15
  "nodes",
16
16
  "onError",
17
17
  "plugins",
18
- "readOnly",
18
+ "readonly",
19
19
  "theme",
20
20
  "value"
21
21
  ],
package/dist/foreign.d.ts CHANGED
@@ -27,7 +27,7 @@ export interface LexicalForeignOptions<Emit = unknown> {
27
27
  /** Controlled document signal; the editor follows it (echo-guarded). */
28
28
  value?: Signal<string>;
29
29
  /** Reactive read-only flag (always supplied by the host's state). */
30
- readOnly: Signal<boolean>;
30
+ readonly: Signal<boolean>;
31
31
  /** Debounce window (ms) for outbound serialization. Default 300. */
32
32
  changeDebounceMs?: number;
33
33
  /** Outbound: serialized document changed (debounced, real edits only). */
package/dist/foreign.js CHANGED
@@ -32,7 +32,7 @@ export function lexicalForeign(opts) {
32
32
  namespace: opts.namespace,
33
33
  nodes,
34
34
  theme: opts.theme,
35
- editable: !opts.readOnly.peek(),
35
+ editable: !opts.readonly.peek(),
36
36
  onError: (error) => {
37
37
  if (opts.onError)
38
38
  opts.onError(error);
@@ -43,7 +43,7 @@ export function lexicalForeign(opts) {
43
43
  // Vanilla Lexical does NOT make the root editable — the caller must set
44
44
  // `contenteditable` (the React `<ContentEditable>` does this). Without it the
45
45
  // browser shows no caret and ignores typing.
46
- el.setAttribute('contenteditable', opts.readOnly.peek() ? 'false' : 'true');
46
+ el.setAttribute('contenteditable', opts.readonly.peek() ? 'false' : 'true');
47
47
  editor.setRootElement(el);
48
48
  opts.onReady?.(editor);
49
49
  let lastEmitted = opts.value ? opts.value.peek() : (opts.defaultValue ?? '');
@@ -105,12 +105,12 @@ export function lexicalForeign(opts) {
105
105
  },
106
106
  };
107
107
  };
108
- const readOnly = opts.readOnly;
108
+ const readonly = opts.readonly;
109
109
  const controlled = opts.value;
110
110
  if (controlled) {
111
111
  return foreign({
112
112
  tag: 'div',
113
- state: { value: controlled, readOnly },
113
+ state: { value: controlled, readonly },
114
114
  mount: ({ el, state }) => {
115
115
  const b = boot(el);
116
116
  const unbindValue = state.value.bind((incoming) => {
@@ -119,7 +119,7 @@ export function lexicalForeign(opts) {
119
119
  b.editor.update(() => opts.deserialize(b.editor, incoming), { tag: PROGRAMMATIC_TAG });
120
120
  b.setLastEmitted(incoming);
121
121
  });
122
- const unbindReadOnly = state.readOnly.bind((ro) => {
122
+ const unbindReadOnly = state.readonly.bind((ro) => {
123
123
  b.editor.setEditable(!ro);
124
124
  el.setAttribute('contenteditable', ro ? 'false' : 'true');
125
125
  });
@@ -136,10 +136,10 @@ export function lexicalForeign(opts) {
136
136
  }
137
137
  return foreign({
138
138
  tag: 'div',
139
- state: { readOnly },
139
+ state: { readonly },
140
140
  mount: ({ el, state }) => {
141
141
  const b = boot(el);
142
- const unbindReadOnly = state.readOnly.bind((ro) => {
142
+ const unbindReadOnly = state.readonly.bind((ro) => {
143
143
  b.editor.setEditable(!ro);
144
144
  el.contentEditable = ro ? 'false' : 'true';
145
145
  });
@@ -1 +1 @@
1
- {"version":3,"file":"foreign.js","sourceRoot":"","sources":["../src/foreign.ts"],"names":[],"mappings":"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","sourcesContent":["// The load-bearing seam: mount a Lexical editor inside an LLui view via\n// `foreign()`. Lexical owns the contentEditable subtree; LLui owns the chrome.\n//\n// Inbound (controlled): a `value` signal drives the document, echo-suppressed so\n// the editor never fights its own emissions. Outbound: a debounced\n// update-listener serializes the document and a synchronous one surfaces the\n// selection/format. Serialize/deserialize are injected so this stays\n// markdown-agnostic — the markdown layer supplies the transformer converters.\n\nimport {\n CAN_REDO_COMMAND,\n CAN_UNDO_COMMAND,\n COMMAND_PRIORITY_LOW,\n createEditor,\n type EditorThemeClasses,\n type Klass,\n type LexicalEditor,\n type LexicalNode,\n} from 'lexical'\nimport { registerRichText } from '@lexical/rich-text'\nimport { registerHistory, createEmptyHistoryState } from '@lexical/history'\nimport { mergeRegister } from '@lexical/utils'\nimport { foreign, type Mountable, type Signal } from '@llui/dom'\nimport type { LexicalPlugin, PluginContext } from './plugin.js'\nimport { registerShortcuts } from './register.js'\n\n/** Lexical update tag marking a programmatic write (seed / controlled setValue),\n * so the outbound change listener doesn't echo it back to the host. */\nexport const PROGRAMMATIC_TAG = '@llui/lexical:programmatic'\n\n/** Context handed to the selection callback on every commit. */\nexport interface SelectionContext {\n editor: LexicalEditor\n canUndo: boolean\n canRedo: boolean\n}\n\nexport interface LexicalForeignOptions<Emit = unknown> {\n /** Editor namespace (instance isolation; required for distinct editors). */\n namespace: string\n theme?: EditorThemeClasses\n /** Node classes registered in addition to the plugins' own nodes. */\n nodes?: ReadonlyArray<Klass<LexicalNode>>\n /** Plugins: their `nodes` are merged, `register`/`shortcuts` wired at mount. */\n plugins?: ReadonlyArray<LexicalPlugin<Emit>>\n /** Serialize the live document → string (runs in a read context). */\n serialize: (editor: LexicalEditor) => string\n /** Deserialize a string into the document (runs in an update context). */\n deserialize: (editor: LexicalEditor, value: string) => void\n /** Initial document (uncontrolled) — ignored when `value` is provided. */\n defaultValue?: string\n /** Controlled document signal; the editor follows it (echo-guarded). */\n value?: Signal<string>\n /** Reactive read-only flag (always supplied by the host's state). */\n readOnly: Signal<boolean>\n /** Debounce window (ms) for outbound serialization. Default 300. */\n changeDebounceMs?: number\n /** Outbound: serialized document changed (debounced, real edits only). */\n onChange?: (value: string) => void\n /** Outbound: selection / format / structure changed (every commit). */\n onSelectionChange?: (ctx: SelectionContext) => void\n /** Host emit, handed to each plugin's `register` context. */\n emit?: (msg: Emit) => void\n /** Receives the live editor at mount (host dispatches commands through it). */\n onReady?: (editor: LexicalEditor) => void\n /** Extra registration after rich-text (e.g. markdown shortcuts). Disposer. */\n register?: (editor: LexicalEditor) => () => void\n onError?: (error: Error) => void\n}\n\n/** The booted editor + echo-guard accessors, shared by both control modes. */\ninterface BootResult {\n editor: LexicalEditor\n getLastEmitted: () => string\n setLastEmitted: (value: string) => void\n /** Tear down listeners, history, plugins, and the pending debounce timer. */\n dispose: () => void\n}\n\n/** The `foreign` instance — only a disposer is needed at unmount. */\ninterface ForeignInst {\n dispose: () => void\n}\n\n/** Mount Lexical into an LLui view. Returns a `Mountable` placed in the view\n * array; Lexical is created on mount and destroyed on the component's dispose. */\nexport function lexicalForeign<Emit = unknown>(opts: LexicalForeignOptions<Emit>): Mountable {\n const debounceMs = opts.changeDebounceMs ?? 300\n\n const boot = (el: Element): BootResult => {\n // De-duplicate node classes by reference: registering the same Klass twice\n // (e.g. two decorator plugins sharing LLuiDecoratorNode) throws in Lexical.\n const nodeSet = new Set<Klass<LexicalNode>>(opts.nodes ?? [])\n for (const plugin of opts.plugins ?? []) {\n for (const node of plugin.nodes ?? []) nodeSet.add(node)\n }\n const nodes = [...nodeSet]\n\n const editor = createEditor({\n namespace: opts.namespace,\n nodes,\n theme: opts.theme,\n editable: !opts.readOnly.peek(),\n onError: (error: Error) => {\n if (opts.onError) opts.onError(error)\n else throw error\n },\n })\n // Vanilla Lexical does NOT make the root editable — the caller must set\n // `contenteditable` (the React `<ContentEditable>` does this). Without it the\n // browser shows no caret and ignores typing.\n el.setAttribute('contenteditable', opts.readOnly.peek() ? 'false' : 'true')\n editor.setRootElement(el as HTMLElement)\n opts.onReady?.(editor)\n\n let lastEmitted = opts.value ? opts.value.peek() : (opts.defaultValue ?? '')\n let canUndo = false\n let canRedo = false\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n // Seed the initial document (programmatic — not echoed outbound). Discrete so\n // the host is populated synchronously at mount (before the first paint/read).\n // NB: seeding happens AFTER registration below, so plugins/decorator bridges\n // are live when the seed document is built (e.g. a callout in the seed needs\n // its bridge registered to decorate).\n\n const ctx: PluginContext<Emit> = { emit: (msg) => opts.emit?.(msg) }\n const pluginDisposers = (opts.plugins ?? []).map((plugin) => {\n const reg = plugin.register?.(editor, ctx) ?? (() => {})\n const shortcuts = plugin.shortcuts ? registerShortcuts(editor, plugin.shortcuts) : () => {}\n return () => {\n reg()\n shortcuts()\n }\n })\n\n const emitSelection = (): void => opts.onSelectionChange?.({ editor, canUndo, canRedo })\n\n const baseDispose = mergeRegister(\n registerRichText(editor),\n registerHistory(editor, createEmptyHistoryState(), 1000),\n opts.register?.(editor) ?? (() => {}),\n editor.registerCommand(\n CAN_UNDO_COMMAND,\n (payload: boolean) => {\n canUndo = payload\n emitSelection()\n return false\n },\n COMMAND_PRIORITY_LOW,\n ),\n editor.registerCommand(\n CAN_REDO_COMMAND,\n (payload: boolean) => {\n canRedo = payload\n emitSelection()\n return false\n },\n COMMAND_PRIORITY_LOW,\n ),\n editor.registerUpdateListener(({ editorState, tags }) => {\n emitSelection()\n if (tags.has(PROGRAMMATIC_TAG)) return\n if (debounceTimer !== undefined) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n editorState.read(() => {\n const next = opts.serialize(editor)\n lastEmitted = next\n opts.onChange?.(next)\n })\n }, debounceMs)\n }),\n ...pluginDisposers,\n )\n\n // Seed now that rich-text, history, plugins, and decorator bridges are live.\n editor.update(() => opts.deserialize(editor, lastEmitted), {\n tag: PROGRAMMATIC_TAG,\n discrete: true,\n })\n\n return {\n editor,\n getLastEmitted: () => lastEmitted,\n setLastEmitted: (value) => {\n lastEmitted = value\n },\n dispose: () => {\n if (debounceTimer !== undefined) clearTimeout(debounceTimer)\n baseDispose()\n },\n }\n }\n\n const readOnly = opts.readOnly\n const controlled = opts.value\n\n if (controlled) {\n return foreign<ForeignInst, { value: Signal<string>; readOnly: Signal<boolean> }>({\n tag: 'div',\n state: { value: controlled, readOnly },\n mount: ({ el, state }) => {\n const b = boot(el)\n const unbindValue = state.value.bind((incoming) => {\n if (incoming === b.getLastEmitted()) return\n b.editor.update(() => opts.deserialize(b.editor, incoming), { tag: PROGRAMMATIC_TAG })\n b.setLastEmitted(incoming)\n })\n const unbindReadOnly = state.readOnly.bind((ro) => {\n b.editor.setEditable(!ro)\n el.setAttribute('contenteditable', ro ? 'false' : 'true')\n })\n return {\n dispose: () => {\n unbindValue()\n unbindReadOnly()\n b.dispose()\n },\n }\n },\n unmount: (inst) => inst.dispose(),\n })\n }\n\n return foreign<ForeignInst, { readOnly: Signal<boolean> }>({\n tag: 'div',\n state: { readOnly },\n mount: ({ el, state }) => {\n const b = boot(el)\n const unbindReadOnly = state.readOnly.bind((ro) => {\n b.editor.setEditable(!ro)\n ;(el as HTMLElement).contentEditable = ro ? 'false' : 'true'\n })\n return {\n dispose: () => {\n unbindReadOnly()\n b.dispose()\n },\n }\n },\n unmount: (inst) => inst.dispose(),\n })\n}\n"]}
1
+ {"version":3,"file":"foreign.js","sourceRoot":"","sources":["../src/foreign.ts"],"names":[],"mappings":"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","sourcesContent":["// The load-bearing seam: mount a Lexical editor inside an LLui view via\n// `foreign()`. Lexical owns the contentEditable subtree; LLui owns the chrome.\n//\n// Inbound (controlled): a `value` signal drives the document, echo-suppressed so\n// the editor never fights its own emissions. Outbound: a debounced\n// update-listener serializes the document and a synchronous one surfaces the\n// selection/format. Serialize/deserialize are injected so this stays\n// markdown-agnostic — the markdown layer supplies the transformer converters.\n\nimport {\n CAN_REDO_COMMAND,\n CAN_UNDO_COMMAND,\n COMMAND_PRIORITY_LOW,\n createEditor,\n type EditorThemeClasses,\n type Klass,\n type LexicalEditor,\n type LexicalNode,\n} from 'lexical'\nimport { registerRichText } from '@lexical/rich-text'\nimport { registerHistory, createEmptyHistoryState } from '@lexical/history'\nimport { mergeRegister } from '@lexical/utils'\nimport { foreign, type Mountable, type Signal } from '@llui/dom'\nimport type { LexicalPlugin, PluginContext } from './plugin.js'\nimport { registerShortcuts } from './register.js'\n\n/** Lexical update tag marking a programmatic write (seed / controlled setValue),\n * so the outbound change listener doesn't echo it back to the host. */\nexport const PROGRAMMATIC_TAG = '@llui/lexical:programmatic'\n\n/** Context handed to the selection callback on every commit. */\nexport interface SelectionContext {\n editor: LexicalEditor\n canUndo: boolean\n canRedo: boolean\n}\n\nexport interface LexicalForeignOptions<Emit = unknown> {\n /** Editor namespace (instance isolation; required for distinct editors). */\n namespace: string\n theme?: EditorThemeClasses\n /** Node classes registered in addition to the plugins' own nodes. */\n nodes?: ReadonlyArray<Klass<LexicalNode>>\n /** Plugins: their `nodes` are merged, `register`/`shortcuts` wired at mount. */\n plugins?: ReadonlyArray<LexicalPlugin<Emit>>\n /** Serialize the live document → string (runs in a read context). */\n serialize: (editor: LexicalEditor) => string\n /** Deserialize a string into the document (runs in an update context). */\n deserialize: (editor: LexicalEditor, value: string) => void\n /** Initial document (uncontrolled) — ignored when `value` is provided. */\n defaultValue?: string\n /** Controlled document signal; the editor follows it (echo-guarded). */\n value?: Signal<string>\n /** Reactive read-only flag (always supplied by the host's state). */\n readonly: Signal<boolean>\n /** Debounce window (ms) for outbound serialization. Default 300. */\n changeDebounceMs?: number\n /** Outbound: serialized document changed (debounced, real edits only). */\n onChange?: (value: string) => void\n /** Outbound: selection / format / structure changed (every commit). */\n onSelectionChange?: (ctx: SelectionContext) => void\n /** Host emit, handed to each plugin's `register` context. */\n emit?: (msg: Emit) => void\n /** Receives the live editor at mount (host dispatches commands through it). */\n onReady?: (editor: LexicalEditor) => void\n /** Extra registration after rich-text (e.g. markdown shortcuts). Disposer. */\n register?: (editor: LexicalEditor) => () => void\n onError?: (error: Error) => void\n}\n\n/** The booted editor + echo-guard accessors, shared by both control modes. */\ninterface BootResult {\n editor: LexicalEditor\n getLastEmitted: () => string\n setLastEmitted: (value: string) => void\n /** Tear down listeners, history, plugins, and the pending debounce timer. */\n dispose: () => void\n}\n\n/** The `foreign` instance — only a disposer is needed at unmount. */\ninterface ForeignInst {\n dispose: () => void\n}\n\n/** Mount Lexical into an LLui view. Returns a `Mountable` placed in the view\n * array; Lexical is created on mount and destroyed on the component's dispose. */\nexport function lexicalForeign<Emit = unknown>(opts: LexicalForeignOptions<Emit>): Mountable {\n const debounceMs = opts.changeDebounceMs ?? 300\n\n const boot = (el: Element): BootResult => {\n // De-duplicate node classes by reference: registering the same Klass twice\n // (e.g. two decorator plugins sharing LLuiDecoratorNode) throws in Lexical.\n const nodeSet = new Set<Klass<LexicalNode>>(opts.nodes ?? [])\n for (const plugin of opts.plugins ?? []) {\n for (const node of plugin.nodes ?? []) nodeSet.add(node)\n }\n const nodes = [...nodeSet]\n\n const editor = createEditor({\n namespace: opts.namespace,\n nodes,\n theme: opts.theme,\n editable: !opts.readonly.peek(),\n onError: (error: Error) => {\n if (opts.onError) opts.onError(error)\n else throw error\n },\n })\n // Vanilla Lexical does NOT make the root editable — the caller must set\n // `contenteditable` (the React `<ContentEditable>` does this). Without it the\n // browser shows no caret and ignores typing.\n el.setAttribute('contenteditable', opts.readonly.peek() ? 'false' : 'true')\n editor.setRootElement(el as HTMLElement)\n opts.onReady?.(editor)\n\n let lastEmitted = opts.value ? opts.value.peek() : (opts.defaultValue ?? '')\n let canUndo = false\n let canRedo = false\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n // Seed the initial document (programmatic — not echoed outbound). Discrete so\n // the host is populated synchronously at mount (before the first paint/read).\n // NB: seeding happens AFTER registration below, so plugins/decorator bridges\n // are live when the seed document is built (e.g. a callout in the seed needs\n // its bridge registered to decorate).\n\n const ctx: PluginContext<Emit> = { emit: (msg) => opts.emit?.(msg) }\n const pluginDisposers = (opts.plugins ?? []).map((plugin) => {\n const reg = plugin.register?.(editor, ctx) ?? (() => {})\n const shortcuts = plugin.shortcuts ? registerShortcuts(editor, plugin.shortcuts) : () => {}\n return () => {\n reg()\n shortcuts()\n }\n })\n\n const emitSelection = (): void => opts.onSelectionChange?.({ editor, canUndo, canRedo })\n\n const baseDispose = mergeRegister(\n registerRichText(editor),\n registerHistory(editor, createEmptyHistoryState(), 1000),\n opts.register?.(editor) ?? (() => {}),\n editor.registerCommand(\n CAN_UNDO_COMMAND,\n (payload: boolean) => {\n canUndo = payload\n emitSelection()\n return false\n },\n COMMAND_PRIORITY_LOW,\n ),\n editor.registerCommand(\n CAN_REDO_COMMAND,\n (payload: boolean) => {\n canRedo = payload\n emitSelection()\n return false\n },\n COMMAND_PRIORITY_LOW,\n ),\n editor.registerUpdateListener(({ editorState, tags }) => {\n emitSelection()\n if (tags.has(PROGRAMMATIC_TAG)) return\n if (debounceTimer !== undefined) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n editorState.read(() => {\n const next = opts.serialize(editor)\n lastEmitted = next\n opts.onChange?.(next)\n })\n }, debounceMs)\n }),\n ...pluginDisposers,\n )\n\n // Seed now that rich-text, history, plugins, and decorator bridges are live.\n editor.update(() => opts.deserialize(editor, lastEmitted), {\n tag: PROGRAMMATIC_TAG,\n discrete: true,\n })\n\n return {\n editor,\n getLastEmitted: () => lastEmitted,\n setLastEmitted: (value) => {\n lastEmitted = value\n },\n dispose: () => {\n if (debounceTimer !== undefined) clearTimeout(debounceTimer)\n baseDispose()\n },\n }\n }\n\n const readonly = opts.readonly\n const controlled = opts.value\n\n if (controlled) {\n return foreign<ForeignInst, { value: Signal<string>; readonly: Signal<boolean> }>({\n tag: 'div',\n state: { value: controlled, readonly },\n mount: ({ el, state }) => {\n const b = boot(el)\n const unbindValue = state.value.bind((incoming) => {\n if (incoming === b.getLastEmitted()) return\n b.editor.update(() => opts.deserialize(b.editor, incoming), { tag: PROGRAMMATIC_TAG })\n b.setLastEmitted(incoming)\n })\n const unbindReadOnly = state.readonly.bind((ro) => {\n b.editor.setEditable(!ro)\n el.setAttribute('contenteditable', ro ? 'false' : 'true')\n })\n return {\n dispose: () => {\n unbindValue()\n unbindReadOnly()\n b.dispose()\n },\n }\n },\n unmount: (inst) => inst.dispose(),\n })\n }\n\n return foreign<ForeignInst, { readonly: Signal<boolean> }>({\n tag: 'div',\n state: { readonly },\n mount: ({ el, state }) => {\n const b = boot(el)\n const unbindReadOnly = state.readonly.bind((ro) => {\n b.editor.setEditable(!ro)\n ;(el as HTMLElement).contentEditable = ro ? 'false' : 'true'\n })\n return {\n dispose: () => {\n unbindReadOnly()\n b.dispose()\n },\n }\n },\n unmount: (inst) => inst.dispose(),\n })\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/lexical",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  }
12
12
  },
13
13
  "peerDependencies": {
14
- "@llui/dom": "^0.9.0",
14
+ "@llui/dom": "^0.10.0",
15
15
  "lexical": "^0.45.0",
16
16
  "@lexical/history": "^0.45.0",
17
17
  "@lexical/rich-text": "^0.45.0",
@@ -27,7 +27,7 @@
27
27
  "@lexical/utils": "^0.45.0",
28
28
  "typescript": "^6.0.0",
29
29
  "vitest": "^4.1.2",
30
- "@llui/dom": "0.9.0"
30
+ "@llui/dom": "0.10.0"
31
31
  },
32
32
  "sideEffects": false,
33
33
  "description": "Low-level binding between Lexical and the LLui signal runtime — mount Lexical via foreign(), plugin contract, and the DecoratorNode ↔ LLui sub-view bridge",