@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,50 @@
|
|
|
1
|
+
// Emoji plugin — replace `:shortcode:` with the emoji while typing or on import,
|
|
2
|
+
// via a text-match transformer. Unknown shortcodes are left untouched.
|
|
3
|
+
import { $createTextNode } from 'lexical';
|
|
4
|
+
/** A small default shortcode → emoji map. Extend via `emojiPlugin({ emoji })`. */
|
|
5
|
+
export const DEFAULT_EMOJI = {
|
|
6
|
+
smile: '😄',
|
|
7
|
+
grin: '😁',
|
|
8
|
+
joy: '😂',
|
|
9
|
+
wink: '😉',
|
|
10
|
+
heart: '❤️',
|
|
11
|
+
thumbsup: '👍',
|
|
12
|
+
'+1': '👍',
|
|
13
|
+
thumbsdown: '👎',
|
|
14
|
+
fire: '🔥',
|
|
15
|
+
tada: '🎉',
|
|
16
|
+
rocket: '🚀',
|
|
17
|
+
star: '⭐',
|
|
18
|
+
check: '✅',
|
|
19
|
+
x: '❌',
|
|
20
|
+
warning: '⚠️',
|
|
21
|
+
bulb: '💡',
|
|
22
|
+
eyes: '👀',
|
|
23
|
+
sparkles: '✨',
|
|
24
|
+
'100': '💯',
|
|
25
|
+
thinking: '🤔',
|
|
26
|
+
};
|
|
27
|
+
function transformer(map) {
|
|
28
|
+
return {
|
|
29
|
+
dependencies: [],
|
|
30
|
+
// `null` keeps the emoji as a unicode character in the exported markdown.
|
|
31
|
+
export: () => null,
|
|
32
|
+
importRegExp: /:([a-z0-9_+-]+):/,
|
|
33
|
+
regExp: /:([a-z0-9_+-]+):$/,
|
|
34
|
+
trigger: ':',
|
|
35
|
+
replace: (node, match) => {
|
|
36
|
+
const emoji = map[match[1] ?? ''];
|
|
37
|
+
if (emoji)
|
|
38
|
+
node.replace($createTextNode(emoji));
|
|
39
|
+
},
|
|
40
|
+
type: 'text-match',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function emojiPlugin(opts = {}) {
|
|
44
|
+
const map = { ...DEFAULT_EMOJI, ...(opts.emoji ?? {}) };
|
|
45
|
+
return {
|
|
46
|
+
name: 'emoji',
|
|
47
|
+
transformers: [transformer(map)],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=emoji.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emoji.js","sourceRoot":"","sources":["../../src/plugins/emoji.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,uEAAuE;AAEvE,OAAO,EAAE,eAAe,EAAiB,MAAM,SAAS,CAAA;AAIxD,kFAAkF;AAClF,MAAM,CAAC,MAAM,aAAa,GAAqC;IAC7D,KAAK,EAAE,IAAI;IACX,IAAI,EAAE,IAAI;IACV,GAAG,EAAE,IAAI;IACT,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,IAAI;IACX,QAAQ,EAAE,IAAI;IACd,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,MAAM,EAAE,IAAI;IACZ,IAAI,EAAE,GAAG;IACT,KAAK,EAAE,GAAG;IACV,CAAC,EAAE,GAAG;IACN,OAAO,EAAE,IAAI;IACb,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,QAAQ,EAAE,GAAG;IACb,KAAK,EAAE,IAAI;IACX,QAAQ,EAAE,IAAI;CACf,CAAA;AAED,SAAS,WAAW,CAAC,GAAqC;IACxD,OAAO;QACL,YAAY,EAAE,EAAE;QAChB,0EAA0E;QAC1E,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI;QAClB,YAAY,EAAE,kBAAkB;QAChC,MAAM,EAAE,mBAAmB;QAC3B,OAAO,EAAE,GAAG;QACZ,OAAO,EAAE,CAAC,IAAc,EAAE,KAAuB,EAAQ,EAAE;YACzD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;YACjC,IAAI,KAAK;gBAAE,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAA;QACjD,CAAC;QACD,IAAI,EAAE,YAAY;KACnB,CAAA;AACH,CAAC;AAOD,MAAM,UAAU,WAAW,CAAC,OAA2B,EAAE;IACvD,MAAM,GAAG,GAAG,EAAE,GAAG,aAAa,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAA;IACvD,OAAO;QACL,IAAI,EAAE,OAAO;QACb,YAAY,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;KACjC,CAAA;AACH,CAAC","sourcesContent":["// Emoji plugin — replace `:shortcode:` with the emoji while typing or on import,\n// via a text-match transformer. Unknown shortcodes are left untouched.\n\nimport { $createTextNode, type TextNode } from 'lexical'\nimport type { TextMatchTransformer } from '@lexical/markdown'\nimport type { MarkdownPlugin } from './types.js'\n\n/** A small default shortcode → emoji map. Extend via `emojiPlugin({ emoji })`. */\nexport const DEFAULT_EMOJI: Readonly<Record<string, string>> = {\n smile: '😄',\n grin: '😁',\n joy: '😂',\n wink: '😉',\n heart: '❤️',\n thumbsup: '👍',\n '+1': '👍',\n thumbsdown: '👎',\n fire: '🔥',\n tada: '🎉',\n rocket: '🚀',\n star: '⭐',\n check: '✅',\n x: '❌',\n warning: '⚠️',\n bulb: '💡',\n eyes: '👀',\n sparkles: '✨',\n '100': '💯',\n thinking: '🤔',\n}\n\nfunction transformer(map: Readonly<Record<string, string>>): TextMatchTransformer {\n return {\n dependencies: [],\n // `null` keeps the emoji as a unicode character in the exported markdown.\n export: () => null,\n importRegExp: /:([a-z0-9_+-]+):/,\n regExp: /:([a-z0-9_+-]+):$/,\n trigger: ':',\n replace: (node: TextNode, match: RegExpMatchArray): void => {\n const emoji = map[match[1] ?? '']\n if (emoji) node.replace($createTextNode(emoji))\n },\n type: 'text-match',\n }\n}\n\nexport interface EmojiPluginOptions {\n /** Extra/override shortcode → emoji entries (merged over the defaults). */\n emoji?: Readonly<Record<string, string>>\n}\n\nexport function emojiPlugin(opts: EmojiPluginOptions = {}): MarkdownPlugin {\n const map = { ...DEFAULT_EMOJI, ...(opts.emoji ?? {}) }\n return {\n name: 'emoji',\n transformers: [transformer(map)],\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floating-toolbar.d.ts","sourceRoot":"","sources":["../../src/plugins/floating-toolbar.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,YAAY,CAAA;AAgE7D,wBAAgB,qBAAqB,IAAI,cAAc,CAgGtD"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Floating selection toolbar — a bubble of inline-format actions that appears
|
|
2
|
+
// above a non-collapsed text selection. A plugin-UI overlay: `register` watches
|
|
3
|
+
// selection changes and positions/fills the bar; clicking a button runs the
|
|
4
|
+
// command on the still-live selection.
|
|
5
|
+
import { $getSelection, $isRangeSelection } from 'lexical';
|
|
6
|
+
import { $findMatchingParent, mergeRegister } from '@lexical/utils';
|
|
7
|
+
import { $isLinkNode } from '@lexical/link';
|
|
8
|
+
import { button, each, span, text, unsafeHtml } from '@llui/dom';
|
|
9
|
+
import { definePluginUI } from './ui.js';
|
|
10
|
+
import { OVERLAY_Z, hideOverlay, onViewportChange, overlayRoot } from './overlay.js';
|
|
11
|
+
import { DEFAULT_GLYPHS } from '../surfaces/toolbar.js';
|
|
12
|
+
function readFormat(editor) {
|
|
13
|
+
return editor.getEditorState().read(() => {
|
|
14
|
+
const selection = $getSelection();
|
|
15
|
+
if (!$isRangeSelection(selection)) {
|
|
16
|
+
return { bold: false, italic: false, strikethrough: false, code: false, link: false };
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
bold: selection.hasFormat('bold'),
|
|
20
|
+
italic: selection.hasFormat('italic'),
|
|
21
|
+
strikethrough: selection.hasFormat('strikethrough'),
|
|
22
|
+
code: selection.hasFormat('code'),
|
|
23
|
+
link: $findMatchingParent(selection.anchor.getNode(), (n) => $isLinkNode(n)) !== null,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function activeFor(id, fmt) {
|
|
28
|
+
switch (id) {
|
|
29
|
+
case 'bold':
|
|
30
|
+
return fmt.bold;
|
|
31
|
+
case 'italic':
|
|
32
|
+
return fmt.italic;
|
|
33
|
+
case 'strikethrough':
|
|
34
|
+
return fmt.strikethrough;
|
|
35
|
+
case 'code':
|
|
36
|
+
return fmt.code;
|
|
37
|
+
case 'link':
|
|
38
|
+
return fmt.link;
|
|
39
|
+
default:
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function floatingToolbarPlugin() {
|
|
44
|
+
let floatingItems = [];
|
|
45
|
+
return {
|
|
46
|
+
name: 'floatingToolbar',
|
|
47
|
+
onItems: (items) => {
|
|
48
|
+
floatingItems = items.filter((i) => i.surfaces ? i.surfaces.includes('floating') : i.group === 'inline');
|
|
49
|
+
},
|
|
50
|
+
register: (editor, ctx) => {
|
|
51
|
+
const refresh = () => {
|
|
52
|
+
const collapsed = editor.getEditorState().read(() => {
|
|
53
|
+
const s = $getSelection();
|
|
54
|
+
return !$isRangeSelection(s) || s.isCollapsed();
|
|
55
|
+
});
|
|
56
|
+
const dom = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
57
|
+
if (collapsed || !dom || dom.rangeCount === 0) {
|
|
58
|
+
ctx.emit({ type: 'plugin', name: 'floatingToolbar', msg: { type: 'hide' } });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const rect = dom.getRangeAt(0).getBoundingClientRect();
|
|
62
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
63
|
+
ctx.emit({ type: 'plugin', name: 'floatingToolbar', msg: { type: 'hide' } });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const fmt = readFormat(editor);
|
|
67
|
+
const items = floatingItems.map((i) => ({
|
|
68
|
+
id: i.id,
|
|
69
|
+
label: i.label,
|
|
70
|
+
glyph: DEFAULT_GLYPHS[i.id] ?? i.label,
|
|
71
|
+
active: activeFor(i.id, fmt),
|
|
72
|
+
}));
|
|
73
|
+
ctx.emit({
|
|
74
|
+
type: 'plugin',
|
|
75
|
+
name: 'floatingToolbar',
|
|
76
|
+
msg: { type: 'show', x: rect.left + rect.width / 2, y: rect.top, items },
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
return mergeRegister(editor.registerUpdateListener(() => refresh()),
|
|
80
|
+
// Keep the bubble glued to the selection while the page scrolls.
|
|
81
|
+
onViewportChange(refresh));
|
|
82
|
+
},
|
|
83
|
+
ui: definePluginUI({
|
|
84
|
+
init: () => ({ open: false, x: 0, y: 0, items: [] }),
|
|
85
|
+
update: (state, msg) => {
|
|
86
|
+
switch (msg.type) {
|
|
87
|
+
case 'show':
|
|
88
|
+
return { open: msg.items.length > 0, x: msg.x, y: msg.y, items: msg.items };
|
|
89
|
+
case 'hide':
|
|
90
|
+
return hideOverlay(state);
|
|
91
|
+
case 'run': {
|
|
92
|
+
const item = state.items[msg.index];
|
|
93
|
+
return item ? [state, [{ type: 'run', id: item.id }]] : state;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
onEffect: (effect, ctx) => {
|
|
98
|
+
ctx.emit({ type: 'runCommand', id: effect.id });
|
|
99
|
+
},
|
|
100
|
+
// `x` is the selection's horizontal centre; the transform centres the bar on
|
|
101
|
+
// it and lifts it above the selection.
|
|
102
|
+
view: ({ state, send }) => overlayRoot({
|
|
103
|
+
open: state.at('open'),
|
|
104
|
+
x: state.at('x'),
|
|
105
|
+
y: state.at('y'),
|
|
106
|
+
zIndex: OVERLAY_Z.floatingToolbar,
|
|
107
|
+
transform: 'transform:translate(-50%,-115%)',
|
|
108
|
+
attrs: { 'data-scope': 'md-floating', 'data-part': 'bar' },
|
|
109
|
+
children: () => [
|
|
110
|
+
each(state.at('items'), {
|
|
111
|
+
key: (it) => it.id,
|
|
112
|
+
render: (item, index) => [
|
|
113
|
+
button({
|
|
114
|
+
type: 'button',
|
|
115
|
+
'data-scope': 'md-floating',
|
|
116
|
+
'data-part': 'item',
|
|
117
|
+
'data-active': item.map((it) => (it.active ? '' : undefined)),
|
|
118
|
+
'aria-label': item.map((it) => it.label),
|
|
119
|
+
onMouseDown: (e) => {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
send({ type: 'run', index: index.peek() });
|
|
122
|
+
},
|
|
123
|
+
}, [span({ 'data-part': 'glyph', 'aria-hidden': 'true' }, [renderGlyph(item)])]),
|
|
124
|
+
],
|
|
125
|
+
}),
|
|
126
|
+
],
|
|
127
|
+
}),
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Render an item's glyph (SVG markup → unsafeHtml, otherwise text). */
|
|
132
|
+
function renderGlyph(item) {
|
|
133
|
+
// The glyph value is stable per row; reading once is fine.
|
|
134
|
+
const glyph = item.peek().glyph;
|
|
135
|
+
return glyph.trimStart().startsWith('<svg') ? unsafeHtml(glyph) : text(item.map((it) => it.glyph));
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=floating-toolbar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floating-toolbar.js","sourceRoot":"","sources":["../../src/plugins/floating-toolbar.ts"],"names":[],"mappings":"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","sourcesContent":["// Floating selection toolbar — a bubble of inline-format actions that appears\n// above a non-collapsed text selection. A plugin-UI overlay: `register` watches\n// selection changes and positions/fills the bar; clicking a button runs the\n// command on the still-live selection.\n\nimport { $getSelection, $isRangeSelection } from 'lexical'\nimport { $findMatchingParent, mergeRegister } from '@lexical/utils'\nimport { $isLinkNode } from '@lexical/link'\nimport { button, each, span, text, unsafeHtml, type Signal } from '@llui/dom'\nimport { definePluginUI } from './ui.js'\nimport { OVERLAY_Z, hideOverlay, onViewportChange, overlayRoot } from './overlay.js'\nimport { DEFAULT_GLYPHS } from '../surfaces/toolbar.js'\nimport type { CommandItem, MarkdownPlugin } from './types.js'\n\ninterface BarItem {\n id: string\n label: string\n glyph: string\n active: boolean\n}\n\ninterface FloatState {\n open: boolean\n x: number\n y: number\n items: BarItem[]\n}\n\ntype FloatMsg =\n | { type: 'show'; x: number; y: number; items: BarItem[] }\n | { type: 'hide' }\n | { type: 'run'; index: number }\n\ntype FloatEffect = { type: 'run'; id: string }\n\ninterface InlineFormat {\n bold: boolean\n italic: boolean\n strikethrough: boolean\n code: boolean\n link: boolean\n}\n\nfunction readFormat(editor: import('lexical').LexicalEditor): InlineFormat {\n return editor.getEditorState().read(() => {\n const selection = $getSelection()\n if (!$isRangeSelection(selection)) {\n return { bold: false, italic: false, strikethrough: false, code: false, link: false }\n }\n return {\n bold: selection.hasFormat('bold'),\n italic: selection.hasFormat('italic'),\n strikethrough: selection.hasFormat('strikethrough'),\n code: selection.hasFormat('code'),\n link: $findMatchingParent(selection.anchor.getNode(), (n) => $isLinkNode(n)) !== null,\n }\n })\n}\n\nfunction activeFor(id: string, fmt: InlineFormat): boolean {\n switch (id) {\n case 'bold':\n return fmt.bold\n case 'italic':\n return fmt.italic\n case 'strikethrough':\n return fmt.strikethrough\n case 'code':\n return fmt.code\n case 'link':\n return fmt.link\n default:\n return false\n }\n}\n\nexport function floatingToolbarPlugin(): MarkdownPlugin {\n let floatingItems: CommandItem[] = []\n\n return {\n name: 'floatingToolbar',\n onItems: (items) => {\n floatingItems = items.filter((i) =>\n i.surfaces ? i.surfaces.includes('floating') : i.group === 'inline',\n )\n },\n register: (editor, ctx) => {\n const refresh = (): void => {\n const collapsed = editor.getEditorState().read(() => {\n const s = $getSelection()\n return !$isRangeSelection(s) || s.isCollapsed()\n })\n const dom = typeof window !== 'undefined' ? window.getSelection() : null\n if (collapsed || !dom || dom.rangeCount === 0) {\n ctx.emit({ type: 'plugin', name: 'floatingToolbar', msg: { type: 'hide' } })\n return\n }\n const rect = dom.getRangeAt(0).getBoundingClientRect()\n if (rect.width === 0 && rect.height === 0) {\n ctx.emit({ type: 'plugin', name: 'floatingToolbar', msg: { type: 'hide' } })\n return\n }\n const fmt = readFormat(editor)\n const items: BarItem[] = floatingItems.map((i) => ({\n id: i.id,\n label: i.label,\n glyph: DEFAULT_GLYPHS[i.id] ?? i.label,\n active: activeFor(i.id, fmt),\n }))\n ctx.emit({\n type: 'plugin',\n name: 'floatingToolbar',\n msg: { type: 'show', x: rect.left + rect.width / 2, y: rect.top, items },\n })\n }\n return mergeRegister(\n editor.registerUpdateListener(() => refresh()),\n // Keep the bubble glued to the selection while the page scrolls.\n onViewportChange(refresh),\n )\n },\n ui: definePluginUI<FloatState, FloatMsg, FloatEffect>({\n init: () => ({ open: false, x: 0, y: 0, items: [] }),\n update: (state, msg) => {\n switch (msg.type) {\n case 'show':\n return { open: msg.items.length > 0, x: msg.x, y: msg.y, items: msg.items }\n case 'hide':\n return hideOverlay(state)\n case 'run': {\n const item = state.items[msg.index]\n return item ? [state, [{ type: 'run', id: item.id }]] : state\n }\n }\n },\n onEffect: (effect, ctx) => {\n ctx.emit({ type: 'runCommand', id: effect.id })\n },\n // `x` is the selection's horizontal centre; the transform centres the bar on\n // it and lifts it above the selection.\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.floatingToolbar,\n transform: 'transform:translate(-50%,-115%)',\n attrs: { 'data-scope': 'md-floating', 'data-part': 'bar' },\n children: () => [\n each(state.at('items') as Signal<BarItem[]>, {\n key: (it) => it.id,\n render: (item, index) => [\n button(\n {\n type: 'button',\n 'data-scope': 'md-floating',\n 'data-part': 'item',\n 'data-active': item.map((it) => (it.active ? '' : undefined)),\n 'aria-label': item.map((it) => it.label),\n onMouseDown: (e: MouseEvent) => {\n e.preventDefault()\n send({ type: 'run', index: index.peek() })\n },\n },\n [span({ 'data-part': 'glyph', 'aria-hidden': 'true' }, [renderGlyph(item)])],\n ),\n ],\n }),\n ],\n }),\n }),\n }\n}\n\n/** Render an item's glyph (SVG markup → unsafeHtml, otherwise text). */\nfunction renderGlyph(item: Signal<BarItem>): import('@llui/dom').Mountable {\n // The glyph value is stable per row; reading once is fine.\n const glyph = item.peek().glyph\n return glyph.trimStart().startsWith('<svg') ? unsafeHtml(glyph) : text(item.map((it) => it.glyph))\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hr.d.ts","sourceRoot":"","sources":["../../src/plugins/hr.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AA6BhD,yDAAyD;AACzD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,wBAAgB,QAAQ,IAAI,cAAc,CAkBzC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Horizontal-rule plugin — a thematic break rendered as an `<hr>` via the
|
|
2
|
+
// decorator bridge, round-tripping to `---` markdown.
|
|
3
|
+
import {} from 'lexical';
|
|
4
|
+
import { $insertNodeToNearestRoot } from '@lexical/utils';
|
|
5
|
+
import { $createLLuiDecoratorNode, $isLLuiDecoratorNode, LLuiDecoratorNode, decoratorBridge, } from '@llui/lexical';
|
|
6
|
+
import { component, hr } from '@llui/dom';
|
|
7
|
+
const BRIDGE_TYPE = 'hr';
|
|
8
|
+
const hrBridge = decoratorBridge(BRIDGE_TYPE, () => component({
|
|
9
|
+
name: 'HorizontalRule',
|
|
10
|
+
init: () => ({}),
|
|
11
|
+
update: (state) => state,
|
|
12
|
+
view: () => [hr({ 'data-md-hr': '', contenteditable: 'false' })],
|
|
13
|
+
}));
|
|
14
|
+
const HR_TRANSFORMER = {
|
|
15
|
+
dependencies: [LLuiDecoratorNode],
|
|
16
|
+
export: (node) => $isLLuiDecoratorNode(node) && node.getBridgeType() === BRIDGE_TYPE ? '---' : null,
|
|
17
|
+
regExp: /^(---|\*\*\*|___)\s*$/,
|
|
18
|
+
replace: (parentNode) => {
|
|
19
|
+
parentNode.replace($createLLuiDecoratorNode(BRIDGE_TYPE, {}));
|
|
20
|
+
},
|
|
21
|
+
type: 'element',
|
|
22
|
+
};
|
|
23
|
+
/** Insert a horizontal rule at the current selection. */
|
|
24
|
+
export function $insertHorizontalRule() {
|
|
25
|
+
$insertNodeToNearestRoot($createLLuiDecoratorNode(BRIDGE_TYPE, {}));
|
|
26
|
+
}
|
|
27
|
+
export function hrPlugin() {
|
|
28
|
+
return {
|
|
29
|
+
name: 'hr',
|
|
30
|
+
nodes: [LLuiDecoratorNode],
|
|
31
|
+
decorators: [hrBridge],
|
|
32
|
+
transformers: [HR_TRANSFORMER],
|
|
33
|
+
items: [
|
|
34
|
+
{
|
|
35
|
+
id: 'horizontalRule',
|
|
36
|
+
label: 'Divider',
|
|
37
|
+
icon: 'hr',
|
|
38
|
+
group: 'insert',
|
|
39
|
+
keywords: ['rule', 'divider', 'separator', 'hr'],
|
|
40
|
+
run: (editor) => editor.update(() => $insertHorizontalRule()),
|
|
41
|
+
surfaces: ['toolbar', 'slash', 'context'],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=hr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hr.js","sourceRoot":"","sources":["../../src/plugins/hr.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,sDAAsD;AAEtD,OAAO,EAAsC,MAAM,SAAS,CAAA;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAEzD,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,GAChB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAGzC,MAAM,WAAW,GAAG,IAAI,CAAA;AAExB,MAAM,QAAQ,GAAG,eAAe,CAK9B,WAAW,EAAE,GAAG,EAAE,CAClB,SAAS,CAAiD;IACxD,IAAI,EAAE,gBAAgB;IACtB,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC;IAChB,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;IACxB,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,CAAC;CACjE,CAAC,CACH,CAAA;AAED,MAAM,cAAc,GAAuB;IACzC,YAAY,EAAE,CAAC,iBAAiB,CAAC;IACjC,MAAM,EAAE,CAAC,IAAiB,EAAiB,EAAE,CAC3C,oBAAoB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,aAAa,EAAE,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;IACnF,MAAM,EAAE,uBAAuB;IAC/B,OAAO,EAAE,CAAC,UAAuB,EAAQ,EAAE;QACzC,UAAU,CAAC,OAAO,CAAC,wBAAwB,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAA;IAC/D,CAAC;IACD,IAAI,EAAE,SAAS;CAChB,CAAA;AAED,yDAAyD;AACzD,MAAM,UAAU,qBAAqB;IACnC,wBAAwB,CAAC,wBAAwB,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAA;AACrE,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,OAAO;QACL,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,CAAC,iBAAiB,CAAC;QAC1B,UAAU,EAAE,CAAC,QAAQ,CAAC;QACtB,YAAY,EAAE,CAAC,cAAc,CAAC;QAC9B,KAAK,EAAE;YACL;gBACE,EAAE,EAAE,gBAAgB;gBACpB,KAAK,EAAE,SAAS;gBAChB,IAAI,EAAE,IAAI;gBACV,KAAK,EAAE,QAAQ;gBACf,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,CAAC;gBAChD,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,qBAAqB,EAAE,CAAC;gBAC7D,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC;aAC1C;SACF;KACF,CAAA;AACH,CAAC","sourcesContent":["// Horizontal-rule plugin — a thematic break rendered as an `<hr>` via the\n// decorator bridge, round-tripping to `---` markdown.\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, hr } from '@llui/dom'\nimport type { MarkdownPlugin } from './types.js'\n\nconst BRIDGE_TYPE = 'hr'\n\nconst hrBridge = decoratorBridge<\n Record<string, never>,\n Record<string, never>,\n { type: 'noop' },\n never\n>(BRIDGE_TYPE, () =>\n component<Record<string, never>, { type: 'noop' }, never>({\n name: 'HorizontalRule',\n init: () => ({}),\n update: (state) => state,\n view: () => [hr({ 'data-md-hr': '', contenteditable: 'false' })],\n }),\n)\n\nconst HR_TRANSFORMER: ElementTransformer = {\n dependencies: [LLuiDecoratorNode],\n export: (node: LexicalNode): string | null =>\n $isLLuiDecoratorNode(node) && node.getBridgeType() === BRIDGE_TYPE ? '---' : null,\n regExp: /^(---|\\*\\*\\*|___)\\s*$/,\n replace: (parentNode: ElementNode): void => {\n parentNode.replace($createLLuiDecoratorNode(BRIDGE_TYPE, {}))\n },\n type: 'element',\n}\n\n/** Insert a horizontal rule at the current selection. */\nexport function $insertHorizontalRule(): void {\n $insertNodeToNearestRoot($createLLuiDecoratorNode(BRIDGE_TYPE, {}))\n}\n\nexport function hrPlugin(): MarkdownPlugin {\n return {\n name: 'hr',\n nodes: [LLuiDecoratorNode],\n decorators: [hrBridge],\n transformers: [HR_TRANSFORMER],\n items: [\n {\n id: 'horizontalRule',\n label: 'Divider',\n icon: 'hr',\n group: 'insert',\n keywords: ['rule', 'divider', 'separator', 'hr'],\n run: (editor) => editor.update(() => $insertHorizontalRule()),\n surfaces: ['toolbar', 'slash', 'context'],\n },\n ],\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MarkdownPlugin } from './types.js';
|
|
2
|
+
export interface ImagePluginOptions {
|
|
3
|
+
/** Upload a chosen file and resolve to its URL. When omitted, the file picker
|
|
4
|
+
* is hidden and only URL entry is offered. */
|
|
5
|
+
upload?: (file: File) => Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function imagePlugin(opts?: ImagePluginOptions): MarkdownPlugin;
|
|
8
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/plugins/image.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AA6EhD,MAAM,WAAW,kBAAkB;IACjC;kDAC8C;IAC9C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CACzC;AAED,wBAAgB,WAAW,CAAC,IAAI,GAAE,kBAAuB,GAAG,cAAc,CA2HzE"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Image plugin — a block image rendered via the decorator bridge, round-tripping
|
|
2
|
+
// to `` markdown, inserted through a plugin-UI dialog (URL + alt, with
|
|
3
|
+
// optional file upload). Exercises decorator rendering + a transformer + the
|
|
4
|
+
// plugin-UI extension all at once.
|
|
5
|
+
import { $getSelection, $setSelection, } from 'lexical';
|
|
6
|
+
import { $insertNodeToNearestRoot } from '@lexical/utils';
|
|
7
|
+
import { $createLLuiDecoratorNode, $isLLuiDecoratorNode, LLuiDecoratorNode, decoratorBridge, } from '@llui/lexical';
|
|
8
|
+
import { button, component, div, img, input, text } from '@llui/dom';
|
|
9
|
+
import { connect as connectDialog, overlay as overlayDialog, } from '@llui/components/dialog';
|
|
10
|
+
import { definePluginUI } from './ui.js';
|
|
11
|
+
const BRIDGE_TYPE = 'image';
|
|
12
|
+
function isImageData(value) {
|
|
13
|
+
return (typeof value === 'object' &&
|
|
14
|
+
value !== null &&
|
|
15
|
+
typeof value.src === 'string' &&
|
|
16
|
+
typeof value.alt === 'string');
|
|
17
|
+
}
|
|
18
|
+
const imageBridge = decoratorBridge(BRIDGE_TYPE, (data) => component({
|
|
19
|
+
name: 'Image',
|
|
20
|
+
init: () => ({ src: data.src, alt: data.alt }),
|
|
21
|
+
update: (state) => state,
|
|
22
|
+
view: ({ state }) => [
|
|
23
|
+
div({ 'data-scope': 'md-image', 'data-part': 'root', contenteditable: 'false' }, [
|
|
24
|
+
img({ src: state.at('src'), alt: state.at('alt') }),
|
|
25
|
+
]),
|
|
26
|
+
],
|
|
27
|
+
}));
|
|
28
|
+
const IMAGE_TRANSFORMER = {
|
|
29
|
+
dependencies: [LLuiDecoratorNode],
|
|
30
|
+
export: (node) => {
|
|
31
|
+
if (!$isLLuiDecoratorNode(node) || node.getBridgeType() !== BRIDGE_TYPE)
|
|
32
|
+
return null;
|
|
33
|
+
const data = node.getData();
|
|
34
|
+
return isImageData(data) ? `` : null;
|
|
35
|
+
},
|
|
36
|
+
regExp: /^!\[([^\]]*)\]\(([^)]+)\)$/,
|
|
37
|
+
replace: (parentNode, _children, match) => {
|
|
38
|
+
parentNode.replace($createLLuiDecoratorNode(BRIDGE_TYPE, { alt: match[1] ?? '', src: match[2] ?? '' }));
|
|
39
|
+
},
|
|
40
|
+
type: 'element',
|
|
41
|
+
};
|
|
42
|
+
function dialogOpen(msg, current) {
|
|
43
|
+
switch (msg.type) {
|
|
44
|
+
case 'open':
|
|
45
|
+
return true;
|
|
46
|
+
case 'close':
|
|
47
|
+
return false;
|
|
48
|
+
case 'toggle':
|
|
49
|
+
return !current;
|
|
50
|
+
case 'setOpen':
|
|
51
|
+
return msg.open;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function imagePlugin(opts = {}) {
|
|
55
|
+
let savedSelection = null;
|
|
56
|
+
return {
|
|
57
|
+
name: 'image',
|
|
58
|
+
nodes: [LLuiDecoratorNode],
|
|
59
|
+
decorators: [imageBridge],
|
|
60
|
+
transformers: [IMAGE_TRANSFORMER],
|
|
61
|
+
items: [
|
|
62
|
+
{
|
|
63
|
+
id: 'image',
|
|
64
|
+
label: 'Image',
|
|
65
|
+
icon: 'image',
|
|
66
|
+
group: 'insert',
|
|
67
|
+
keywords: ['img', 'picture', 'photo'],
|
|
68
|
+
run: (_editor, ctx) => ctx.send({ type: 'plugin', name: 'image', msg: { type: 'open' } }),
|
|
69
|
+
surfaces: ['toolbar', 'slash', 'context'],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
ui: definePluginUI({
|
|
73
|
+
init: () => ({ dialog: { open: false }, src: '', alt: '' }),
|
|
74
|
+
update: (state, msg) => {
|
|
75
|
+
switch (msg.type) {
|
|
76
|
+
case 'open':
|
|
77
|
+
return [{ dialog: { open: true }, src: '', alt: '' }, [{ type: 'begin' }]];
|
|
78
|
+
case 'setSrc':
|
|
79
|
+
return { ...state, src: msg.src };
|
|
80
|
+
case 'setAlt':
|
|
81
|
+
return { ...state, alt: msg.alt };
|
|
82
|
+
case 'submit':
|
|
83
|
+
return [
|
|
84
|
+
{ ...state, dialog: { open: false } },
|
|
85
|
+
[{ type: 'insert', src: state.src, alt: state.alt }],
|
|
86
|
+
];
|
|
87
|
+
case 'dialog': {
|
|
88
|
+
const open = dialogOpen(msg.msg, state.dialog.open);
|
|
89
|
+
return open === state.dialog.open ? state : { ...state, dialog: { open } };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
view: ({ state, send }) => {
|
|
94
|
+
const dialogSend = (msg) => send({ type: 'dialog', msg });
|
|
95
|
+
const parts = connectDialog(state.at('dialog'), dialogSend, {
|
|
96
|
+
id: 'md-image-dialog',
|
|
97
|
+
closeLabel: 'Cancel',
|
|
98
|
+
});
|
|
99
|
+
return [
|
|
100
|
+
overlayDialog({
|
|
101
|
+
state: state.at('dialog'),
|
|
102
|
+
send: dialogSend,
|
|
103
|
+
parts,
|
|
104
|
+
content: () => [
|
|
105
|
+
div({ ...parts.content, 'data-md-link': 'box' }, [
|
|
106
|
+
div({ ...parts.title, 'data-md-link': 'title' }, [text('Insert image')]),
|
|
107
|
+
input({
|
|
108
|
+
'data-md-link': 'input',
|
|
109
|
+
type: 'url',
|
|
110
|
+
placeholder: 'https://example.com/image.png',
|
|
111
|
+
value: state.at('src'),
|
|
112
|
+
onInput: (e) => send({ type: 'setSrc', src: e.target.value }),
|
|
113
|
+
}),
|
|
114
|
+
input({
|
|
115
|
+
'data-md-link': 'input',
|
|
116
|
+
'data-md-image': 'alt',
|
|
117
|
+
type: 'text',
|
|
118
|
+
placeholder: 'Alt text (description)',
|
|
119
|
+
value: state.at('alt'),
|
|
120
|
+
onInput: (e) => send({ type: 'setAlt', alt: e.target.value }),
|
|
121
|
+
}),
|
|
122
|
+
...(opts.upload
|
|
123
|
+
? [
|
|
124
|
+
input({
|
|
125
|
+
'data-md-image': 'file',
|
|
126
|
+
type: 'file',
|
|
127
|
+
accept: 'image/*',
|
|
128
|
+
onChange: (e) => {
|
|
129
|
+
const file = e.target.files?.[0];
|
|
130
|
+
if (file && opts.upload) {
|
|
131
|
+
void opts.upload(file).then((src) => send({ type: 'setSrc', src }));
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
]
|
|
136
|
+
: []),
|
|
137
|
+
div({ 'data-md-link': 'actions' }, [
|
|
138
|
+
button({ ...parts.closeTrigger, 'data-md-link': 'cancel' }, [text('Cancel')]),
|
|
139
|
+
button({
|
|
140
|
+
type: 'button',
|
|
141
|
+
'data-md-link': 'apply',
|
|
142
|
+
onClick: () => send({ type: 'submit' }),
|
|
143
|
+
}, [text('Insert')]),
|
|
144
|
+
]),
|
|
145
|
+
]),
|
|
146
|
+
],
|
|
147
|
+
}),
|
|
148
|
+
];
|
|
149
|
+
},
|
|
150
|
+
onEffect: (effect, ctx) => {
|
|
151
|
+
const editor = ctx.editor();
|
|
152
|
+
if (!editor)
|
|
153
|
+
return;
|
|
154
|
+
if (effect.type === 'begin') {
|
|
155
|
+
savedSelection = editor.getEditorState().read(() => {
|
|
156
|
+
const selection = $getSelection();
|
|
157
|
+
return selection ? selection.clone() : null;
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (effect.src.trim() === '')
|
|
162
|
+
return;
|
|
163
|
+
editor.update(() => {
|
|
164
|
+
if (savedSelection)
|
|
165
|
+
$setSelection(savedSelection.clone());
|
|
166
|
+
$insertNodeToNearestRoot($createLLuiDecoratorNode(BRIDGE_TYPE, { src: effect.src.trim(), alt: effect.alt }));
|
|
167
|
+
});
|
|
168
|
+
savedSelection = null;
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/plugins/image.ts"],"names":[],"mappings":"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","sourcesContent":["// Image plugin — a block image rendered via the decorator bridge, round-tripping\n// to `` markdown, inserted through a plugin-UI dialog (URL + alt, with\n// optional file upload). Exercises decorator rendering + a transformer + the\n// plugin-UI extension all at once.\n\nimport {\n $getSelection,\n $setSelection,\n type BaseSelection,\n type ElementNode,\n type LexicalNode,\n} 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 { button, component, div, img, input, text, type Signal } from '@llui/dom'\nimport {\n connect as connectDialog,\n overlay as overlayDialog,\n type DialogMsg,\n} from '@llui/components/dialog'\nimport { definePluginUI } from './ui.js'\nimport type { MarkdownPlugin } from './types.js'\n\nconst BRIDGE_TYPE = 'image'\n\ninterface ImageData {\n src: string\n alt: string\n}\n\nfunction isImageData(value: unknown): value is ImageData {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as ImageData).src === 'string' &&\n typeof (value as ImageData).alt === 'string'\n )\n}\n\nconst imageBridge = decoratorBridge<ImageData, ImageData, { type: 'noop' }, never>(\n BRIDGE_TYPE,\n (data) =>\n component<ImageData, { type: 'noop' }, never>({\n name: 'Image',\n init: () => ({ src: data.src, alt: data.alt }),\n update: (state) => state,\n view: ({ state }) => [\n div({ 'data-scope': 'md-image', 'data-part': 'root', contenteditable: 'false' }, [\n img({ src: state.at('src') as Signal<string>, alt: state.at('alt') as Signal<string> }),\n ]),\n ],\n }),\n)\n\nconst IMAGE_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 isImageData(data) ? `` : null\n },\n regExp: /^!\\[([^\\]]*)\\]\\(([^)]+)\\)$/,\n replace: (parentNode: ElementNode, _children, match): void => {\n parentNode.replace(\n $createLLuiDecoratorNode(BRIDGE_TYPE, { alt: match[1] ?? '', src: match[2] ?? '' }),\n )\n },\n type: 'element',\n}\n\ninterface ImageState {\n dialog: { open: boolean }\n src: string\n alt: string\n}\n\ntype ImageMsg =\n | { type: 'open' }\n | { type: 'setSrc'; src: string }\n | { type: 'setAlt'; alt: string }\n | { type: 'submit' }\n | { type: 'dialog'; msg: DialogMsg }\n\ntype ImageEffect = { type: 'begin' } | { type: 'insert'; src: string; alt: string }\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 ImagePluginOptions {\n /** Upload a chosen file and resolve to its URL. When omitted, the file picker\n * is hidden and only URL entry is offered. */\n upload?: (file: File) => Promise<string>\n}\n\nexport function imagePlugin(opts: ImagePluginOptions = {}): MarkdownPlugin {\n let savedSelection: BaseSelection | null = null\n\n return {\n name: 'image',\n nodes: [LLuiDecoratorNode],\n decorators: [imageBridge],\n transformers: [IMAGE_TRANSFORMER],\n items: [\n {\n id: 'image',\n label: 'Image',\n icon: 'image',\n group: 'insert',\n keywords: ['img', 'picture', 'photo'],\n run: (_editor, ctx) => ctx.send({ type: 'plugin', name: 'image', msg: { type: 'open' } }),\n surfaces: ['toolbar', 'slash', 'context'],\n },\n ],\n ui: definePluginUI<ImageState, ImageMsg, ImageEffect>({\n init: () => ({ dialog: { open: false }, src: '', alt: '' }),\n update: (state, msg) => {\n switch (msg.type) {\n case 'open':\n return [{ dialog: { open: true }, src: '', alt: '' }, [{ type: 'begin' }]]\n case 'setSrc':\n return { ...state, src: msg.src }\n case 'setAlt':\n return { ...state, alt: msg.alt }\n case 'submit':\n return [\n { ...state, dialog: { open: false } },\n [{ type: 'insert', src: state.src, alt: state.alt }],\n ]\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 const dialogSend = (msg: DialogMsg): void => send({ type: 'dialog', msg })\n const parts = connectDialog(state.at('dialog'), dialogSend, {\n id: 'md-image-dialog',\n closeLabel: 'Cancel',\n })\n return [\n overlayDialog({\n state: state.at('dialog'),\n send: dialogSend,\n parts,\n content: () => [\n div({ ...parts.content, 'data-md-link': 'box' }, [\n div({ ...parts.title, 'data-md-link': 'title' }, [text('Insert image')]),\n input({\n 'data-md-link': 'input',\n type: 'url',\n placeholder: 'https://example.com/image.png',\n value: state.at('src') as Signal<string>,\n onInput: (e: Event) =>\n send({ type: 'setSrc', src: (e.target as HTMLInputElement).value }),\n }),\n input({\n 'data-md-link': 'input',\n 'data-md-image': 'alt',\n type: 'text',\n placeholder: 'Alt text (description)',\n value: state.at('alt') as Signal<string>,\n onInput: (e: Event) =>\n send({ type: 'setAlt', alt: (e.target as HTMLInputElement).value }),\n }),\n ...(opts.upload\n ? [\n input({\n 'data-md-image': 'file',\n type: 'file',\n accept: 'image/*',\n onChange: (e: Event) => {\n const file = (e.target as HTMLInputElement).files?.[0]\n if (file && opts.upload) {\n void opts.upload(file).then((src) => send({ type: 'setSrc', src }))\n }\n },\n }),\n ]\n : []),\n div({ 'data-md-link': 'actions' }, [\n button({ ...parts.closeTrigger, 'data-md-link': 'cancel' }, [text('Cancel')]),\n button(\n {\n type: 'button',\n 'data-md-link': 'apply',\n onClick: () => send({ type: 'submit' }),\n },\n [text('Insert')],\n ),\n ]),\n ]),\n ],\n }),\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 return\n }\n if (effect.src.trim() === '') return\n editor.update(() => {\n if (savedSelection) $setSelection(savedSelection.clone())\n $insertNodeToNearestRoot(\n $createLLuiDecoratorNode(BRIDGE_TYPE, { src: effect.src.trim(), alt: effect.alt }),\n )\n })\n savedSelection = null\n },\n }),\n }\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MarkdownPlugin } from './types.js';
|
|
2
|
+
export interface LinkPluginOptions {
|
|
3
|
+
/** Default URL pre-filled when there's no existing link (default ''). */
|
|
4
|
+
defaultUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function linkPlugin(opts?: LinkPluginOptions): MarkdownPlugin;
|
|
7
|
+
//# sourceMappingURL=link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/plugins/link.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,YAAY,CAAA;AAyC7D,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,UAAU,CAAC,IAAI,GAAE,iBAAsB,GAAG,cAAc,CAkEvE"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// The link plugin — a stateful, UI-bearing feature built entirely as a plugin
|
|
2
|
+
// (no core editor changes). It owns its dialog state, its view (the modal), and
|
|
3
|
+
// its effects (save/restore selection + toggle the link). This is the proof that
|
|
4
|
+
// the plugin-UI extension makes such features pluggable rather than built-in.
|
|
5
|
+
import { $getSelection, $isRangeSelection, $setSelection, } from 'lexical';
|
|
6
|
+
import { $findMatchingParent } from '@lexical/utils';
|
|
7
|
+
import { $isLinkNode, $toggleLink } from '@lexical/link';
|
|
8
|
+
import { linkDialog } from '../surfaces/link-dialog.js';
|
|
9
|
+
import { definePluginUI } from './ui.js';
|
|
10
|
+
const PLUGIN = 'link';
|
|
11
|
+
/** Read the URL of the link wrapping the current selection (empty if none). */
|
|
12
|
+
function readLinkUrl(editor) {
|
|
13
|
+
return editor.getEditorState().read(() => {
|
|
14
|
+
const selection = $getSelection();
|
|
15
|
+
if (!$isRangeSelection(selection))
|
|
16
|
+
return '';
|
|
17
|
+
const link = $findMatchingParent(selection.anchor.getNode(), (node) => $isLinkNode(node));
|
|
18
|
+
return $isLinkNode(link) ? link.getURL() : '';
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function dialogOpen(msg, current) {
|
|
22
|
+
switch (msg.type) {
|
|
23
|
+
case 'open':
|
|
24
|
+
return true;
|
|
25
|
+
case 'close':
|
|
26
|
+
return false;
|
|
27
|
+
case 'toggle':
|
|
28
|
+
return !current;
|
|
29
|
+
case 'setOpen':
|
|
30
|
+
return msg.open;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function linkPlugin(opts = {}) {
|
|
34
|
+
// Selection saved when the dialog opens (the modal steals focus/selection),
|
|
35
|
+
// restored on commit. Encapsulated in the plugin instance — not the core editor.
|
|
36
|
+
let savedSelection = null;
|
|
37
|
+
const item = {
|
|
38
|
+
id: 'link',
|
|
39
|
+
label: 'Link',
|
|
40
|
+
icon: 'link',
|
|
41
|
+
group: 'inline',
|
|
42
|
+
keywords: ['url', 'href'],
|
|
43
|
+
run: (_editor, ctx) => ctx.send({ type: 'plugin', name: PLUGIN, msg: { type: 'open' } }),
|
|
44
|
+
isActive: (f) => f.link,
|
|
45
|
+
surfaces: ['toolbar', 'floating', 'context'],
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
name: PLUGIN,
|
|
49
|
+
items: [item],
|
|
50
|
+
ui: definePluginUI({
|
|
51
|
+
init: () => ({ dialog: { open: false }, url: opts.defaultUrl ?? '' }),
|
|
52
|
+
update: (state, msg) => {
|
|
53
|
+
switch (msg.type) {
|
|
54
|
+
case 'open':
|
|
55
|
+
return [state, [{ type: 'begin' }]];
|
|
56
|
+
case 'show':
|
|
57
|
+
return { dialog: { open: true }, url: msg.url };
|
|
58
|
+
case 'setUrl':
|
|
59
|
+
return { ...state, url: msg.url };
|
|
60
|
+
case 'submit':
|
|
61
|
+
return [{ ...state, dialog: { open: false } }, [{ type: 'commit', url: state.url }]];
|
|
62
|
+
case 'dialog': {
|
|
63
|
+
const open = dialogOpen(msg.msg, state.dialog.open);
|
|
64
|
+
return open === state.dialog.open ? state : { ...state, dialog: { open } };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
view: ({ state, send }) => [
|
|
69
|
+
linkDialog({
|
|
70
|
+
dialog: state.at('dialog'),
|
|
71
|
+
url: state.at('url'),
|
|
72
|
+
onInput: (url) => send({ type: 'setUrl', url }),
|
|
73
|
+
onSubmit: () => send({ type: 'submit' }),
|
|
74
|
+
onDialog: (msg) => send({ type: 'dialog', msg }),
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
|
+
onEffect: (effect, ctx) => {
|
|
78
|
+
const editor = ctx.editor();
|
|
79
|
+
if (!editor)
|
|
80
|
+
return;
|
|
81
|
+
if (effect.type === 'begin') {
|
|
82
|
+
savedSelection = editor.getEditorState().read(() => {
|
|
83
|
+
const selection = $getSelection();
|
|
84
|
+
return selection ? selection.clone() : null;
|
|
85
|
+
});
|
|
86
|
+
ctx.send({ type: 'show', url: readLinkUrl(editor) });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const url = effect.url.trim();
|
|
90
|
+
editor.update(() => {
|
|
91
|
+
if (savedSelection)
|
|
92
|
+
$setSelection(savedSelection.clone());
|
|
93
|
+
$toggleLink(url === '' ? null : url);
|
|
94
|
+
});
|
|
95
|
+
savedSelection = null;
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=link.js.map
|