@llui/markdown-editor 0.1.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/LICENSE +21 -0
- package/dist/__llui_deps.json +252 -0
- package/dist/editor.d.ts +42 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +157 -0
- package/dist/editor.js.map +1 -0
- package/dist/effects.d.ts +17 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +33 -0
- package/dist/effects.js.map +1 -0
- package/dist/format.d.ts +6 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +51 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/callout.d.ts +15 -0
- package/dist/plugins/callout.d.ts.map +1 -0
- package/dist/plugins/callout.js +151 -0
- package/dist/plugins/callout.js.map +1 -0
- package/dist/plugins/context-menu.d.ts +3 -0
- package/dist/plugins/context-menu.d.ts.map +1 -0
- package/dist/plugins/context-menu.js +93 -0
- package/dist/plugins/context-menu.js.map +1 -0
- package/dist/plugins/core.d.ts +7 -0
- package/dist/plugins/core.d.ts.map +1 -0
- package/dist/plugins/core.js +189 -0
- package/dist/plugins/core.js.map +1 -0
- package/dist/plugins/emoji.d.ts +9 -0
- package/dist/plugins/emoji.d.ts.map +1 -0
- package/dist/plugins/emoji.js +50 -0
- package/dist/plugins/emoji.js.map +1 -0
- package/dist/plugins/floating-toolbar.d.ts +3 -0
- package/dist/plugins/floating-toolbar.d.ts.map +1 -0
- package/dist/plugins/floating-toolbar.js +137 -0
- package/dist/plugins/floating-toolbar.js.map +1 -0
- package/dist/plugins/hr.d.ts +5 -0
- package/dist/plugins/hr.d.ts.map +1 -0
- package/dist/plugins/hr.js +46 -0
- package/dist/plugins/hr.js.map +1 -0
- package/dist/plugins/image.d.ts +8 -0
- package/dist/plugins/image.d.ts.map +1 -0
- package/dist/plugins/image.js +173 -0
- package/dist/plugins/image.js.map +1 -0
- package/dist/plugins/link.d.ts +7 -0
- package/dist/plugins/link.d.ts.map +1 -0
- package/dist/plugins/link.js +100 -0
- package/dist/plugins/link.js.map +1 -0
- package/dist/plugins/math.d.ts +8 -0
- package/dist/plugins/math.d.ts.map +1 -0
- package/dist/plugins/math.js +81 -0
- package/dist/plugins/math.js.map +1 -0
- package/dist/plugins/mention.d.ts +11 -0
- package/dist/plugins/mention.d.ts.map +1 -0
- package/dist/plugins/mention.js +163 -0
- package/dist/plugins/mention.js.map +1 -0
- package/dist/plugins/mermaid.d.ts +8 -0
- package/dist/plugins/mermaid.d.ts.map +1 -0
- package/dist/plugins/mermaid.js +92 -0
- package/dist/plugins/mermaid.js.map +1 -0
- package/dist/plugins/overlay.d.ts +46 -0
- package/dist/plugins/overlay.d.ts.map +1 -0
- package/dist/plugins/overlay.js +83 -0
- package/dist/plugins/overlay.js.map +1 -0
- package/dist/plugins/slash.d.ts +3 -0
- package/dist/plugins/slash.d.ts.map +1 -0
- package/dist/plugins/slash.js +167 -0
- package/dist/plugins/slash.js.map +1 -0
- package/dist/plugins/table.d.ts +3 -0
- package/dist/plugins/table.d.ts.map +1 -0
- package/dist/plugins/table.js +227 -0
- package/dist/plugins/table.js.map +1 -0
- package/dist/plugins/types.d.ts +44 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +4 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/plugins/ui.d.ts +44 -0
- package/dist/plugins/ui.d.ts.map +1 -0
- package/dist/plugins/ui.js +34 -0
- package/dist/plugins/ui.js.map +1 -0
- package/dist/state.d.ts +105 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +100 -0
- package/dist/state.js.map +1 -0
- package/dist/styles/editor.css +517 -0
- package/dist/surfaces/link-dialog.d.ts +19 -0
- package/dist/surfaces/link-dialog.d.ts.map +1 -0
- package/dist/surfaces/link-dialog.js +45 -0
- package/dist/surfaces/link-dialog.js.map +1 -0
- package/dist/surfaces/toolbar.d.ts +48 -0
- package/dist/surfaces/toolbar.d.ts.map +1 -0
- package/dist/surfaces/toolbar.js +134 -0
- package/dist/surfaces/toolbar.js.map +1 -0
- package/dist/transformers/gfm.d.ts +7 -0
- package/dist/transformers/gfm.d.ts.map +1 -0
- package/dist/transformers/gfm.js +41 -0
- package/dist/transformers/gfm.js.map +1 -0
- package/dist/transformers/registry.d.ts +9 -0
- package/dist/transformers/registry.d.ts.map +1 -0
- package/dist/transformers/registry.js +43 -0
- package/dist/transformers/registry.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.js","sourceRoot":"","sources":["../../src/plugins/link.ts"],"names":[],"mappings":"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","sourcesContent":["// The link plugin — a stateful, UI-bearing feature built entirely as a plugin\n// (no core editor changes). It owns its dialog state, its view (the modal), and\n// its effects (save/restore selection + toggle the link). This is the proof that\n// the plugin-UI extension makes such features pluggable rather than built-in.\n\nimport {\n $getSelection,\n $isRangeSelection,\n $setSelection,\n type BaseSelection,\n type LexicalEditor,\n} from 'lexical'\nimport { $findMatchingParent } from '@lexical/utils'\nimport { $isLinkNode, $toggleLink } from '@lexical/link'\nimport type { DialogMsg, DialogState } from '@llui/components/dialog'\nimport { linkDialog } from '../surfaces/link-dialog.js'\nimport { definePluginUI } from './ui.js'\nimport type { CommandItem, MarkdownPlugin } from './types.js'\n\nconst PLUGIN = 'link'\n\ninterface LinkState {\n dialog: DialogState\n url: string\n}\n\ntype LinkMsg =\n | { type: 'open' }\n | { type: 'show'; url: string }\n | { type: 'setUrl'; url: string }\n | { type: 'submit' }\n | { type: 'dialog'; msg: DialogMsg }\n\ntype LinkEffect = { type: 'begin' } | { type: 'commit'; url: string }\n\n/** Read the URL of the link wrapping the current selection (empty if none). */\nfunction readLinkUrl(editor: LexicalEditor): string {\n return editor.getEditorState().read(() => {\n const selection = $getSelection()\n if (!$isRangeSelection(selection)) return ''\n const link = $findMatchingParent(selection.anchor.getNode(), (node) => $isLinkNode(node))\n return $isLinkNode(link) ? link.getURL() : ''\n })\n}\n\nfunction dialogOpen(msg: DialogMsg, current: boolean): boolean {\n switch (msg.type) {\n case 'open':\n return true\n case 'close':\n return false\n case 'toggle':\n return !current\n case 'setOpen':\n return msg.open\n }\n}\n\nexport interface LinkPluginOptions {\n /** Default URL pre-filled when there's no existing link (default ''). */\n defaultUrl?: string\n}\n\nexport function linkPlugin(opts: LinkPluginOptions = {}): MarkdownPlugin {\n // Selection saved when the dialog opens (the modal steals focus/selection),\n // restored on commit. Encapsulated in the plugin instance — not the core editor.\n let savedSelection: BaseSelection | null = null\n\n const item: CommandItem = {\n id: 'link',\n label: 'Link',\n icon: 'link',\n group: 'inline',\n keywords: ['url', 'href'],\n run: (_editor, ctx) => ctx.send({ type: 'plugin', name: PLUGIN, msg: { type: 'open' } }),\n isActive: (f) => f.link,\n surfaces: ['toolbar', 'floating', 'context'],\n }\n\n return {\n name: PLUGIN,\n items: [item],\n ui: definePluginUI<LinkState, LinkMsg, LinkEffect>({\n init: () => ({ dialog: { open: false }, url: opts.defaultUrl ?? '' }),\n update: (state, msg) => {\n switch (msg.type) {\n case 'open':\n return [state, [{ type: 'begin' }]]\n case 'show':\n return { dialog: { open: true }, url: msg.url }\n case 'setUrl':\n return { ...state, url: msg.url }\n case 'submit':\n return [{ ...state, dialog: { open: false } }, [{ type: 'commit', url: state.url }]]\n case 'dialog': {\n const open = dialogOpen(msg.msg, state.dialog.open)\n return open === state.dialog.open ? state : { ...state, dialog: { open } }\n }\n }\n },\n view: ({ state, send }) => [\n linkDialog({\n dialog: state.at('dialog'),\n url: state.at('url'),\n onInput: (url) => send({ type: 'setUrl', url }),\n onSubmit: () => send({ type: 'submit' }),\n onDialog: (msg) => send({ type: 'dialog', msg }),\n }),\n ],\n onEffect: (effect, ctx) => {\n const editor = ctx.editor()\n if (!editor) return\n if (effect.type === 'begin') {\n savedSelection = editor.getEditorState().read(() => {\n const selection = $getSelection()\n return selection ? selection.clone() : null\n })\n ctx.send({ type: 'show', url: readLinkUrl(editor) })\n return\n }\n const url = effect.url.trim()\n editor.update(() => {\n if (savedSelection) $setSelection(savedSelection.clone())\n $toggleLink(url === '' ? null : url)\n })\n savedSelection = null\n },\n }),\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MarkdownPlugin } from './types.js';
|
|
2
|
+
export interface MathPluginOptions {
|
|
3
|
+
/** Typeset TeX to an HTML string (e.g. via KaTeX). When omitted, the raw TeX is
|
|
4
|
+
* shown in a styled box. */
|
|
5
|
+
render?: (tex: string) => string;
|
|
6
|
+
}
|
|
7
|
+
export declare function mathPlugin(opts?: MathPluginOptions): MarkdownPlugin;
|
|
8
|
+
//# sourceMappingURL=math.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"math.d.ts","sourceRoot":"","sources":["../../src/plugins/math.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAgBhD,MAAM,WAAW,iBAAiB;IAChC;gCAC4B;IAC5B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;CACjC;AAED,wBAAgB,UAAU,CAAC,IAAI,GAAE,iBAAsB,GAAG,cAAc,CA8EvE"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Math plugin — a block math node ($$…$$) rendered via the decorator bridge. The
|
|
2
|
+
// TeX source is editable inline (persists on blur); an optional `render` callback
|
|
3
|
+
// (e.g. KaTeX) turns it into a typeset preview. Round-trips to `$$tex$$`.
|
|
4
|
+
import {} from 'lexical';
|
|
5
|
+
import { $insertNodeToNearestRoot } from '@lexical/utils';
|
|
6
|
+
import { $createLLuiDecoratorNode, $isLLuiDecoratorNode, LLuiDecoratorNode, decoratorBridge, } from '@llui/lexical';
|
|
7
|
+
import { component, div, span, text, unsafeHtml } from '@llui/dom';
|
|
8
|
+
const BRIDGE_TYPE = 'math';
|
|
9
|
+
function isMathData(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && typeof value.tex === 'string';
|
|
11
|
+
}
|
|
12
|
+
const stop = (e) => e.stopPropagation();
|
|
13
|
+
export function mathPlugin(opts = {}) {
|
|
14
|
+
const bridge = decoratorBridge(BRIDGE_TYPE, (data, api) => component({
|
|
15
|
+
name: 'Math',
|
|
16
|
+
init: () => ({ tex: data.tex }),
|
|
17
|
+
update: (state, msg) => {
|
|
18
|
+
if (msg.type === 'commit') {
|
|
19
|
+
if (msg.tex === state.tex)
|
|
20
|
+
return state;
|
|
21
|
+
api.update({ tex: msg.tex });
|
|
22
|
+
return { tex: msg.tex };
|
|
23
|
+
}
|
|
24
|
+
return state;
|
|
25
|
+
},
|
|
26
|
+
view: ({ state, send }) => {
|
|
27
|
+
const children = [
|
|
28
|
+
span({
|
|
29
|
+
'data-part': 'source',
|
|
30
|
+
contenteditable: 'true',
|
|
31
|
+
role: 'textbox',
|
|
32
|
+
'aria-label': 'TeX source',
|
|
33
|
+
onKeyDown: stop,
|
|
34
|
+
onBeforeInput: stop,
|
|
35
|
+
onPaste: stop,
|
|
36
|
+
onBlur: (e) => send({ type: 'commit', tex: e.target.textContent ?? '' }),
|
|
37
|
+
}, [text(state.at('tex'))]),
|
|
38
|
+
];
|
|
39
|
+
if (opts.render) {
|
|
40
|
+
children.push(span({ 'data-part': 'preview', contenteditable: 'false' }, [
|
|
41
|
+
unsafeHtml(state.at('tex').map((tex) => opts.render(tex))),
|
|
42
|
+
]));
|
|
43
|
+
}
|
|
44
|
+
return [
|
|
45
|
+
div({ 'data-scope': 'md-math', 'data-part': 'root', contenteditable: 'false' }, children),
|
|
46
|
+
];
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
const transformer = {
|
|
50
|
+
dependencies: [LLuiDecoratorNode],
|
|
51
|
+
export: (node) => {
|
|
52
|
+
if (!$isLLuiDecoratorNode(node) || node.getBridgeType() !== BRIDGE_TYPE)
|
|
53
|
+
return null;
|
|
54
|
+
const data = node.getData();
|
|
55
|
+
return isMathData(data) ? `$$${data.tex}$$` : null;
|
|
56
|
+
},
|
|
57
|
+
regExp: /^\$\$(.+)\$\$$/,
|
|
58
|
+
replace: (parentNode, _children, match) => {
|
|
59
|
+
parentNode.replace($createLLuiDecoratorNode(BRIDGE_TYPE, { tex: match[1] ?? '' }));
|
|
60
|
+
},
|
|
61
|
+
type: 'element',
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
name: 'math',
|
|
65
|
+
nodes: [LLuiDecoratorNode],
|
|
66
|
+
decorators: [bridge],
|
|
67
|
+
transformers: [transformer],
|
|
68
|
+
items: [
|
|
69
|
+
{
|
|
70
|
+
id: 'math',
|
|
71
|
+
label: 'Math block',
|
|
72
|
+
icon: 'math',
|
|
73
|
+
group: 'insert',
|
|
74
|
+
keywords: ['latex', 'tex', 'equation', 'formula'],
|
|
75
|
+
run: (editor) => editor.update(() => $insertNodeToNearestRoot($createLLuiDecoratorNode(BRIDGE_TYPE, { tex: 'e = mc^2' }))),
|
|
76
|
+
surfaces: ['slash', 'context'],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=math.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"math.js","sourceRoot":"","sources":["../../src/plugins/math.ts"],"names":[],"mappings":"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","sourcesContent":["// Math plugin — a block math node ($$…$$) rendered via the decorator bridge. The\n// TeX source is editable inline (persists on blur); an optional `render` callback\n// (e.g. KaTeX) turns it into a typeset preview. Round-trips to `$$tex$$`.\n\nimport { type ElementNode, type LexicalNode } from 'lexical'\nimport { $insertNodeToNearestRoot } from '@lexical/utils'\nimport type { ElementTransformer } from '@lexical/markdown'\nimport {\n $createLLuiDecoratorNode,\n $isLLuiDecoratorNode,\n LLuiDecoratorNode,\n decoratorBridge,\n} from '@llui/lexical'\nimport { component, div, span, text, unsafeHtml, type Mountable, type Signal } from '@llui/dom'\nimport type { MarkdownPlugin } from './types.js'\n\nconst BRIDGE_TYPE = 'math'\n\ninterface MathData {\n tex: string\n}\n\nfunction isMathData(value: unknown): value is MathData {\n return typeof value === 'object' && value !== null && typeof (value as MathData).tex === 'string'\n}\n\ntype MathMsg = { type: 'commit'; tex: string }\n\nconst stop = (e: Event): void => e.stopPropagation()\n\nexport interface MathPluginOptions {\n /** Typeset TeX to an HTML string (e.g. via KaTeX). When omitted, the raw TeX is\n * shown in a styled box. */\n render?: (tex: string) => string\n}\n\nexport function mathPlugin(opts: MathPluginOptions = {}): MarkdownPlugin {\n const bridge = decoratorBridge<MathData, MathData, MathMsg, never>(BRIDGE_TYPE, (data, api) =>\n component<MathData, MathMsg, never>({\n name: 'Math',\n init: () => ({ tex: data.tex }),\n update: (state, msg) => {\n if (msg.type === 'commit') {\n if (msg.tex === state.tex) return state\n api.update({ tex: msg.tex })\n return { tex: msg.tex }\n }\n return state\n },\n view: ({ state, send }) => {\n const children: Mountable[] = [\n span(\n {\n 'data-part': 'source',\n contenteditable: 'true',\n role: 'textbox',\n 'aria-label': 'TeX source',\n onKeyDown: stop,\n onBeforeInput: stop,\n onPaste: stop,\n onBlur: (e: FocusEvent) =>\n send({ type: 'commit', tex: (e.target as HTMLElement).textContent ?? '' }),\n },\n [text(state.at('tex') as Signal<string>)],\n ),\n ]\n if (opts.render) {\n children.push(\n span({ 'data-part': 'preview', contenteditable: 'false' }, [\n unsafeHtml(state.at('tex').map((tex) => opts.render!(tex)) as Signal<string>),\n ]),\n )\n }\n return [\n div({ 'data-scope': 'md-math', 'data-part': 'root', contenteditable: 'false' }, children),\n ]\n },\n }),\n )\n\n const transformer: ElementTransformer = {\n dependencies: [LLuiDecoratorNode],\n export: (node: LexicalNode): string | null => {\n if (!$isLLuiDecoratorNode(node) || node.getBridgeType() !== BRIDGE_TYPE) return null\n const data = node.getData()\n return isMathData(data) ? `$$${data.tex}$$` : null\n },\n regExp: /^\\$\\$(.+)\\$\\$$/,\n replace: (parentNode: ElementNode, _children, match): void => {\n parentNode.replace($createLLuiDecoratorNode(BRIDGE_TYPE, { tex: match[1] ?? '' }))\n },\n type: 'element',\n }\n\n return {\n name: 'math',\n nodes: [LLuiDecoratorNode],\n decorators: [bridge],\n transformers: [transformer],\n items: [\n {\n id: 'math',\n label: 'Math block',\n icon: 'math',\n group: 'insert',\n keywords: ['latex', 'tex', 'equation', 'formula'],\n run: (editor) =>\n editor.update(() =>\n $insertNodeToNearestRoot($createLLuiDecoratorNode(BRIDGE_TYPE, { tex: 'e = mc^2' })),\n ),\n surfaces: ['slash', 'context'],\n },\n ],\n }\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MarkdownPlugin } from './types.js';
|
|
2
|
+
export interface Mention {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MentionPluginOptions {
|
|
7
|
+
/** Resolve candidates for a query (default: a small sample list). */
|
|
8
|
+
source?: (query: string) => readonly Mention[];
|
|
9
|
+
}
|
|
10
|
+
export declare function mentionPlugin(opts?: MentionPluginOptions): MarkdownPlugin;
|
|
11
|
+
//# sourceMappingURL=mention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mention.d.ts","sourceRoot":"","sources":["../../src/plugins/mention.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd;AA4DD,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,SAAS,OAAO,EAAE,CAAA;CAC/C;AAED,wBAAgB,aAAa,CAAC,IAAI,GAAE,oBAAyB,GAAG,cAAc,CAsI7E"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Mention plugin — an `@`-triggered typeahead. Same shape as the slash menu, but
|
|
2
|
+
// the candidates come from a configurable `source`, and choosing inserts the
|
|
3
|
+
// mention text (`@label`) rather than running a command. Demonstrates a second
|
|
4
|
+
// typeahead built on the plugin-UI seam with no new machinery.
|
|
5
|
+
import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, } from 'lexical';
|
|
6
|
+
import { mergeRegister } from '@lexical/utils';
|
|
7
|
+
import { div, each, text } from '@llui/dom';
|
|
8
|
+
import { definePluginUI } from './ui.js';
|
|
9
|
+
import { OVERLAY_Z, hideOverlay, overlayRoot } from './overlay.js';
|
|
10
|
+
const TRIGGER = /(?:^|\s)@(\w*)$/;
|
|
11
|
+
const DEFAULT_MENTIONS = [
|
|
12
|
+
{ id: 'franco', label: 'Franco' },
|
|
13
|
+
{ id: 'ada', label: 'Ada' },
|
|
14
|
+
{ id: 'grace', label: 'Grace' },
|
|
15
|
+
{ id: 'linus', label: 'Linus' },
|
|
16
|
+
{ id: 'margaret', label: 'Margaret' },
|
|
17
|
+
];
|
|
18
|
+
function withActive(items, index) {
|
|
19
|
+
return items.map((it, i) => ({ id: it.id, label: it.label, active: i === index }));
|
|
20
|
+
}
|
|
21
|
+
function $readQuery() {
|
|
22
|
+
const selection = $getSelection();
|
|
23
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
24
|
+
return null;
|
|
25
|
+
const node = selection.anchor.getNode();
|
|
26
|
+
if (!$isTextNode(node))
|
|
27
|
+
return null;
|
|
28
|
+
const before = node.getTextContent().slice(0, selection.anchor.offset);
|
|
29
|
+
const match = before.match(TRIGGER);
|
|
30
|
+
return match ? (match[1] ?? '') : null;
|
|
31
|
+
}
|
|
32
|
+
function caretXY() {
|
|
33
|
+
if (typeof window === 'undefined')
|
|
34
|
+
return { x: 0, y: 0 };
|
|
35
|
+
const sel = window.getSelection();
|
|
36
|
+
if (sel && sel.rangeCount > 0) {
|
|
37
|
+
const rect = sel.getRangeAt(0).getBoundingClientRect();
|
|
38
|
+
return { x: rect.left, y: rect.bottom + 4 };
|
|
39
|
+
}
|
|
40
|
+
return { x: 0, y: 0 };
|
|
41
|
+
}
|
|
42
|
+
export function mentionPlugin(opts = {}) {
|
|
43
|
+
const source = opts.source ??
|
|
44
|
+
((query) => DEFAULT_MENTIONS.filter((m) => m.label.toLowerCase().includes(query.toLowerCase())));
|
|
45
|
+
return {
|
|
46
|
+
name: 'mention',
|
|
47
|
+
register: (editor, ctx) => {
|
|
48
|
+
const isActive = () => editor.getEditorState().read(() => $readQuery() !== null);
|
|
49
|
+
const refresh = () => {
|
|
50
|
+
const query = editor.getEditorState().read(() => $readQuery());
|
|
51
|
+
if (query === null) {
|
|
52
|
+
ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'hide' } });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const items = [...source(query)].slice(0, 8);
|
|
56
|
+
const { x, y } = caretXY();
|
|
57
|
+
ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'show', query, items, x, y } });
|
|
58
|
+
};
|
|
59
|
+
const nav = (delta, e) => {
|
|
60
|
+
if (!isActive())
|
|
61
|
+
return false;
|
|
62
|
+
e?.preventDefault();
|
|
63
|
+
ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'move', delta } });
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
return mergeRegister(editor.registerUpdateListener(() => refresh()), editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (e) => nav(1, e), COMMAND_PRIORITY_HIGH), editor.registerCommand(KEY_ARROW_UP_COMMAND, (e) => nav(-1, e), COMMAND_PRIORITY_HIGH), editor.registerCommand(KEY_ENTER_COMMAND, (e) => {
|
|
67
|
+
if (!isActive())
|
|
68
|
+
return false;
|
|
69
|
+
e?.preventDefault();
|
|
70
|
+
ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'choose' } });
|
|
71
|
+
return true;
|
|
72
|
+
}, COMMAND_PRIORITY_HIGH), editor.registerCommand(KEY_ESCAPE_COMMAND, () => {
|
|
73
|
+
if (!isActive())
|
|
74
|
+
return false;
|
|
75
|
+
ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'hide' } });
|
|
76
|
+
return true;
|
|
77
|
+
}, COMMAND_PRIORITY_HIGH));
|
|
78
|
+
},
|
|
79
|
+
ui: definePluginUI({
|
|
80
|
+
init: () => ({ open: false, query: '', items: [], index: 0, x: 0, y: 0 }),
|
|
81
|
+
update: (state, msg) => {
|
|
82
|
+
switch (msg.type) {
|
|
83
|
+
case 'show':
|
|
84
|
+
return {
|
|
85
|
+
open: msg.items.length > 0,
|
|
86
|
+
query: msg.query,
|
|
87
|
+
items: withActive(msg.items, 0),
|
|
88
|
+
index: 0,
|
|
89
|
+
x: msg.x,
|
|
90
|
+
y: msg.y,
|
|
91
|
+
};
|
|
92
|
+
case 'hide':
|
|
93
|
+
return hideOverlay(state);
|
|
94
|
+
case 'move': {
|
|
95
|
+
if (!state.open || state.items.length === 0)
|
|
96
|
+
return state;
|
|
97
|
+
const index = (state.index + msg.delta + state.items.length) % state.items.length;
|
|
98
|
+
return { ...state, index, items: withActive(state.items, index) };
|
|
99
|
+
}
|
|
100
|
+
case 'choose': {
|
|
101
|
+
const item = state.items[state.index];
|
|
102
|
+
if (!state.open || !item)
|
|
103
|
+
return hideOverlay(state);
|
|
104
|
+
return [
|
|
105
|
+
{ ...state, open: false },
|
|
106
|
+
[{ type: 'insert', label: item.label, query: state.query }],
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
case 'click': {
|
|
110
|
+
const item = state.items[msg.index];
|
|
111
|
+
if (!item)
|
|
112
|
+
return state;
|
|
113
|
+
return [
|
|
114
|
+
{ ...state, open: false },
|
|
115
|
+
[{ type: 'insert', label: item.label, query: state.query }],
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
onEffect: (effect, ctx) => {
|
|
121
|
+
const editor = ctx.editor();
|
|
122
|
+
if (!editor)
|
|
123
|
+
return;
|
|
124
|
+
editor.update(() => {
|
|
125
|
+
const selection = $getSelection();
|
|
126
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
127
|
+
return;
|
|
128
|
+
const node = selection.anchor.getNode();
|
|
129
|
+
if (!$isTextNode(node))
|
|
130
|
+
return;
|
|
131
|
+
const offset = selection.anchor.offset;
|
|
132
|
+
const start = offset - effect.query.length - 1;
|
|
133
|
+
if (start >= 0)
|
|
134
|
+
node.spliceText(start, effect.query.length + 1, `@${effect.label} `, true);
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
view: ({ state, send }) => overlayRoot({
|
|
138
|
+
open: state.at('open'),
|
|
139
|
+
x: state.at('x'),
|
|
140
|
+
y: state.at('y'),
|
|
141
|
+
zIndex: OVERLAY_Z.typeahead,
|
|
142
|
+
attrs: { 'data-scope': 'md-slash', 'data-part': 'root' },
|
|
143
|
+
children: () => [
|
|
144
|
+
each(state.at('items'), {
|
|
145
|
+
key: (it) => it.id,
|
|
146
|
+
render: (item, index) => [
|
|
147
|
+
div({
|
|
148
|
+
'data-scope': 'md-slash',
|
|
149
|
+
'data-part': 'option',
|
|
150
|
+
'data-active': item.map((it) => (it.active ? '' : undefined)),
|
|
151
|
+
onMouseDown: (e) => {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
send({ type: 'click', index: index.peek() });
|
|
154
|
+
},
|
|
155
|
+
}, [text(item.map((it) => `@${it.label}`))]),
|
|
156
|
+
],
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
}),
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=mention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mention.js","sourceRoot":"","sources":["../../src/plugins/mention.ts"],"names":[],"mappings":"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","sourcesContent":["// Mention plugin — an `@`-triggered typeahead. Same shape as the slash menu, but\n// the candidates come from a configurable `source`, and choosing inserts the\n// mention text (`@label`) rather than running a command. Demonstrates a second\n// typeahead built on the plugin-UI seam with no new machinery.\n\nimport {\n $getSelection,\n $isRangeSelection,\n $isTextNode,\n COMMAND_PRIORITY_HIGH,\n KEY_ARROW_DOWN_COMMAND,\n KEY_ARROW_UP_COMMAND,\n KEY_ENTER_COMMAND,\n KEY_ESCAPE_COMMAND,\n} from 'lexical'\nimport { mergeRegister } from '@lexical/utils'\nimport { div, each, text, type Signal } from '@llui/dom'\nimport { definePluginUI } from './ui.js'\nimport { OVERLAY_Z, hideOverlay, overlayRoot } from './overlay.js'\nimport type { MarkdownPlugin } from './types.js'\n\nexport interface Mention {\n id: string\n label: string\n}\n\ninterface Row {\n id: string\n label: string\n active: boolean\n}\n\ninterface MentionState {\n open: boolean\n query: string\n items: Row[]\n index: number\n x: number\n y: number\n}\n\ntype MentionMsg =\n | { type: 'show'; query: string; items: Mention[]; x: number; y: number }\n | { type: 'hide' }\n | { type: 'move'; delta: number }\n | { type: 'choose' }\n | { type: 'click'; index: number }\n\ntype MentionEffect = { type: 'insert'; label: string; query: string }\n\nconst TRIGGER = /(?:^|\\s)@(\\w*)$/\n\nconst DEFAULT_MENTIONS: readonly Mention[] = [\n { id: 'franco', label: 'Franco' },\n { id: 'ada', label: 'Ada' },\n { id: 'grace', label: 'Grace' },\n { id: 'linus', label: 'Linus' },\n { id: 'margaret', label: 'Margaret' },\n]\n\nfunction withActive(items: readonly Mention[], index: number): Row[] {\n return items.map((it, i) => ({ id: it.id, label: it.label, active: i === index }))\n}\n\nfunction $readQuery(): string | null {\n const selection = $getSelection()\n if (!$isRangeSelection(selection) || !selection.isCollapsed()) return null\n const node = selection.anchor.getNode()\n if (!$isTextNode(node)) return null\n const before = node.getTextContent().slice(0, selection.anchor.offset)\n const match = before.match(TRIGGER)\n return match ? (match[1] ?? '') : null\n}\n\nfunction caretXY(): { x: number; y: number } {\n if (typeof window === 'undefined') return { x: 0, y: 0 }\n const sel = window.getSelection()\n if (sel && sel.rangeCount > 0) {\n const rect = sel.getRangeAt(0).getBoundingClientRect()\n return { x: rect.left, y: rect.bottom + 4 }\n }\n return { x: 0, y: 0 }\n}\n\nexport interface MentionPluginOptions {\n /** Resolve candidates for a query (default: a small sample list). */\n source?: (query: string) => readonly Mention[]\n}\n\nexport function mentionPlugin(opts: MentionPluginOptions = {}): MarkdownPlugin {\n const source =\n opts.source ??\n ((query: string) =>\n DEFAULT_MENTIONS.filter((m) => m.label.toLowerCase().includes(query.toLowerCase())))\n\n return {\n name: 'mention',\n register: (editor, ctx) => {\n const isActive = (): boolean => editor.getEditorState().read(() => $readQuery() !== null)\n\n const refresh = (): void => {\n const query = editor.getEditorState().read(() => $readQuery())\n if (query === null) {\n ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'hide' } })\n return\n }\n const items = [...source(query)].slice(0, 8)\n const { x, y } = caretXY()\n ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'show', query, items, x, y } })\n }\n\n const nav = (delta: number, e: KeyboardEvent | null): boolean => {\n if (!isActive()) return false\n e?.preventDefault()\n ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'move', delta } })\n return true\n }\n\n return mergeRegister(\n editor.registerUpdateListener(() => refresh()),\n editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (e) => nav(1, e), COMMAND_PRIORITY_HIGH),\n editor.registerCommand(KEY_ARROW_UP_COMMAND, (e) => nav(-1, e), COMMAND_PRIORITY_HIGH),\n editor.registerCommand(\n KEY_ENTER_COMMAND,\n (e) => {\n if (!isActive()) return false\n e?.preventDefault()\n ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'choose' } })\n return true\n },\n COMMAND_PRIORITY_HIGH,\n ),\n editor.registerCommand(\n KEY_ESCAPE_COMMAND,\n () => {\n if (!isActive()) return false\n ctx.emit({ type: 'plugin', name: 'mention', msg: { type: 'hide' } })\n return true\n },\n COMMAND_PRIORITY_HIGH,\n ),\n )\n },\n ui: definePluginUI<MentionState, MentionMsg, MentionEffect>({\n init: () => ({ open: false, query: '', items: [], index: 0, x: 0, y: 0 }),\n update: (state, msg) => {\n switch (msg.type) {\n case 'show':\n return {\n open: msg.items.length > 0,\n query: msg.query,\n items: withActive(msg.items, 0),\n index: 0,\n x: msg.x,\n y: msg.y,\n }\n case 'hide':\n return hideOverlay(state)\n case 'move': {\n if (!state.open || state.items.length === 0) return state\n const index = (state.index + msg.delta + state.items.length) % state.items.length\n return { ...state, index, items: withActive(state.items, index) }\n }\n case 'choose': {\n const item = state.items[state.index]\n if (!state.open || !item) return hideOverlay(state)\n return [\n { ...state, open: false },\n [{ type: 'insert', label: item.label, query: state.query }],\n ]\n }\n case 'click': {\n const item = state.items[msg.index]\n if (!item) return state\n return [\n { ...state, open: false },\n [{ type: 'insert', label: item.label, query: state.query }],\n ]\n }\n }\n },\n onEffect: (effect, ctx) => {\n const editor = ctx.editor()\n if (!editor) return\n editor.update(() => {\n const selection = $getSelection()\n if (!$isRangeSelection(selection) || !selection.isCollapsed()) return\n const node = selection.anchor.getNode()\n if (!$isTextNode(node)) return\n const offset = selection.anchor.offset\n const start = offset - effect.query.length - 1\n if (start >= 0) node.spliceText(start, effect.query.length + 1, `@${effect.label} `, true)\n })\n },\n view: ({ state, send }) =>\n overlayRoot({\n open: state.at('open'),\n x: state.at('x'),\n y: state.at('y'),\n zIndex: OVERLAY_Z.typeahead,\n attrs: { 'data-scope': 'md-slash', 'data-part': 'root' },\n children: () => [\n each(state.at('items') as Signal<Row[]>, {\n key: (it) => it.id,\n render: (item, index) => [\n div(\n {\n 'data-scope': 'md-slash',\n 'data-part': 'option',\n 'data-active': item.map((it) => (it.active ? '' : undefined)),\n onMouseDown: (e: MouseEvent) => {\n e.preventDefault()\n send({ type: 'click', index: index.peek() })\n },\n },\n [text(item.map((it) => `@${it.label}`))],\n ),\n ],\n }),\n ],\n }),\n }),\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MarkdownPlugin } from './types.js';
|
|
2
|
+
export interface MermaidPluginOptions {
|
|
3
|
+
/** Render the diagram source to an HTML string (e.g. mermaid). When omitted,
|
|
4
|
+
* the raw source is shown in a styled box. */
|
|
5
|
+
render?: (code: string) => string;
|
|
6
|
+
}
|
|
7
|
+
export declare function mermaidPlugin(opts?: MermaidPluginOptions): MarkdownPlugin;
|
|
8
|
+
//# sourceMappingURL=mermaid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mermaid.d.ts","sourceRoot":"","sources":["../../src/plugins/mermaid.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAkBhD,MAAM,WAAW,oBAAoB;IACnC;kDAC8C;IAC9C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC;AAED,wBAAgB,aAAa,CAAC,IAAI,GAAE,oBAAyB,GAAG,cAAc,CA0F7E"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Mermaid plugin — a fenced ```mermaid block rendered via the decorator bridge.
|
|
2
|
+
// The diagram source is editable (persists on blur); an optional `render` callback
|
|
3
|
+
// (e.g. the mermaid library) produces the diagram. Round-trips to a ```mermaid
|
|
4
|
+
// fence.
|
|
5
|
+
//
|
|
6
|
+
// NOTE: place `mermaidPlugin()` BEFORE `corePlugin()` in the plugins array so its
|
|
7
|
+
// multiline transformer matches ```mermaid before the generic code-block one.
|
|
8
|
+
import {} from 'lexical';
|
|
9
|
+
import { $insertNodeToNearestRoot } from '@lexical/utils';
|
|
10
|
+
import { $createLLuiDecoratorNode, $isLLuiDecoratorNode, LLuiDecoratorNode, decoratorBridge, } from '@llui/lexical';
|
|
11
|
+
import { component, div, text, unsafeHtml } from '@llui/dom';
|
|
12
|
+
const BRIDGE_TYPE = 'mermaid';
|
|
13
|
+
function isMermaidData(value) {
|
|
14
|
+
return (typeof value === 'object' && value !== null && typeof value.code === 'string');
|
|
15
|
+
}
|
|
16
|
+
const stop = (e) => e.stopPropagation();
|
|
17
|
+
export function mermaidPlugin(opts = {}) {
|
|
18
|
+
const bridge = decoratorBridge(BRIDGE_TYPE, (data, api) => component({
|
|
19
|
+
name: 'Mermaid',
|
|
20
|
+
init: () => ({ code: data.code }),
|
|
21
|
+
update: (state, msg) => {
|
|
22
|
+
if (msg.type === 'commit') {
|
|
23
|
+
if (msg.code === state.code)
|
|
24
|
+
return state;
|
|
25
|
+
api.update({ code: msg.code });
|
|
26
|
+
return { code: msg.code };
|
|
27
|
+
}
|
|
28
|
+
return state;
|
|
29
|
+
},
|
|
30
|
+
view: ({ state, send }) => {
|
|
31
|
+
const children = [
|
|
32
|
+
div({
|
|
33
|
+
'data-part': 'source',
|
|
34
|
+
contenteditable: 'true',
|
|
35
|
+
role: 'textbox',
|
|
36
|
+
'aria-label': 'Mermaid source',
|
|
37
|
+
onKeyDown: stop,
|
|
38
|
+
onBeforeInput: stop,
|
|
39
|
+
onPaste: stop,
|
|
40
|
+
onBlur: (e) => send({ type: 'commit', code: e.target.textContent ?? '' }),
|
|
41
|
+
}, [text(state.at('code'))]),
|
|
42
|
+
];
|
|
43
|
+
if (opts.render) {
|
|
44
|
+
children.push(div({ 'data-part': 'preview', contenteditable: 'false' }, [
|
|
45
|
+
unsafeHtml(state.at('code').map((code) => opts.render(code))),
|
|
46
|
+
]));
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
div({ 'data-scope': 'md-mermaid', 'data-part': 'root', contenteditable: 'false' }, children),
|
|
50
|
+
];
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
const transformer = {
|
|
54
|
+
dependencies: [LLuiDecoratorNode],
|
|
55
|
+
export: (node) => {
|
|
56
|
+
if (!$isLLuiDecoratorNode(node) || node.getBridgeType() !== BRIDGE_TYPE)
|
|
57
|
+
return null;
|
|
58
|
+
const data = node.getData();
|
|
59
|
+
return isMermaidData(data) ? '```mermaid\n' + data.code + '\n```' : null;
|
|
60
|
+
},
|
|
61
|
+
regExpStart: /^```mermaid$/,
|
|
62
|
+
regExpEnd: /^```$/,
|
|
63
|
+
replace: (rootNode, _children, _startMatch, _endMatch, linesInBetween) => {
|
|
64
|
+
const lines = [...(linesInBetween ?? [])];
|
|
65
|
+
while (lines.length > 0 && lines[0] === '')
|
|
66
|
+
lines.shift();
|
|
67
|
+
while (lines.length > 0 && lines[lines.length - 1] === '')
|
|
68
|
+
lines.pop();
|
|
69
|
+
rootNode.append($createLLuiDecoratorNode(BRIDGE_TYPE, { code: lines.join('\n') }));
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
type: 'multiline-element',
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
name: 'mermaid',
|
|
76
|
+
nodes: [LLuiDecoratorNode],
|
|
77
|
+
decorators: [bridge],
|
|
78
|
+
transformers: [transformer],
|
|
79
|
+
items: [
|
|
80
|
+
{
|
|
81
|
+
id: 'mermaid',
|
|
82
|
+
label: 'Diagram',
|
|
83
|
+
icon: 'mermaid',
|
|
84
|
+
group: 'insert',
|
|
85
|
+
keywords: ['mermaid', 'diagram', 'flowchart', 'graph'],
|
|
86
|
+
run: (editor) => editor.update(() => $insertNodeToNearestRoot($createLLuiDecoratorNode(BRIDGE_TYPE, { code: 'graph TD\n A --> B' }))),
|
|
87
|
+
surfaces: ['slash', 'context'],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=mermaid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../../src/plugins/mermaid.ts"],"names":[],"mappings":"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","sourcesContent":["// Mermaid plugin — a fenced ```mermaid block rendered via the decorator bridge.\n// The diagram source is editable (persists on blur); an optional `render` callback\n// (e.g. the mermaid library) produces the diagram. Round-trips to a ```mermaid\n// fence.\n//\n// NOTE: place `mermaidPlugin()` BEFORE `corePlugin()` in the plugins array so its\n// multiline transformer matches ```mermaid before the generic code-block one.\n\nimport { type LexicalNode } from 'lexical'\nimport { $insertNodeToNearestRoot } from '@lexical/utils'\nimport type { MultilineElementTransformer } from '@lexical/markdown'\nimport {\n $createLLuiDecoratorNode,\n $isLLuiDecoratorNode,\n LLuiDecoratorNode,\n decoratorBridge,\n} from '@llui/lexical'\nimport { component, div, text, unsafeHtml, type Mountable, type Signal } from '@llui/dom'\nimport type { MarkdownPlugin } from './types.js'\n\nconst BRIDGE_TYPE = 'mermaid'\n\ninterface MermaidData {\n code: string\n}\n\nfunction isMermaidData(value: unknown): value is MermaidData {\n return (\n typeof value === 'object' && value !== null && typeof (value as MermaidData).code === 'string'\n )\n}\n\ntype MermaidMsg = { type: 'commit'; code: string }\n\nconst stop = (e: Event): void => e.stopPropagation()\n\nexport interface MermaidPluginOptions {\n /** Render the diagram source to an HTML string (e.g. mermaid). When omitted,\n * the raw source is shown in a styled box. */\n render?: (code: string) => string\n}\n\nexport function mermaidPlugin(opts: MermaidPluginOptions = {}): MarkdownPlugin {\n const bridge = decoratorBridge<MermaidData, MermaidData, MermaidMsg, never>(\n BRIDGE_TYPE,\n (data, api) =>\n component<MermaidData, MermaidMsg, never>({\n name: 'Mermaid',\n init: () => ({ code: data.code }),\n update: (state, msg) => {\n if (msg.type === 'commit') {\n if (msg.code === state.code) return state\n api.update({ code: msg.code })\n return { code: msg.code }\n }\n return state\n },\n view: ({ state, send }) => {\n const children: Mountable[] = [\n div(\n {\n 'data-part': 'source',\n contenteditable: 'true',\n role: 'textbox',\n 'aria-label': 'Mermaid source',\n onKeyDown: stop,\n onBeforeInput: stop,\n onPaste: stop,\n onBlur: (e: FocusEvent) =>\n send({ type: 'commit', code: (e.target as HTMLElement).textContent ?? '' }),\n },\n [text(state.at('code') as Signal<string>)],\n ),\n ]\n if (opts.render) {\n children.push(\n div({ 'data-part': 'preview', contenteditable: 'false' }, [\n unsafeHtml(state.at('code').map((code) => opts.render!(code)) as Signal<string>),\n ]),\n )\n }\n return [\n div(\n { 'data-scope': 'md-mermaid', 'data-part': 'root', contenteditable: 'false' },\n children,\n ),\n ]\n },\n }),\n )\n\n const transformer: MultilineElementTransformer = {\n dependencies: [LLuiDecoratorNode],\n export: (node: LexicalNode): string | null => {\n if (!$isLLuiDecoratorNode(node) || node.getBridgeType() !== BRIDGE_TYPE) return null\n const data = node.getData()\n return isMermaidData(data) ? '```mermaid\\n' + data.code + '\\n```' : null\n },\n regExpStart: /^```mermaid$/,\n regExpEnd: /^```$/,\n replace: (rootNode, _children, _startMatch, _endMatch, linesInBetween): boolean => {\n const lines = [...(linesInBetween ?? [])]\n while (lines.length > 0 && lines[0] === '') lines.shift()\n while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()\n rootNode.append($createLLuiDecoratorNode(BRIDGE_TYPE, { code: lines.join('\\n') }))\n return true\n },\n type: 'multiline-element',\n }\n\n return {\n name: 'mermaid',\n nodes: [LLuiDecoratorNode],\n decorators: [bridge],\n transformers: [transformer],\n items: [\n {\n id: 'mermaid',\n label: 'Diagram',\n icon: 'mermaid',\n group: 'insert',\n keywords: ['mermaid', 'diagram', 'flowchart', 'graph'],\n run: (editor) =>\n editor.update(() =>\n $insertNodeToNearestRoot(\n $createLLuiDecoratorNode(BRIDGE_TYPE, { code: 'graph TD\\n A --> B' }),\n ),\n ),\n surfaces: ['slash', 'context'],\n },\n ],\n }\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Renderable, type Signal } from '@llui/dom';
|
|
2
|
+
export declare const OVERLAY_Z: {
|
|
3
|
+
readonly typeahead: 60;
|
|
4
|
+
readonly contextMenu: 61;
|
|
5
|
+
readonly floatingToolbar: 62;
|
|
6
|
+
readonly tableTools: 63;
|
|
7
|
+
};
|
|
8
|
+
export interface OverlayRootConfig {
|
|
9
|
+
/** Whether the overlay is shown. */
|
|
10
|
+
open: Signal<boolean>;
|
|
11
|
+
/** Viewport x of the anchor point (px). */
|
|
12
|
+
x: Signal<number>;
|
|
13
|
+
/** Viewport y of the anchor point (px). */
|
|
14
|
+
y: Signal<number>;
|
|
15
|
+
/** Stacking level — use {@link OVERLAY_Z}. */
|
|
16
|
+
zIndex: number;
|
|
17
|
+
/** Extra CSS appended after positioning (e.g. a centering/lift transform). */
|
|
18
|
+
transform?: string;
|
|
19
|
+
/** Attributes for the positioned element (`data-scope`/`data-part`, handlers). */
|
|
20
|
+
attrs: Record<string, unknown>;
|
|
21
|
+
/** Optional siblings rendered inside the portal before the root (e.g. a backdrop). */
|
|
22
|
+
before?: () => Renderable;
|
|
23
|
+
/** The positioned element's children. */
|
|
24
|
+
children: () => Renderable;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A portaled, `position:fixed` overlay placed at `(x, y)` and shown while `open`.
|
|
28
|
+
* Returns the `Renderable` a plugin's `view` yields directly. The emitted style is
|
|
29
|
+
* a deterministic string — `position:fixed;left:${x}px;top:${y}px;z-index:${z}` plus
|
|
30
|
+
* an optional `transform` — so positioning is unit-testable.
|
|
31
|
+
*/
|
|
32
|
+
export declare function overlayRoot(cfg: OverlayRootConfig): Renderable;
|
|
33
|
+
/** Guarded close: collapse `open` to false, preserving the reference when already
|
|
34
|
+
* closed so the host doesn't reconcile a no-op. */
|
|
35
|
+
export declare function hideOverlay<S extends {
|
|
36
|
+
open: boolean;
|
|
37
|
+
}>(state: S): S;
|
|
38
|
+
/**
|
|
39
|
+
* Run `run` (debounced to one call per animation frame) whenever the viewport
|
|
40
|
+
* changes — any ancestor scroll or a window resize. Overlays anchored to a
|
|
41
|
+
* persistent element (a table) or a live selection must reposition on scroll;
|
|
42
|
+
* editor update listeners alone never fire for a plain scroll. Returns a cleanup
|
|
43
|
+
* that removes the listeners. No-op (and cleanup is a no-op) outside the browser.
|
|
44
|
+
*/
|
|
45
|
+
export declare function onViewportChange(run: () => void): () => void;
|
|
46
|
+
//# sourceMappingURL=overlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../src/plugins/overlay.ts"],"names":[],"mappings":"AAsBA,OAAO,EAA8C,KAAK,UAAU,EAAE,KAAK,MAAM,EAAE,MAAM,WAAW,CAAA;AAEpG,eAAO,MAAM,SAAS;;;;;CAKZ,CAAA;AAIV,MAAM,WAAW,iBAAiB;IAChC,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IACrB,2CAA2C;IAC3C,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;IACjB,2CAA2C;IAC3C,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;IACjB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,UAAU,CAAA;IACzB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,UAAU,CAAA;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,UAAU,CAgB9D;AAED;mDACmD;AACnD,wBAAgB,WAAW,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAoB5D"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Shared overlay primitive for plugin-UI surfaces (context menu, slash/mention
|
|
2
|
+
// typeaheads, floating toolbar, table tools). Every one of these is a portaled,
|
|
3
|
+
// viewport-positioned panel whose `{ open, x, y }` lives in pure, JSON state and
|
|
4
|
+
// is rendered with `show → portal → fixed-positioned div`. Factoring that single
|
|
5
|
+
// shape here keeps positioning testable (a plain style string, not imperative
|
|
6
|
+
// floating-ui calls) and removes the copy-paste that otherwise accumulates one
|
|
7
|
+
// near-identical state machine per overlay plugin.
|
|
8
|
+
//
|
|
9
|
+
// Anchored positioning via `@llui/components`' `attachFloating` (floating-ui) was
|
|
10
|
+
// considered: it gives flip/shift/auto-update for free but needs a live element
|
|
11
|
+
// ref and async, jsdom-untestable DOM writes — a poor fit for overlays whose
|
|
12
|
+
// position is derived from a transient caret/pointer/element rect each `register`
|
|
13
|
+
// tick. The pure-state idiom below covers scroll/resize via {@link onViewportChange}.
|
|
14
|
+
//
|
|
15
|
+
// ## Stacking order (z-index)
|
|
16
|
+
// A single documented scale so overlays never tie and obscure each other:
|
|
17
|
+
// 60 — typeaheads (slash, mention) — caret-anchored, lowest
|
|
18
|
+
// 61 — context menu — pointer-anchored
|
|
19
|
+
// 62 — floating selection toolbar — selection-anchored bubble
|
|
20
|
+
// 63 — table tools — element-anchored, sits over a table
|
|
21
|
+
// The values live with each plugin's view; this comment is the source of truth.
|
|
22
|
+
import { div, derived, portal, show } from '@llui/dom';
|
|
23
|
+
export const OVERLAY_Z = {
|
|
24
|
+
typeahead: 60,
|
|
25
|
+
contextMenu: 61,
|
|
26
|
+
floatingToolbar: 62,
|
|
27
|
+
tableTools: 63,
|
|
28
|
+
};
|
|
29
|
+
const toMountables = (r) => [...r];
|
|
30
|
+
/**
|
|
31
|
+
* A portaled, `position:fixed` overlay placed at `(x, y)` and shown while `open`.
|
|
32
|
+
* Returns the `Renderable` a plugin's `view` yields directly. The emitted style is
|
|
33
|
+
* a deterministic string — `position:fixed;left:${x}px;top:${y}px;z-index:${z}` plus
|
|
34
|
+
* an optional `transform` — so positioning is unit-testable.
|
|
35
|
+
*/
|
|
36
|
+
export function overlayRoot(cfg) {
|
|
37
|
+
const style = derived(cfg.x, cfg.y, (x, y) => `position:fixed;left:${x}px;top:${y}px;z-index:${cfg.zIndex}` +
|
|
38
|
+
(cfg.transform ? `;${cfg.transform}` : ''));
|
|
39
|
+
return [
|
|
40
|
+
show(cfg.open, () => [
|
|
41
|
+
portal(() => [
|
|
42
|
+
...(cfg.before ? toMountables(cfg.before()) : []),
|
|
43
|
+
div({ ...cfg.attrs, style }, toMountables(cfg.children())),
|
|
44
|
+
]),
|
|
45
|
+
]),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
/** Guarded close: collapse `open` to false, preserving the reference when already
|
|
49
|
+
* closed so the host doesn't reconcile a no-op. */
|
|
50
|
+
export function hideOverlay(state) {
|
|
51
|
+
return state.open ? { ...state, open: false } : state;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run `run` (debounced to one call per animation frame) whenever the viewport
|
|
55
|
+
* changes — any ancestor scroll or a window resize. Overlays anchored to a
|
|
56
|
+
* persistent element (a table) or a live selection must reposition on scroll;
|
|
57
|
+
* editor update listeners alone never fire for a plain scroll. Returns a cleanup
|
|
58
|
+
* that removes the listeners. No-op (and cleanup is a no-op) outside the browser.
|
|
59
|
+
*/
|
|
60
|
+
export function onViewportChange(run) {
|
|
61
|
+
if (typeof window === 'undefined' || typeof requestAnimationFrame !== 'function') {
|
|
62
|
+
return () => { };
|
|
63
|
+
}
|
|
64
|
+
let pending = 0;
|
|
65
|
+
const handler = () => {
|
|
66
|
+
if (pending)
|
|
67
|
+
return;
|
|
68
|
+
pending = requestAnimationFrame(() => {
|
|
69
|
+
pending = 0;
|
|
70
|
+
run();
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
// Capture phase so scrolls in any nested scroll container are caught.
|
|
74
|
+
window.addEventListener('scroll', handler, true);
|
|
75
|
+
window.addEventListener('resize', handler);
|
|
76
|
+
return () => {
|
|
77
|
+
window.removeEventListener('scroll', handler, true);
|
|
78
|
+
window.removeEventListener('resize', handler);
|
|
79
|
+
if (pending)
|
|
80
|
+
cancelAnimationFrame(pending);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=overlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlay.js","sourceRoot":"","sources":["../../src/plugins/overlay.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,gFAAgF;AAChF,iFAAiF;AACjF,iFAAiF;AACjF,8EAA8E;AAC9E,+EAA+E;AAC/E,mDAAmD;AACnD,EAAE;AACF,kFAAkF;AAClF,gFAAgF;AAChF,6EAA6E;AAC7E,kFAAkF;AAClF,sFAAsF;AACtF,EAAE;AACF,8BAA8B;AAC9B,0EAA0E;AAC1E,qEAAqE;AACrE,gEAAgE;AAChE,yEAAyE;AACzE,mFAAmF;AACnF,gFAAgF;AAEhF,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAgD,MAAM,WAAW,CAAA;AAEpG,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,SAAS,EAAE,EAAE;IACb,WAAW,EAAE,EAAE;IACf,eAAe,EAAE,EAAE;IACnB,UAAU,EAAE,EAAE;CACN,CAAA;AAEV,MAAM,YAAY,GAAG,CAAC,CAAa,EAAe,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAqB3D;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAsB;IAChD,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,CAAC,CAAC,EACL,GAAG,CAAC,CAAC,EACL,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACP,uBAAuB,CAAC,UAAU,CAAC,cAAc,GAAG,CAAC,MAAM,EAAE;QAC7D,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAC3B,CAAA;IACnB,OAAO;QACL,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC;YACnB,MAAM,CAAC,GAAG,EAAE,CAAC;gBACX,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;aAC3D,CAAC;SACH,CAAC;KACH,CAAA;AACH,CAAC;AAED;mDACmD;AACnD,MAAM,UAAU,WAAW,CAA8B,KAAQ;IAC/D,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAe;IAC9C,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,qBAAqB,KAAK,UAAU,EAAE,CAAC;QACjF,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;IACjB,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,IAAI,OAAO;YAAE,OAAM;QACnB,OAAO,GAAG,qBAAqB,CAAC,GAAG,EAAE;YACnC,OAAO,GAAG,CAAC,CAAA;YACX,GAAG,EAAE,CAAA;QACP,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;IACD,sEAAsE;IACtE,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IAChD,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC1C,OAAO,GAAG,EAAE;QACV,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;QACnD,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC7C,IAAI,OAAO;YAAE,oBAAoB,CAAC,OAAO,CAAC,CAAA;IAC5C,CAAC,CAAA;AACH,CAAC","sourcesContent":["// Shared overlay primitive for plugin-UI surfaces (context menu, slash/mention\n// typeaheads, floating toolbar, table tools). Every one of these is a portaled,\n// viewport-positioned panel whose `{ open, x, y }` lives in pure, JSON state and\n// is rendered with `show → portal → fixed-positioned div`. Factoring that single\n// shape here keeps positioning testable (a plain style string, not imperative\n// floating-ui calls) and removes the copy-paste that otherwise accumulates one\n// near-identical state machine per overlay plugin.\n//\n// Anchored positioning via `@llui/components`' `attachFloating` (floating-ui) was\n// considered: it gives flip/shift/auto-update for free but needs a live element\n// ref and async, jsdom-untestable DOM writes — a poor fit for overlays whose\n// position is derived from a transient caret/pointer/element rect each `register`\n// tick. The pure-state idiom below covers scroll/resize via {@link onViewportChange}.\n//\n// ## Stacking order (z-index)\n// A single documented scale so overlays never tie and obscure each other:\n// 60 — typeaheads (slash, mention) — caret-anchored, lowest\n// 61 — context menu — pointer-anchored\n// 62 — floating selection toolbar — selection-anchored bubble\n// 63 — table tools — element-anchored, sits over a table\n// The values live with each plugin's view; this comment is the source of truth.\n\nimport { div, derived, portal, show, type Mountable, type Renderable, type Signal } from '@llui/dom'\n\nexport const OVERLAY_Z = {\n typeahead: 60,\n contextMenu: 61,\n floatingToolbar: 62,\n tableTools: 63,\n} as const\n\nconst toMountables = (r: Renderable): Mountable[] => [...r]\n\nexport interface OverlayRootConfig {\n /** Whether the overlay is shown. */\n open: Signal<boolean>\n /** Viewport x of the anchor point (px). */\n x: Signal<number>\n /** Viewport y of the anchor point (px). */\n y: Signal<number>\n /** Stacking level — use {@link OVERLAY_Z}. */\n zIndex: number\n /** Extra CSS appended after positioning (e.g. a centering/lift transform). */\n transform?: string\n /** Attributes for the positioned element (`data-scope`/`data-part`, handlers). */\n attrs: Record<string, unknown>\n /** Optional siblings rendered inside the portal before the root (e.g. a backdrop). */\n before?: () => Renderable\n /** The positioned element's children. */\n children: () => Renderable\n}\n\n/**\n * A portaled, `position:fixed` overlay placed at `(x, y)` and shown while `open`.\n * Returns the `Renderable` a plugin's `view` yields directly. The emitted style is\n * a deterministic string — `position:fixed;left:${x}px;top:${y}px;z-index:${z}` plus\n * an optional `transform` — so positioning is unit-testable.\n */\nexport function overlayRoot(cfg: OverlayRootConfig): Renderable {\n const style = derived(\n cfg.x,\n cfg.y,\n (x, y) =>\n `position:fixed;left:${x}px;top:${y}px;z-index:${cfg.zIndex}` +\n (cfg.transform ? `;${cfg.transform}` : ''),\n ) as Signal<string>\n return [\n show(cfg.open, () => [\n portal(() => [\n ...(cfg.before ? toMountables(cfg.before()) : []),\n div({ ...cfg.attrs, style }, toMountables(cfg.children())),\n ]),\n ]),\n ]\n}\n\n/** Guarded close: collapse `open` to false, preserving the reference when already\n * closed so the host doesn't reconcile a no-op. */\nexport function hideOverlay<S extends { open: boolean }>(state: S): S {\n return state.open ? { ...state, open: false } : state\n}\n\n/**\n * Run `run` (debounced to one call per animation frame) whenever the viewport\n * changes — any ancestor scroll or a window resize. Overlays anchored to a\n * persistent element (a table) or a live selection must reposition on scroll;\n * editor update listeners alone never fire for a plain scroll. Returns a cleanup\n * that removes the listeners. No-op (and cleanup is a no-op) outside the browser.\n */\nexport function onViewportChange(run: () => void): () => void {\n if (typeof window === 'undefined' || typeof requestAnimationFrame !== 'function') {\n return () => {}\n }\n let pending = 0\n const handler = (): void => {\n if (pending) return\n pending = requestAnimationFrame(() => {\n pending = 0\n run()\n })\n }\n // Capture phase so scrolls in any nested scroll container are caught.\n window.addEventListener('scroll', handler, true)\n window.addEventListener('resize', handler)\n return () => {\n window.removeEventListener('scroll', handler, true)\n window.removeEventListener('resize', handler)\n if (pending) cancelAnimationFrame(pending)\n }\n}\n"]}
|