@llui/lexical 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 +78 -0
- package/dist/decorator.d.ts +40 -0
- package/dist/decorator.d.ts.map +1 -0
- package/dist/decorator.js +147 -0
- package/dist/decorator.js.map +1 -0
- package/dist/foreign.d.ts +48 -0
- package/dist/foreign.d.ts.map +1 -0
- package/dist/foreign.js +156 -0
- package/dist/foreign.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +64 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +29 -0
- package/dist/plugin.js.map +1 -0
- package/dist/register.d.ts +23 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +80 -0
- package/dist/register.js.map +1 -0
- package/dist/selection.d.ts +25 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/selection.js +73 -0
- package/dist/selection.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Franco Ponticelli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerVersion": "0.3.0",
|
|
3
|
+
"components": {},
|
|
4
|
+
"helpers": {
|
|
5
|
+
"foreign#lexicalForeign": {
|
|
6
|
+
"helperLocalPaths": [],
|
|
7
|
+
"kind": "view-helper",
|
|
8
|
+
"viaParams": [
|
|
9
|
+
{
|
|
10
|
+
"index": 0,
|
|
11
|
+
"reads": [
|
|
12
|
+
"changeDebounceMs",
|
|
13
|
+
"defaultValue",
|
|
14
|
+
"namespace",
|
|
15
|
+
"nodes",
|
|
16
|
+
"onError",
|
|
17
|
+
"plugins",
|
|
18
|
+
"readOnly",
|
|
19
|
+
"theme",
|
|
20
|
+
"value"
|
|
21
|
+
],
|
|
22
|
+
"shape": "state-value"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"register#matchesCombo": {
|
|
27
|
+
"helperLocalPaths": [],
|
|
28
|
+
"kind": "view-helper",
|
|
29
|
+
"viaParams": [
|
|
30
|
+
{
|
|
31
|
+
"index": 0,
|
|
32
|
+
"reads": [
|
|
33
|
+
"altKey",
|
|
34
|
+
"ctrlKey",
|
|
35
|
+
"key",
|
|
36
|
+
"key.length",
|
|
37
|
+
"metaKey",
|
|
38
|
+
"shiftKey"
|
|
39
|
+
],
|
|
40
|
+
"shape": "state-value"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"index": 1,
|
|
44
|
+
"reads": [
|
|
45
|
+
"alt",
|
|
46
|
+
"ctrl",
|
|
47
|
+
"key",
|
|
48
|
+
"mod",
|
|
49
|
+
"shift"
|
|
50
|
+
],
|
|
51
|
+
"shape": "state-value"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"index": 2,
|
|
55
|
+
"shape": "opaque"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"register#registerShortcuts": {
|
|
60
|
+
"helperLocalPaths": [],
|
|
61
|
+
"kind": "view-helper",
|
|
62
|
+
"viaParams": [
|
|
63
|
+
{
|
|
64
|
+
"index": 0,
|
|
65
|
+
"shape": "opaque"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"index": 1,
|
|
69
|
+
"reads": [
|
|
70
|
+
"length"
|
|
71
|
+
],
|
|
72
|
+
"shape": "state-value"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"version": 2
|
|
78
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DecoratorNode, type DOMConversionMap, type EditorConfig, type LexicalEditor, type LexicalNode, type NodeKey, type SerializedLexicalNode, type Spread } from 'lexical';
|
|
2
|
+
import type { DecoratorBridge } from './plugin.js';
|
|
3
|
+
export type SerializedLLuiDecoratorNode = Spread<{
|
|
4
|
+
bridgeType: string;
|
|
5
|
+
data: unknown;
|
|
6
|
+
}, SerializedLexicalNode>;
|
|
7
|
+
/** A generic decorator node that mounts an LLui sub-view via a registered
|
|
8
|
+
* {@link DecoratorBridge}. */
|
|
9
|
+
export declare class LLuiDecoratorNode extends DecoratorNode<HTMLElement> {
|
|
10
|
+
__bridgeType: string;
|
|
11
|
+
__data: unknown;
|
|
12
|
+
static getType(): string;
|
|
13
|
+
static clone(node: LLuiDecoratorNode): LLuiDecoratorNode;
|
|
14
|
+
constructor(bridgeType: string, data: unknown, key?: NodeKey);
|
|
15
|
+
createDOM(_config: EditorConfig): HTMLElement;
|
|
16
|
+
updateDOM(): false;
|
|
17
|
+
/** LLui decorators are block-level (callouts, math/mermaid blocks, embeds). */
|
|
18
|
+
isInline(): false;
|
|
19
|
+
static importDOM(): DOMConversionMap | null;
|
|
20
|
+
/** The bridge type id this node renders (used by markdown transformers). */
|
|
21
|
+
getBridgeType(): string;
|
|
22
|
+
/** Read the latest serialized data (for the sub-view). */
|
|
23
|
+
getData(): unknown;
|
|
24
|
+
/** Persist new data into the node (markdown-serializable). */
|
|
25
|
+
setData(data: unknown): void;
|
|
26
|
+
decorate(editor: LexicalEditor): HTMLElement;
|
|
27
|
+
exportJSON(): SerializedLLuiDecoratorNode;
|
|
28
|
+
static importJSON(json: SerializedLLuiDecoratorNode): LLuiDecoratorNode;
|
|
29
|
+
}
|
|
30
|
+
/** Create a decorator node for `bridgeType` carrying `data`. */
|
|
31
|
+
export declare function $createLLuiDecoratorNode(bridgeType: string, data: unknown): LLuiDecoratorNode;
|
|
32
|
+
export declare function $isLLuiDecoratorNode(node: LexicalNode | null | undefined): node is LLuiDecoratorNode;
|
|
33
|
+
/**
|
|
34
|
+
* Wire decorator bridges onto an editor: register the bridge registry, place
|
|
35
|
+
* each decoration element into its node's DOM, and dispose sub-apps when their
|
|
36
|
+
* nodes are destroyed. Returns a disposer that tears down all live sub-apps.
|
|
37
|
+
* Typically called from a plugin's `register`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function registerDecoratorBridges(editor: LexicalEditor, bridges: readonly DecoratorBridge[]): () => void;
|
|
40
|
+
//# sourceMappingURL=decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorator.d.ts","sourceRoot":"","sources":["../src/decorator.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,aAAa,EACb,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,MAAM,EACZ,MAAM,SAAS,CAAA;AAChB,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,MAAM,2BAA2B,GAAG,MAAM,CAC9C;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,EACrC,qBAAqB,CACtB,CAAA;AAcD;8BAC8B;AAC9B,qBAAa,iBAAkB,SAAQ,aAAa,CAAC,WAAW,CAAC;IAC/D,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,OAAO,CAAA;WAEC,OAAO,IAAI,MAAM;WAIjB,KAAK,CAAC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB;gBAIrD,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO;IAMnD,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,WAAW;IAM7C,SAAS,IAAI,KAAK;IAI3B,+EAA+E;IACtE,QAAQ,IAAI,KAAK;WAIV,SAAS,IAAI,gBAAgB,GAAG,IAAI;IAIpD,4EAA4E;IAC5E,aAAa,IAAI,MAAM;IAIvB,0DAA0D;IAC1D,OAAO,IAAI,OAAO;IAIlB,8DAA8D;IAC9D,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAInB,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW;IAsB5C,UAAU,IAAI,2BAA2B;WAUlC,UAAU,CAAC,IAAI,EAAE,2BAA2B,GAAG,iBAAiB;CAGjF;AAED,gEAAgE;AAChE,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,iBAAiB,CAE7F;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GACnC,IAAI,IAAI,iBAAiB,CAE3B;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,SAAS,eAAe,EAAE,GAClC,MAAM,IAAI,CAqCZ"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// The DecoratorNode ↔ LLui sub-view bridge.
|
|
2
|
+
//
|
|
3
|
+
// `LLuiDecoratorNode` is a single generic node class: it stores a `bridgeType`
|
|
4
|
+
// + serialized `data` (so it round-trips through markdown) and, when decorated,
|
|
5
|
+
// mounts the matching {@link DecoratorBridge}'s LLui sub-app into its DOM. Each
|
|
6
|
+
// sub-app is an independent TEA loop with its own `send`/state. Registries and
|
|
7
|
+
// live mount disposers are keyed per-editor (no module-global mutable state, so
|
|
8
|
+
// multiple editors and SSR stay isolated).
|
|
9
|
+
import { $getNodeByKey, DecoratorNode, } from 'lexical';
|
|
10
|
+
const REGISTRIES = new WeakMap();
|
|
11
|
+
const MOUNTS = new WeakMap();
|
|
12
|
+
function mountsFor(editor) {
|
|
13
|
+
let mounts = MOUNTS.get(editor);
|
|
14
|
+
if (!mounts) {
|
|
15
|
+
mounts = new Map();
|
|
16
|
+
MOUNTS.set(editor, mounts);
|
|
17
|
+
}
|
|
18
|
+
return mounts;
|
|
19
|
+
}
|
|
20
|
+
/** A generic decorator node that mounts an LLui sub-view via a registered
|
|
21
|
+
* {@link DecoratorBridge}. */
|
|
22
|
+
export class LLuiDecoratorNode extends DecoratorNode {
|
|
23
|
+
__bridgeType;
|
|
24
|
+
__data;
|
|
25
|
+
static getType() {
|
|
26
|
+
return 'llui-decorator';
|
|
27
|
+
}
|
|
28
|
+
static clone(node) {
|
|
29
|
+
return new LLuiDecoratorNode(node.__bridgeType, node.__data, node.__key);
|
|
30
|
+
}
|
|
31
|
+
constructor(bridgeType, data, key) {
|
|
32
|
+
super(key);
|
|
33
|
+
this.__bridgeType = bridgeType;
|
|
34
|
+
this.__data = data;
|
|
35
|
+
}
|
|
36
|
+
createDOM(_config) {
|
|
37
|
+
const el = document.createElement('div');
|
|
38
|
+
el.setAttribute('data-llui-decorator', this.__bridgeType);
|
|
39
|
+
return el;
|
|
40
|
+
}
|
|
41
|
+
updateDOM() {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/** LLui decorators are block-level (callouts, math/mermaid blocks, embeds). */
|
|
45
|
+
isInline() {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
static importDOM() {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/** The bridge type id this node renders (used by markdown transformers). */
|
|
52
|
+
getBridgeType() {
|
|
53
|
+
return this.getLatest().__bridgeType;
|
|
54
|
+
}
|
|
55
|
+
/** Read the latest serialized data (for the sub-view). */
|
|
56
|
+
getData() {
|
|
57
|
+
return this.getLatest().__data;
|
|
58
|
+
}
|
|
59
|
+
/** Persist new data into the node (markdown-serializable). */
|
|
60
|
+
setData(data) {
|
|
61
|
+
this.getWritable().__data = data;
|
|
62
|
+
}
|
|
63
|
+
decorate(editor) {
|
|
64
|
+
const container = document.createElement('div');
|
|
65
|
+
const bridge = REGISTRIES.get(editor)?.get(this.__bridgeType);
|
|
66
|
+
if (!bridge)
|
|
67
|
+
return container;
|
|
68
|
+
const key = this.getKey();
|
|
69
|
+
const api = {
|
|
70
|
+
editor,
|
|
71
|
+
update: (next) => editor.update(() => {
|
|
72
|
+
const node = $getNodeByKey(key);
|
|
73
|
+
if ($isLLuiDecoratorNode(node))
|
|
74
|
+
node.setData(next);
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
const mounts = mountsFor(editor);
|
|
78
|
+
// Re-decoration (data change): tear the previous sub-app down first.
|
|
79
|
+
mounts.get(key)?.();
|
|
80
|
+
mounts.set(key, bridge.mount(container, this.getData(), api));
|
|
81
|
+
return container;
|
|
82
|
+
}
|
|
83
|
+
exportJSON() {
|
|
84
|
+
return {
|
|
85
|
+
...super.exportJSON(),
|
|
86
|
+
type: 'llui-decorator',
|
|
87
|
+
version: 1,
|
|
88
|
+
bridgeType: this.__bridgeType,
|
|
89
|
+
data: this.__data,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
static importJSON(json) {
|
|
93
|
+
return new LLuiDecoratorNode(json.bridgeType, json.data);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Create a decorator node for `bridgeType` carrying `data`. */
|
|
97
|
+
export function $createLLuiDecoratorNode(bridgeType, data) {
|
|
98
|
+
return new LLuiDecoratorNode(bridgeType, data);
|
|
99
|
+
}
|
|
100
|
+
export function $isLLuiDecoratorNode(node) {
|
|
101
|
+
return node instanceof LLuiDecoratorNode;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Wire decorator bridges onto an editor: register the bridge registry, place
|
|
105
|
+
* each decoration element into its node's DOM, and dispose sub-apps when their
|
|
106
|
+
* nodes are destroyed. Returns a disposer that tears down all live sub-apps.
|
|
107
|
+
* Typically called from a plugin's `register`.
|
|
108
|
+
*/
|
|
109
|
+
export function registerDecoratorBridges(editor, bridges) {
|
|
110
|
+
// Merge into any existing registry so multiple decorator plugins compose.
|
|
111
|
+
const registry = REGISTRIES.get(editor) ?? new Map();
|
|
112
|
+
for (const bridge of bridges)
|
|
113
|
+
registry.set(bridge.type, bridge);
|
|
114
|
+
REGISTRIES.set(editor, registry);
|
|
115
|
+
const unregisterDecorator = editor.registerDecoratorListener((decorators) => {
|
|
116
|
+
for (const key of Object.keys(decorators)) {
|
|
117
|
+
const decoration = decorators[key];
|
|
118
|
+
const host = editor.getElementByKey(key);
|
|
119
|
+
if (host && decoration && decoration.parentElement !== host) {
|
|
120
|
+
host.replaceChildren(decoration);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const unregisterMutation = editor.registerMutationListener(LLuiDecoratorNode, (mutations) => {
|
|
125
|
+
const mounts = MOUNTS.get(editor);
|
|
126
|
+
if (!mounts)
|
|
127
|
+
return;
|
|
128
|
+
for (const [key, kind] of mutations) {
|
|
129
|
+
if (kind === 'destroyed') {
|
|
130
|
+
mounts.get(key)?.();
|
|
131
|
+
mounts.delete(key);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return () => {
|
|
136
|
+
unregisterDecorator();
|
|
137
|
+
unregisterMutation();
|
|
138
|
+
const mounts = MOUNTS.get(editor);
|
|
139
|
+
if (mounts) {
|
|
140
|
+
for (const dispose of mounts.values())
|
|
141
|
+
dispose();
|
|
142
|
+
mounts.clear();
|
|
143
|
+
}
|
|
144
|
+
REGISTRIES.delete(editor);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorator.js","sourceRoot":"","sources":["../src/decorator.ts"],"names":[],"mappings":"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","sourcesContent":["// The DecoratorNode ↔ LLui sub-view bridge.\n//\n// `LLuiDecoratorNode` is a single generic node class: it stores a `bridgeType`\n// + serialized `data` (so it round-trips through markdown) and, when decorated,\n// mounts the matching {@link DecoratorBridge}'s LLui sub-app into its DOM. Each\n// sub-app is an independent TEA loop with its own `send`/state. Registries and\n// live mount disposers are keyed per-editor (no module-global mutable state, so\n// multiple editors and SSR stay isolated).\n\nimport {\n $getNodeByKey,\n DecoratorNode,\n type DOMConversionMap,\n type EditorConfig,\n type LexicalEditor,\n type LexicalNode,\n type NodeKey,\n type SerializedLexicalNode,\n type Spread,\n} from 'lexical'\nimport type { DecoratorApi, DecoratorBridge } from './plugin.js'\n\nexport type SerializedLLuiDecoratorNode = Spread<\n { bridgeType: string; data: unknown },\n SerializedLexicalNode\n>\n\nconst REGISTRIES = new WeakMap<LexicalEditor, Map<string, DecoratorBridge>>()\nconst MOUNTS = new WeakMap<LexicalEditor, Map<NodeKey, () => void>>()\n\nfunction mountsFor(editor: LexicalEditor): Map<NodeKey, () => void> {\n let mounts = MOUNTS.get(editor)\n if (!mounts) {\n mounts = new Map()\n MOUNTS.set(editor, mounts)\n }\n return mounts\n}\n\n/** A generic decorator node that mounts an LLui sub-view via a registered\n * {@link DecoratorBridge}. */\nexport class LLuiDecoratorNode extends DecoratorNode<HTMLElement> {\n __bridgeType: string\n __data: unknown\n\n static override getType(): string {\n return 'llui-decorator'\n }\n\n static override clone(node: LLuiDecoratorNode): LLuiDecoratorNode {\n return new LLuiDecoratorNode(node.__bridgeType, node.__data, node.__key)\n }\n\n constructor(bridgeType: string, data: unknown, key?: NodeKey) {\n super(key)\n this.__bridgeType = bridgeType\n this.__data = data\n }\n\n override createDOM(_config: EditorConfig): HTMLElement {\n const el = document.createElement('div')\n el.setAttribute('data-llui-decorator', this.__bridgeType)\n return el\n }\n\n override updateDOM(): false {\n return false\n }\n\n /** LLui decorators are block-level (callouts, math/mermaid blocks, embeds). */\n override isInline(): false {\n return false\n }\n\n static override importDOM(): DOMConversionMap | null {\n return null\n }\n\n /** The bridge type id this node renders (used by markdown transformers). */\n getBridgeType(): string {\n return this.getLatest().__bridgeType\n }\n\n /** Read the latest serialized data (for the sub-view). */\n getData(): unknown {\n return this.getLatest().__data\n }\n\n /** Persist new data into the node (markdown-serializable). */\n setData(data: unknown): void {\n this.getWritable().__data = data\n }\n\n override decorate(editor: LexicalEditor): HTMLElement {\n const container = document.createElement('div')\n const bridge = REGISTRIES.get(editor)?.get(this.__bridgeType)\n if (!bridge) return container\n\n const key = this.getKey()\n const api: DecoratorApi<unknown> = {\n editor,\n update: (next) =>\n editor.update(() => {\n const node = $getNodeByKey(key)\n if ($isLLuiDecoratorNode(node)) node.setData(next)\n }),\n }\n\n const mounts = mountsFor(editor)\n // Re-decoration (data change): tear the previous sub-app down first.\n mounts.get(key)?.()\n mounts.set(key, bridge.mount(container, this.getData(), api))\n return container\n }\n\n override exportJSON(): SerializedLLuiDecoratorNode {\n return {\n ...super.exportJSON(),\n type: 'llui-decorator',\n version: 1,\n bridgeType: this.__bridgeType,\n data: this.__data,\n }\n }\n\n static override importJSON(json: SerializedLLuiDecoratorNode): LLuiDecoratorNode {\n return new LLuiDecoratorNode(json.bridgeType, json.data)\n }\n}\n\n/** Create a decorator node for `bridgeType` carrying `data`. */\nexport function $createLLuiDecoratorNode(bridgeType: string, data: unknown): LLuiDecoratorNode {\n return new LLuiDecoratorNode(bridgeType, data)\n}\n\nexport function $isLLuiDecoratorNode(\n node: LexicalNode | null | undefined,\n): node is LLuiDecoratorNode {\n return node instanceof LLuiDecoratorNode\n}\n\n/**\n * Wire decorator bridges onto an editor: register the bridge registry, place\n * each decoration element into its node's DOM, and dispose sub-apps when their\n * nodes are destroyed. Returns a disposer that tears down all live sub-apps.\n * Typically called from a plugin's `register`.\n */\nexport function registerDecoratorBridges(\n editor: LexicalEditor,\n bridges: readonly DecoratorBridge[],\n): () => void {\n // Merge into any existing registry so multiple decorator plugins compose.\n const registry = REGISTRIES.get(editor) ?? new Map<string, DecoratorBridge>()\n for (const bridge of bridges) registry.set(bridge.type, bridge)\n REGISTRIES.set(editor, registry)\n\n const unregisterDecorator = editor.registerDecoratorListener<HTMLElement>((decorators) => {\n for (const key of Object.keys(decorators)) {\n const decoration = decorators[key]\n const host = editor.getElementByKey(key)\n if (host && decoration && decoration.parentElement !== host) {\n host.replaceChildren(decoration)\n }\n }\n })\n\n const unregisterMutation = editor.registerMutationListener(LLuiDecoratorNode, (mutations) => {\n const mounts = MOUNTS.get(editor)\n if (!mounts) return\n for (const [key, kind] of mutations) {\n if (kind === 'destroyed') {\n mounts.get(key)?.()\n mounts.delete(key)\n }\n }\n })\n\n return () => {\n unregisterDecorator()\n unregisterMutation()\n const mounts = MOUNTS.get(editor)\n if (mounts) {\n for (const dispose of mounts.values()) dispose()\n mounts.clear()\n }\n REGISTRIES.delete(editor)\n }\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type EditorThemeClasses, type Klass, type LexicalEditor, type LexicalNode } from 'lexical';
|
|
2
|
+
import { type Mountable, type Signal } from '@llui/dom';
|
|
3
|
+
import type { LexicalPlugin } from './plugin.js';
|
|
4
|
+
/** Lexical update tag marking a programmatic write (seed / controlled setValue),
|
|
5
|
+
* so the outbound change listener doesn't echo it back to the host. */
|
|
6
|
+
export declare const PROGRAMMATIC_TAG = "@llui/lexical:programmatic";
|
|
7
|
+
/** Context handed to the selection callback on every commit. */
|
|
8
|
+
export interface SelectionContext {
|
|
9
|
+
editor: LexicalEditor;
|
|
10
|
+
canUndo: boolean;
|
|
11
|
+
canRedo: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface LexicalForeignOptions<Emit = unknown> {
|
|
14
|
+
/** Editor namespace (instance isolation; required for distinct editors). */
|
|
15
|
+
namespace: string;
|
|
16
|
+
theme?: EditorThemeClasses;
|
|
17
|
+
/** Node classes registered in addition to the plugins' own nodes. */
|
|
18
|
+
nodes?: ReadonlyArray<Klass<LexicalNode>>;
|
|
19
|
+
/** Plugins: their `nodes` are merged, `register`/`shortcuts` wired at mount. */
|
|
20
|
+
plugins?: ReadonlyArray<LexicalPlugin<Emit>>;
|
|
21
|
+
/** Serialize the live document → string (runs in a read context). */
|
|
22
|
+
serialize: (editor: LexicalEditor) => string;
|
|
23
|
+
/** Deserialize a string into the document (runs in an update context). */
|
|
24
|
+
deserialize: (editor: LexicalEditor, value: string) => void;
|
|
25
|
+
/** Initial document (uncontrolled) — ignored when `value` is provided. */
|
|
26
|
+
defaultValue?: string;
|
|
27
|
+
/** Controlled document signal; the editor follows it (echo-guarded). */
|
|
28
|
+
value?: Signal<string>;
|
|
29
|
+
/** Reactive read-only flag (always supplied by the host's state). */
|
|
30
|
+
readOnly: Signal<boolean>;
|
|
31
|
+
/** Debounce window (ms) for outbound serialization. Default 300. */
|
|
32
|
+
changeDebounceMs?: number;
|
|
33
|
+
/** Outbound: serialized document changed (debounced, real edits only). */
|
|
34
|
+
onChange?: (value: string) => void;
|
|
35
|
+
/** Outbound: selection / format / structure changed (every commit). */
|
|
36
|
+
onSelectionChange?: (ctx: SelectionContext) => void;
|
|
37
|
+
/** Host emit, handed to each plugin's `register` context. */
|
|
38
|
+
emit?: (msg: Emit) => void;
|
|
39
|
+
/** Receives the live editor at mount (host dispatches commands through it). */
|
|
40
|
+
onReady?: (editor: LexicalEditor) => void;
|
|
41
|
+
/** Extra registration after rich-text (e.g. markdown shortcuts). Disposer. */
|
|
42
|
+
register?: (editor: LexicalEditor) => () => void;
|
|
43
|
+
onError?: (error: Error) => void;
|
|
44
|
+
}
|
|
45
|
+
/** Mount Lexical into an LLui view. Returns a `Mountable` placed in the view
|
|
46
|
+
* array; Lexical is created on mount and destroyed on the component's dispose. */
|
|
47
|
+
export declare function lexicalForeign<Emit = unknown>(opts: LexicalForeignOptions<Emit>): Mountable;
|
|
48
|
+
//# sourceMappingURL=foreign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"foreign.d.ts","sourceRoot":"","sources":["../src/foreign.ts"],"names":[],"mappings":"AASA,OAAO,EAKL,KAAK,kBAAkB,EACvB,KAAK,KAAK,EACV,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,SAAS,CAAA;AAIhB,OAAO,EAAW,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,aAAa,CAAA;AAG/D;uEACuE;AACvE,eAAO,MAAM,gBAAgB,+BAA+B,CAAA;AAE5D,gEAAgE;AAChE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,aAAa,CAAA;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,qBAAqB,CAAC,IAAI,GAAG,OAAO;IACnD,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,kBAAkB,CAAA;IAC1B,qEAAqE;IACrE,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAA;IACzC,gFAAgF;IAChF,OAAO,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;IAC5C,qEAAqE;IACrE,SAAS,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,CAAA;IAC5C,0EAA0E;IAC1E,WAAW,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3D,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;IACtB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IACzB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACnD,6DAA6D;IAC7D,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,CAAA;IAC1B,+EAA+E;IAC/E,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAA;IACzC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,IAAI,CAAA;IAChD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACjC;AAgBD;kFACkF;AAClF,wBAAgB,cAAc,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,EAAE,qBAAqB,CAAC,IAAI,CAAC,GAAG,SAAS,CA4J3F"}
|
package/dist/foreign.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// The load-bearing seam: mount a Lexical editor inside an LLui view via
|
|
2
|
+
// `foreign()`. Lexical owns the contentEditable subtree; LLui owns the chrome.
|
|
3
|
+
//
|
|
4
|
+
// Inbound (controlled): a `value` signal drives the document, echo-suppressed so
|
|
5
|
+
// the editor never fights its own emissions. Outbound: a debounced
|
|
6
|
+
// update-listener serializes the document and a synchronous one surfaces the
|
|
7
|
+
// selection/format. Serialize/deserialize are injected so this stays
|
|
8
|
+
// markdown-agnostic — the markdown layer supplies the transformer converters.
|
|
9
|
+
import { CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, createEditor, } from 'lexical';
|
|
10
|
+
import { registerRichText } from '@lexical/rich-text';
|
|
11
|
+
import { registerHistory, createEmptyHistoryState } from '@lexical/history';
|
|
12
|
+
import { mergeRegister } from '@lexical/utils';
|
|
13
|
+
import { foreign } from '@llui/dom';
|
|
14
|
+
import { registerShortcuts } from './register.js';
|
|
15
|
+
/** Lexical update tag marking a programmatic write (seed / controlled setValue),
|
|
16
|
+
* so the outbound change listener doesn't echo it back to the host. */
|
|
17
|
+
export const PROGRAMMATIC_TAG = '@llui/lexical:programmatic';
|
|
18
|
+
/** Mount Lexical into an LLui view. Returns a `Mountable` placed in the view
|
|
19
|
+
* array; Lexical is created on mount and destroyed on the component's dispose. */
|
|
20
|
+
export function lexicalForeign(opts) {
|
|
21
|
+
const debounceMs = opts.changeDebounceMs ?? 300;
|
|
22
|
+
const boot = (el) => {
|
|
23
|
+
// De-duplicate node classes by reference: registering the same Klass twice
|
|
24
|
+
// (e.g. two decorator plugins sharing LLuiDecoratorNode) throws in Lexical.
|
|
25
|
+
const nodeSet = new Set(opts.nodes ?? []);
|
|
26
|
+
for (const plugin of opts.plugins ?? []) {
|
|
27
|
+
for (const node of plugin.nodes ?? [])
|
|
28
|
+
nodeSet.add(node);
|
|
29
|
+
}
|
|
30
|
+
const nodes = [...nodeSet];
|
|
31
|
+
const editor = createEditor({
|
|
32
|
+
namespace: opts.namespace,
|
|
33
|
+
nodes,
|
|
34
|
+
theme: opts.theme,
|
|
35
|
+
editable: !opts.readOnly.peek(),
|
|
36
|
+
onError: (error) => {
|
|
37
|
+
if (opts.onError)
|
|
38
|
+
opts.onError(error);
|
|
39
|
+
else
|
|
40
|
+
throw error;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
// Vanilla Lexical does NOT make the root editable — the caller must set
|
|
44
|
+
// `contenteditable` (the React `<ContentEditable>` does this). Without it the
|
|
45
|
+
// browser shows no caret and ignores typing.
|
|
46
|
+
el.setAttribute('contenteditable', opts.readOnly.peek() ? 'false' : 'true');
|
|
47
|
+
editor.setRootElement(el);
|
|
48
|
+
opts.onReady?.(editor);
|
|
49
|
+
let lastEmitted = opts.value ? opts.value.peek() : (opts.defaultValue ?? '');
|
|
50
|
+
let canUndo = false;
|
|
51
|
+
let canRedo = false;
|
|
52
|
+
let debounceTimer;
|
|
53
|
+
// Seed the initial document (programmatic — not echoed outbound). Discrete so
|
|
54
|
+
// the host is populated synchronously at mount (before the first paint/read).
|
|
55
|
+
// NB: seeding happens AFTER registration below, so plugins/decorator bridges
|
|
56
|
+
// are live when the seed document is built (e.g. a callout in the seed needs
|
|
57
|
+
// its bridge registered to decorate).
|
|
58
|
+
const ctx = { emit: (msg) => opts.emit?.(msg) };
|
|
59
|
+
const pluginDisposers = (opts.plugins ?? []).map((plugin) => {
|
|
60
|
+
const reg = plugin.register?.(editor, ctx) ?? (() => { });
|
|
61
|
+
const shortcuts = plugin.shortcuts ? registerShortcuts(editor, plugin.shortcuts) : () => { };
|
|
62
|
+
return () => {
|
|
63
|
+
reg();
|
|
64
|
+
shortcuts();
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
const emitSelection = () => opts.onSelectionChange?.({ editor, canUndo, canRedo });
|
|
68
|
+
const baseDispose = mergeRegister(registerRichText(editor), registerHistory(editor, createEmptyHistoryState(), 1000), opts.register?.(editor) ?? (() => { }), editor.registerCommand(CAN_UNDO_COMMAND, (payload) => {
|
|
69
|
+
canUndo = payload;
|
|
70
|
+
emitSelection();
|
|
71
|
+
return false;
|
|
72
|
+
}, COMMAND_PRIORITY_LOW), editor.registerCommand(CAN_REDO_COMMAND, (payload) => {
|
|
73
|
+
canRedo = payload;
|
|
74
|
+
emitSelection();
|
|
75
|
+
return false;
|
|
76
|
+
}, COMMAND_PRIORITY_LOW), editor.registerUpdateListener(({ editorState, tags }) => {
|
|
77
|
+
emitSelection();
|
|
78
|
+
if (tags.has(PROGRAMMATIC_TAG))
|
|
79
|
+
return;
|
|
80
|
+
if (debounceTimer !== undefined)
|
|
81
|
+
clearTimeout(debounceTimer);
|
|
82
|
+
debounceTimer = setTimeout(() => {
|
|
83
|
+
editorState.read(() => {
|
|
84
|
+
const next = opts.serialize(editor);
|
|
85
|
+
lastEmitted = next;
|
|
86
|
+
opts.onChange?.(next);
|
|
87
|
+
});
|
|
88
|
+
}, debounceMs);
|
|
89
|
+
}), ...pluginDisposers);
|
|
90
|
+
// Seed now that rich-text, history, plugins, and decorator bridges are live.
|
|
91
|
+
editor.update(() => opts.deserialize(editor, lastEmitted), {
|
|
92
|
+
tag: PROGRAMMATIC_TAG,
|
|
93
|
+
discrete: true,
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
editor,
|
|
97
|
+
getLastEmitted: () => lastEmitted,
|
|
98
|
+
setLastEmitted: (value) => {
|
|
99
|
+
lastEmitted = value;
|
|
100
|
+
},
|
|
101
|
+
dispose: () => {
|
|
102
|
+
if (debounceTimer !== undefined)
|
|
103
|
+
clearTimeout(debounceTimer);
|
|
104
|
+
baseDispose();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const readOnly = opts.readOnly;
|
|
109
|
+
const controlled = opts.value;
|
|
110
|
+
if (controlled) {
|
|
111
|
+
return foreign({
|
|
112
|
+
tag: 'div',
|
|
113
|
+
state: { value: controlled, readOnly },
|
|
114
|
+
mount: ({ el, state }) => {
|
|
115
|
+
const b = boot(el);
|
|
116
|
+
const unbindValue = state.value.bind((incoming) => {
|
|
117
|
+
if (incoming === b.getLastEmitted())
|
|
118
|
+
return;
|
|
119
|
+
b.editor.update(() => opts.deserialize(b.editor, incoming), { tag: PROGRAMMATIC_TAG });
|
|
120
|
+
b.setLastEmitted(incoming);
|
|
121
|
+
});
|
|
122
|
+
const unbindReadOnly = state.readOnly.bind((ro) => {
|
|
123
|
+
b.editor.setEditable(!ro);
|
|
124
|
+
el.setAttribute('contenteditable', ro ? 'false' : 'true');
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
dispose: () => {
|
|
128
|
+
unbindValue();
|
|
129
|
+
unbindReadOnly();
|
|
130
|
+
b.dispose();
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
unmount: (inst) => inst.dispose(),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return foreign({
|
|
138
|
+
tag: 'div',
|
|
139
|
+
state: { readOnly },
|
|
140
|
+
mount: ({ el, state }) => {
|
|
141
|
+
const b = boot(el);
|
|
142
|
+
const unbindReadOnly = state.readOnly.bind((ro) => {
|
|
143
|
+
b.editor.setEditable(!ro);
|
|
144
|
+
el.contentEditable = ro ? 'false' : 'true';
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
dispose: () => {
|
|
148
|
+
unbindReadOnly();
|
|
149
|
+
b.dispose();
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
unmount: (inst) => inst.dispose(),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=foreign.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"foreign.js","sourceRoot":"","sources":["../src/foreign.ts"],"names":[],"mappings":"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","sourcesContent":["// The load-bearing seam: mount a Lexical editor inside an LLui view via\n// `foreign()`. Lexical owns the contentEditable subtree; LLui owns the chrome.\n//\n// Inbound (controlled): a `value` signal drives the document, echo-suppressed so\n// the editor never fights its own emissions. Outbound: a debounced\n// update-listener serializes the document and a synchronous one surfaces the\n// selection/format. Serialize/deserialize are injected so this stays\n// markdown-agnostic — the markdown layer supplies the transformer converters.\n\nimport {\n CAN_REDO_COMMAND,\n CAN_UNDO_COMMAND,\n COMMAND_PRIORITY_LOW,\n createEditor,\n type EditorThemeClasses,\n type Klass,\n type LexicalEditor,\n type LexicalNode,\n} from 'lexical'\nimport { registerRichText } from '@lexical/rich-text'\nimport { registerHistory, createEmptyHistoryState } from '@lexical/history'\nimport { mergeRegister } from '@lexical/utils'\nimport { foreign, type Mountable, type Signal } from '@llui/dom'\nimport type { LexicalPlugin, PluginContext } from './plugin.js'\nimport { registerShortcuts } from './register.js'\n\n/** Lexical update tag marking a programmatic write (seed / controlled setValue),\n * so the outbound change listener doesn't echo it back to the host. */\nexport const PROGRAMMATIC_TAG = '@llui/lexical:programmatic'\n\n/** Context handed to the selection callback on every commit. */\nexport interface SelectionContext {\n editor: LexicalEditor\n canUndo: boolean\n canRedo: boolean\n}\n\nexport interface LexicalForeignOptions<Emit = unknown> {\n /** Editor namespace (instance isolation; required for distinct editors). */\n namespace: string\n theme?: EditorThemeClasses\n /** Node classes registered in addition to the plugins' own nodes. */\n nodes?: ReadonlyArray<Klass<LexicalNode>>\n /** Plugins: their `nodes` are merged, `register`/`shortcuts` wired at mount. */\n plugins?: ReadonlyArray<LexicalPlugin<Emit>>\n /** Serialize the live document → string (runs in a read context). */\n serialize: (editor: LexicalEditor) => string\n /** Deserialize a string into the document (runs in an update context). */\n deserialize: (editor: LexicalEditor, value: string) => void\n /** Initial document (uncontrolled) — ignored when `value` is provided. */\n defaultValue?: string\n /** Controlled document signal; the editor follows it (echo-guarded). */\n value?: Signal<string>\n /** Reactive read-only flag (always supplied by the host's state). */\n readOnly: Signal<boolean>\n /** Debounce window (ms) for outbound serialization. Default 300. */\n changeDebounceMs?: number\n /** Outbound: serialized document changed (debounced, real edits only). */\n onChange?: (value: string) => void\n /** Outbound: selection / format / structure changed (every commit). */\n onSelectionChange?: (ctx: SelectionContext) => void\n /** Host emit, handed to each plugin's `register` context. */\n emit?: (msg: Emit) => void\n /** Receives the live editor at mount (host dispatches commands through it). */\n onReady?: (editor: LexicalEditor) => void\n /** Extra registration after rich-text (e.g. markdown shortcuts). Disposer. */\n register?: (editor: LexicalEditor) => () => void\n onError?: (error: Error) => void\n}\n\n/** The booted editor + echo-guard accessors, shared by both control modes. */\ninterface BootResult {\n editor: LexicalEditor\n getLastEmitted: () => string\n setLastEmitted: (value: string) => void\n /** Tear down listeners, history, plugins, and the pending debounce timer. */\n dispose: () => void\n}\n\n/** The `foreign` instance — only a disposer is needed at unmount. */\ninterface ForeignInst {\n dispose: () => void\n}\n\n/** Mount Lexical into an LLui view. Returns a `Mountable` placed in the view\n * array; Lexical is created on mount and destroyed on the component's dispose. */\nexport function lexicalForeign<Emit = unknown>(opts: LexicalForeignOptions<Emit>): Mountable {\n const debounceMs = opts.changeDebounceMs ?? 300\n\n const boot = (el: Element): BootResult => {\n // De-duplicate node classes by reference: registering the same Klass twice\n // (e.g. two decorator plugins sharing LLuiDecoratorNode) throws in Lexical.\n const nodeSet = new Set<Klass<LexicalNode>>(opts.nodes ?? [])\n for (const plugin of opts.plugins ?? []) {\n for (const node of plugin.nodes ?? []) nodeSet.add(node)\n }\n const nodes = [...nodeSet]\n\n const editor = createEditor({\n namespace: opts.namespace,\n nodes,\n theme: opts.theme,\n editable: !opts.readOnly.peek(),\n onError: (error: Error) => {\n if (opts.onError) opts.onError(error)\n else throw error\n },\n })\n // Vanilla Lexical does NOT make the root editable — the caller must set\n // `contenteditable` (the React `<ContentEditable>` does this). Without it the\n // browser shows no caret and ignores typing.\n el.setAttribute('contenteditable', opts.readOnly.peek() ? 'false' : 'true')\n editor.setRootElement(el as HTMLElement)\n opts.onReady?.(editor)\n\n let lastEmitted = opts.value ? opts.value.peek() : (opts.defaultValue ?? '')\n let canUndo = false\n let canRedo = false\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n // Seed the initial document (programmatic — not echoed outbound). Discrete so\n // the host is populated synchronously at mount (before the first paint/read).\n // NB: seeding happens AFTER registration below, so plugins/decorator bridges\n // are live when the seed document is built (e.g. a callout in the seed needs\n // its bridge registered to decorate).\n\n const ctx: PluginContext<Emit> = { emit: (msg) => opts.emit?.(msg) }\n const pluginDisposers = (opts.plugins ?? []).map((plugin) => {\n const reg = plugin.register?.(editor, ctx) ?? (() => {})\n const shortcuts = plugin.shortcuts ? registerShortcuts(editor, plugin.shortcuts) : () => {}\n return () => {\n reg()\n shortcuts()\n }\n })\n\n const emitSelection = (): void => opts.onSelectionChange?.({ editor, canUndo, canRedo })\n\n const baseDispose = mergeRegister(\n registerRichText(editor),\n registerHistory(editor, createEmptyHistoryState(), 1000),\n opts.register?.(editor) ?? (() => {}),\n editor.registerCommand(\n CAN_UNDO_COMMAND,\n (payload: boolean) => {\n canUndo = payload\n emitSelection()\n return false\n },\n COMMAND_PRIORITY_LOW,\n ),\n editor.registerCommand(\n CAN_REDO_COMMAND,\n (payload: boolean) => {\n canRedo = payload\n emitSelection()\n return false\n },\n COMMAND_PRIORITY_LOW,\n ),\n editor.registerUpdateListener(({ editorState, tags }) => {\n emitSelection()\n if (tags.has(PROGRAMMATIC_TAG)) return\n if (debounceTimer !== undefined) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n editorState.read(() => {\n const next = opts.serialize(editor)\n lastEmitted = next\n opts.onChange?.(next)\n })\n }, debounceMs)\n }),\n ...pluginDisposers,\n )\n\n // Seed now that rich-text, history, plugins, and decorator bridges are live.\n editor.update(() => opts.deserialize(editor, lastEmitted), {\n tag: PROGRAMMATIC_TAG,\n discrete: true,\n })\n\n return {\n editor,\n getLastEmitted: () => lastEmitted,\n setLastEmitted: (value) => {\n lastEmitted = value\n },\n dispose: () => {\n if (debounceTimer !== undefined) clearTimeout(debounceTimer)\n baseDispose()\n },\n }\n }\n\n const readOnly = opts.readOnly\n const controlled = opts.value\n\n if (controlled) {\n return foreign<ForeignInst, { value: Signal<string>; readOnly: Signal<boolean> }>({\n tag: 'div',\n state: { value: controlled, readOnly },\n mount: ({ el, state }) => {\n const b = boot(el)\n const unbindValue = state.value.bind((incoming) => {\n if (incoming === b.getLastEmitted()) return\n b.editor.update(() => opts.deserialize(b.editor, incoming), { tag: PROGRAMMATIC_TAG })\n b.setLastEmitted(incoming)\n })\n const unbindReadOnly = state.readOnly.bind((ro) => {\n b.editor.setEditable(!ro)\n el.setAttribute('contenteditable', ro ? 'false' : 'true')\n })\n return {\n dispose: () => {\n unbindValue()\n unbindReadOnly()\n b.dispose()\n },\n }\n },\n unmount: (inst) => inst.dispose(),\n })\n }\n\n return foreign<ForeignInst, { readOnly: Signal<boolean> }>({\n tag: 'div',\n state: { readOnly },\n mount: ({ el, state }) => {\n const b = boot(el)\n const unbindReadOnly = state.readOnly.bind((ro) => {\n b.editor.setEditable(!ro)\n ;(el as HTMLElement).contentEditable = ro ? 'false' : 'true'\n })\n return {\n dispose: () => {\n unbindReadOnly()\n b.dispose()\n },\n }\n },\n unmount: (inst) => inst.dispose(),\n })\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { type ShortcutSpec, type PluginContext, type DecoratorApi, type DecoratorBridge, type LexicalPlugin, decoratorBridge, } from './plugin.js';
|
|
2
|
+
export { type ParsedCombo, parseCombo, matchesCombo, isMacPlatform, registerShortcuts, } from './register.js';
|
|
3
|
+
export { type BaseBlockType, type Alignment, type BaseFormat, $readBaseFormat, readBaseFormat, } from './selection.js';
|
|
4
|
+
export { PROGRAMMATIC_TAG, type SelectionContext, type LexicalForeignOptions, lexicalForeign, } from './foreign.js';
|
|
5
|
+
export { type SerializedLLuiDecoratorNode, LLuiDecoratorNode, $createLLuiDecoratorNode, $isLLuiDecoratorNode, registerDecoratorBridges, } from './decorator.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,eAAe,GAChB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,WAAW,EAChB,UAAU,EACV,YAAY,EACZ,aAAa,EACb,iBAAiB,GAClB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,eAAe,EACf,cAAc,GACf,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,cAAc,GACf,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,KAAK,2BAA2B,EAChC,iBAAiB,EACjB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,gBAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// `@llui/lexical` — low-level binding between Lexical and the LLui signal runtime.
|
|
2
|
+
export { decoratorBridge, } from './plugin.js';
|
|
3
|
+
export { parseCombo, matchesCombo, isMacPlatform, registerShortcuts, } from './register.js';
|
|
4
|
+
export { $readBaseFormat, readBaseFormat, } from './selection.js';
|
|
5
|
+
export { PROGRAMMATIC_TAG, lexicalForeign, } from './foreign.js';
|
|
6
|
+
export { LLuiDecoratorNode, $createLLuiDecoratorNode, $isLLuiDecoratorNode, registerDecoratorBridges, } from './decorator.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAEnF,OAAO,EAML,eAAe,GAChB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAEL,UAAU,EACV,YAAY,EACZ,aAAa,EACb,iBAAiB,GAClB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAIL,eAAe,EACf,cAAc,GACf,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,gBAAgB,EAGhB,cAAc,GACf,MAAM,cAAc,CAAA;AAErB,OAAO,EAEL,iBAAiB,EACjB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,gBAAgB,CAAA","sourcesContent":["// `@llui/lexical` — low-level binding between Lexical and the LLui signal runtime.\n\nexport {\n type ShortcutSpec,\n type PluginContext,\n type DecoratorApi,\n type DecoratorBridge,\n type LexicalPlugin,\n decoratorBridge,\n} from './plugin.js'\n\nexport {\n type ParsedCombo,\n parseCombo,\n matchesCombo,\n isMacPlatform,\n registerShortcuts,\n} from './register.js'\n\nexport {\n type BaseBlockType,\n type Alignment,\n type BaseFormat,\n $readBaseFormat,\n readBaseFormat,\n} from './selection.js'\n\nexport {\n PROGRAMMATIC_TAG,\n type SelectionContext,\n type LexicalForeignOptions,\n lexicalForeign,\n} from './foreign.js'\n\nexport {\n type SerializedLLuiDecoratorNode,\n LLuiDecoratorNode,\n $createLLuiDecoratorNode,\n $isLLuiDecoratorNode,\n registerDecoratorBridges,\n} from './decorator.js'\n"]}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Klass, LexicalEditor, LexicalNode } from 'lexical';
|
|
2
|
+
import { type SignalComponentDef } from '@llui/dom';
|
|
3
|
+
/** A keyboard shortcut bound to an editor action.
|
|
4
|
+
*
|
|
5
|
+
* `combo` is a normalized chord: `Mod` resolves to ⌘ on macOS and Ctrl
|
|
6
|
+
* elsewhere, e.g. `Mod-b`, `Mod-Shift-7`, `Mod-Alt-1`. `run` returns `true`
|
|
7
|
+
* when it handled the event (which stops propagation / prevents default). */
|
|
8
|
+
export interface ShortcutSpec {
|
|
9
|
+
combo: string;
|
|
10
|
+
run: (editor: LexicalEditor) => boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Context handed to `plugin.register` so a plugin can talk back to the host
|
|
13
|
+
* (e.g. open a slash menu) without owning the host's `send`. `Emit` is the
|
|
14
|
+
* host message type; `@llui/lexical` leaves it `unknown`, hosts narrow it. */
|
|
15
|
+
export interface PluginContext<Emit = unknown> {
|
|
16
|
+
/** Emit a host message into the embedding component's update loop. */
|
|
17
|
+
emit: (msg: Emit) => void;
|
|
18
|
+
}
|
|
19
|
+
/** Imperative handle a decorator sub-view uses to talk to its Lexical node. */
|
|
20
|
+
export interface DecoratorApi<Data> {
|
|
21
|
+
/** Persist new node data (writes into the Lexical node → markdown-serializable). */
|
|
22
|
+
update: (next: Data) => void;
|
|
23
|
+
/** The owning Lexical editor (for dispatching commands, reading state). */
|
|
24
|
+
editor: LexicalEditor;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Bridges a custom node type to an LLui sub-view. The sub-view's
|
|
28
|
+
* `State`/`Msg`/`Effect` and `Data` types are fully erased here: a bridge is
|
|
29
|
+
* stored monomorphically in the registry, and `mount` builds + mounts the
|
|
30
|
+
* sub-app, returning a disposer. Authors construct bridges with the typed
|
|
31
|
+
* {@link decoratorBridge} helper, which captures concrete types in a closure.
|
|
32
|
+
*/
|
|
33
|
+
export interface DecoratorBridge {
|
|
34
|
+
/** The id used by the contributing markdown transformer. */
|
|
35
|
+
type: string;
|
|
36
|
+
/** Mount the sub-view for a node's (deserialized) data; returns a disposer. */
|
|
37
|
+
mount: (container: Element, data: unknown, api: DecoratorApi<unknown>) => () => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Author-facing constructor for a {@link DecoratorBridge}. Preserves the
|
|
41
|
+
* sub-component's `State`/`Msg`/`Effect` and the node `Data` type at the
|
|
42
|
+
* definition site; only the node's serialized payload is narrowed back to
|
|
43
|
+
* `Data` at mount time (the single deserialization-boundary cast, exactly like
|
|
44
|
+
* `JSON.parse` returning a declared type).
|
|
45
|
+
*/
|
|
46
|
+
export declare function decoratorBridge<Data, S, M extends {
|
|
47
|
+
type: string;
|
|
48
|
+
}, E extends {
|
|
49
|
+
type: string;
|
|
50
|
+
} = never>(type: string, factory: (data: Data, api: DecoratorApi<Data>) => SignalComponentDef<S, M, E>): DecoratorBridge;
|
|
51
|
+
/** A composable unit of editor behaviour. */
|
|
52
|
+
export interface LexicalPlugin<Emit = unknown> {
|
|
53
|
+
/** Stable identifier (also used for de-duplication and overrides). */
|
|
54
|
+
name: string;
|
|
55
|
+
/** Lexical node classes registered on the editor config. */
|
|
56
|
+
nodes?: ReadonlyArray<Klass<LexicalNode>>;
|
|
57
|
+
/** Imperative registration (commands, listeners). Returns a disposer. */
|
|
58
|
+
register?: (editor: LexicalEditor, ctx: PluginContext<Emit>) => () => void;
|
|
59
|
+
/** Keyboard shortcuts wired through a single KEY_DOWN command. */
|
|
60
|
+
shortcuts?: readonly ShortcutSpec[];
|
|
61
|
+
/** Decorator bridges this plugin owns. */
|
|
62
|
+
decorators?: readonly DecoratorBridge[];
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAChE,OAAO,EAAY,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAE7D;;;;6EAI6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,OAAO,CAAA;CACxC;AAED;;8EAE8E;AAC9E,MAAM,WAAW,aAAa,CAAC,IAAI,GAAG,OAAO;IAC3C,sEAAsE;IACtE,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,CAAA;CAC1B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY,CAAC,IAAI;IAChC,oFAAoF;IACpF,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAC5B,2EAA2E;IAC3E,MAAM,EAAE,aAAa,CAAA;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,KAAK,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC,KAAK,MAAM,IAAI,CAAA;CACrF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EACJ,CAAC,EACD,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAC1B,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,EAElC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,YAAY,CAAC,IAAI,CAAC,KAAK,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAC5E,eAAe,CAYjB;AAED,6CAA6C;AAC7C,MAAM,WAAW,aAAa,CAAC,IAAI,GAAG,OAAO;IAC3C,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAA;IACZ,4DAA4D;IAC5D,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAA;IACzC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,MAAM,IAAI,CAAA;IAC1E,kEAAkE;IAClE,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAA;IACnC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;CACxC"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The plugin contract for the LLui ↔ Lexical binding.
|
|
2
|
+
//
|
|
3
|
+
// A plugin is a pure descriptor: it contributes Lexical node classes, an
|
|
4
|
+
// imperative `register` step (commands/listeners), keyboard shortcuts, and
|
|
5
|
+
// decorator bridges (LLui sub-views mounted inside Lexical DecoratorNodes).
|
|
6
|
+
// It is intentionally markdown-agnostic — `@llui/markdown-editor` extends this
|
|
7
|
+
// contract with markdown transformers and toolbar items.
|
|
8
|
+
import { mountApp } from '@llui/dom';
|
|
9
|
+
/**
|
|
10
|
+
* Author-facing constructor for a {@link DecoratorBridge}. Preserves the
|
|
11
|
+
* sub-component's `State`/`Msg`/`Effect` and the node `Data` type at the
|
|
12
|
+
* definition site; only the node's serialized payload is narrowed back to
|
|
13
|
+
* `Data` at mount time (the single deserialization-boundary cast, exactly like
|
|
14
|
+
* `JSON.parse` returning a declared type).
|
|
15
|
+
*/
|
|
16
|
+
export function decoratorBridge(type, factory) {
|
|
17
|
+
return {
|
|
18
|
+
type,
|
|
19
|
+
mount: (container, data, api) => {
|
|
20
|
+
const typedApi = {
|
|
21
|
+
editor: api.editor,
|
|
22
|
+
update: (next) => api.update(next),
|
|
23
|
+
};
|
|
24
|
+
const handle = mountApp(container, factory(data, typedApi));
|
|
25
|
+
return () => handle.dispose();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,yEAAyE;AACzE,2EAA2E;AAC3E,4EAA4E;AAC5E,+EAA+E;AAC/E,yDAAyD;AAGzD,OAAO,EAAE,QAAQ,EAA2B,MAAM,WAAW,CAAA;AA0C7D;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAM7B,IAAY,EACZ,OAA6E;IAE7E,OAAO;QACL,IAAI;QACJ,KAAK,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;YAC9B,MAAM,QAAQ,GAAuB;gBACnC,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;aACnC,CAAA;YACD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,IAAY,EAAE,QAAQ,CAAC,CAAC,CAAA;YACnE,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAA;QAC/B,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["// The plugin contract for the LLui ↔ Lexical binding.\n//\n// A plugin is a pure descriptor: it contributes Lexical node classes, an\n// imperative `register` step (commands/listeners), keyboard shortcuts, and\n// decorator bridges (LLui sub-views mounted inside Lexical DecoratorNodes).\n// It is intentionally markdown-agnostic — `@llui/markdown-editor` extends this\n// contract with markdown transformers and toolbar items.\n\nimport type { Klass, LexicalEditor, LexicalNode } from 'lexical'\nimport { mountApp, type SignalComponentDef } from '@llui/dom'\n\n/** A keyboard shortcut bound to an editor action.\n *\n * `combo` is a normalized chord: `Mod` resolves to ⌘ on macOS and Ctrl\n * elsewhere, e.g. `Mod-b`, `Mod-Shift-7`, `Mod-Alt-1`. `run` returns `true`\n * when it handled the event (which stops propagation / prevents default). */\nexport interface ShortcutSpec {\n combo: string\n run: (editor: LexicalEditor) => boolean\n}\n\n/** Context handed to `plugin.register` so a plugin can talk back to the host\n * (e.g. open a slash menu) without owning the host's `send`. `Emit` is the\n * host message type; `@llui/lexical` leaves it `unknown`, hosts narrow it. */\nexport interface PluginContext<Emit = unknown> {\n /** Emit a host message into the embedding component's update loop. */\n emit: (msg: Emit) => void\n}\n\n/** Imperative handle a decorator sub-view uses to talk to its Lexical node. */\nexport interface DecoratorApi<Data> {\n /** Persist new node data (writes into the Lexical node → markdown-serializable). */\n update: (next: Data) => void\n /** The owning Lexical editor (for dispatching commands, reading state). */\n editor: LexicalEditor\n}\n\n/**\n * Bridges a custom node type to an LLui sub-view. The sub-view's\n * `State`/`Msg`/`Effect` and `Data` types are fully erased here: a bridge is\n * stored monomorphically in the registry, and `mount` builds + mounts the\n * sub-app, returning a disposer. Authors construct bridges with the typed\n * {@link decoratorBridge} helper, which captures concrete types in a closure.\n */\nexport interface DecoratorBridge {\n /** The id used by the contributing markdown transformer. */\n type: string\n /** Mount the sub-view for a node's (deserialized) data; returns a disposer. */\n mount: (container: Element, data: unknown, api: DecoratorApi<unknown>) => () => void\n}\n\n/**\n * Author-facing constructor for a {@link DecoratorBridge}. Preserves the\n * sub-component's `State`/`Msg`/`Effect` and the node `Data` type at the\n * definition site; only the node's serialized payload is narrowed back to\n * `Data` at mount time (the single deserialization-boundary cast, exactly like\n * `JSON.parse` returning a declared type).\n */\nexport function decoratorBridge<\n Data,\n S,\n M extends { type: string },\n E extends { type: string } = never,\n>(\n type: string,\n factory: (data: Data, api: DecoratorApi<Data>) => SignalComponentDef<S, M, E>,\n): DecoratorBridge {\n return {\n type,\n mount: (container, data, api) => {\n const typedApi: DecoratorApi<Data> = {\n editor: api.editor,\n update: (next) => api.update(next),\n }\n const handle = mountApp(container, factory(data as Data, typedApi))\n return () => handle.dispose()\n },\n }\n}\n\n/** A composable unit of editor behaviour. */\nexport interface LexicalPlugin<Emit = unknown> {\n /** Stable identifier (also used for de-duplication and overrides). */\n name: string\n /** Lexical node classes registered on the editor config. */\n nodes?: ReadonlyArray<Klass<LexicalNode>>\n /** Imperative registration (commands, listeners). Returns a disposer. */\n register?: (editor: LexicalEditor, ctx: PluginContext<Emit>) => () => void\n /** Keyboard shortcuts wired through a single KEY_DOWN command. */\n shortcuts?: readonly ShortcutSpec[]\n /** Decorator bridges this plugin owns. */\n decorators?: readonly DecoratorBridge[]\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type LexicalEditor } from 'lexical';
|
|
2
|
+
import type { ShortcutSpec } from './plugin.js';
|
|
3
|
+
/** A parsed chord. `mod` means ⌘ on macOS / Ctrl elsewhere. */
|
|
4
|
+
export interface ParsedCombo {
|
|
5
|
+
key: string;
|
|
6
|
+
mod: boolean;
|
|
7
|
+
shift: boolean;
|
|
8
|
+
alt: boolean;
|
|
9
|
+
ctrl: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Parse a chord like `Mod-Shift-7` into its parts. Case-insensitive on
|
|
12
|
+
* modifiers; the final segment is the key (lower-cased for letters). */
|
|
13
|
+
export declare function parseCombo(combo: string): ParsedCombo;
|
|
14
|
+
/** Does a keyboard event satisfy a parsed chord? `mod` maps to ⌘ on macOS and
|
|
15
|
+
* Ctrl elsewhere; all declared modifiers must match exactly (no extras). */
|
|
16
|
+
export declare function matchesCombo(event: KeyboardEvent, combo: ParsedCombo, isMac: boolean): boolean;
|
|
17
|
+
/** Best-effort macOS detection (browser only; defaults to false off-DOM). */
|
|
18
|
+
export declare function isMacPlatform(): boolean;
|
|
19
|
+
/** Register a set of shortcuts on the editor through one KEY_DOWN handler.
|
|
20
|
+
* Returns a disposer. The first matching shortcut whose `run` returns `true`
|
|
21
|
+
* wins and the event is consumed. */
|
|
22
|
+
export declare function registerShortcuts(editor: LexicalEditor, shortcuts: readonly ShortcutSpec[]): () => void;
|
|
23
|
+
//# sourceMappingURL=register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6C,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,OAAO,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,GAAG,EAAE,OAAO,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;CACd;AAED;wEACwE;AACxE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAkBrD;AAED;4EAC4E;AAC5E,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAc9F;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAED;;qCAEqC;AACrC,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,SAAS,YAAY,EAAE,GACjC,MAAM,IAAI,CAmBZ"}
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Keyboard-shortcut wiring: parse normalized chord strings and register them
|
|
2
|
+
// through a single KEY_DOWN command on the editor.
|
|
3
|
+
import { COMMAND_PRIORITY_NORMAL, KEY_DOWN_COMMAND } from 'lexical';
|
|
4
|
+
/** Parse a chord like `Mod-Shift-7` into its parts. Case-insensitive on
|
|
5
|
+
* modifiers; the final segment is the key (lower-cased for letters). */
|
|
6
|
+
export function parseCombo(combo) {
|
|
7
|
+
const parts = combo
|
|
8
|
+
.split('-')
|
|
9
|
+
.map((p) => p.trim())
|
|
10
|
+
.filter((p) => p.length > 0);
|
|
11
|
+
const result = { key: '', mod: false, shift: false, alt: false, ctrl: false };
|
|
12
|
+
for (let i = 0; i < parts.length; i++) {
|
|
13
|
+
const part = parts[i];
|
|
14
|
+
const isLast = i === parts.length - 1;
|
|
15
|
+
const lower = part.toLowerCase();
|
|
16
|
+
if (!isLast && (lower === 'mod' || lower === 'cmd' || lower === 'meta'))
|
|
17
|
+
result.mod = true;
|
|
18
|
+
else if (!isLast && lower === 'ctrl')
|
|
19
|
+
result.ctrl = true;
|
|
20
|
+
else if (!isLast && lower === 'shift')
|
|
21
|
+
result.shift = true;
|
|
22
|
+
else if (!isLast && (lower === 'alt' || lower === 'option' || lower === 'opt'))
|
|
23
|
+
result.alt = true;
|
|
24
|
+
else
|
|
25
|
+
result.key = part.length === 1 ? part.toLowerCase() : part;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
/** Does a keyboard event satisfy a parsed chord? `mod` maps to ⌘ on macOS and
|
|
30
|
+
* Ctrl elsewhere; all declared modifiers must match exactly (no extras). */
|
|
31
|
+
export function matchesCombo(event, combo, isMac) {
|
|
32
|
+
const eventKey = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
|
33
|
+
if (eventKey !== combo.key)
|
|
34
|
+
return false;
|
|
35
|
+
const modActive = isMac ? event.metaKey : event.ctrlKey;
|
|
36
|
+
const otherCtrl = isMac ? event.ctrlKey : event.metaKey;
|
|
37
|
+
if (combo.mod !== modActive)
|
|
38
|
+
return false;
|
|
39
|
+
// An explicit `ctrl` on macOS targets the control key specifically.
|
|
40
|
+
if (combo.ctrl !== (isMac ? event.ctrlKey : false)) {
|
|
41
|
+
if (!(combo.mod && !isMac))
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!combo.mod && !combo.ctrl && otherCtrl)
|
|
45
|
+
return false;
|
|
46
|
+
if (combo.shift !== event.shiftKey)
|
|
47
|
+
return false;
|
|
48
|
+
if (combo.alt !== event.altKey)
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/** Best-effort macOS detection (browser only; defaults to false off-DOM). */
|
|
53
|
+
export function isMacPlatform() {
|
|
54
|
+
if (typeof navigator === 'undefined')
|
|
55
|
+
return false;
|
|
56
|
+
const platform = navigator.platform ?? '';
|
|
57
|
+
const ua = navigator.userAgent ?? '';
|
|
58
|
+
return /mac|iphone|ipad|ipod/i.test(platform) || /mac/i.test(ua);
|
|
59
|
+
}
|
|
60
|
+
/** Register a set of shortcuts on the editor through one KEY_DOWN handler.
|
|
61
|
+
* Returns a disposer. The first matching shortcut whose `run` returns `true`
|
|
62
|
+
* wins and the event is consumed. */
|
|
63
|
+
export function registerShortcuts(editor, shortcuts) {
|
|
64
|
+
if (shortcuts.length === 0)
|
|
65
|
+
return () => { };
|
|
66
|
+
const parsed = shortcuts.map((s) => ({ spec: s, combo: parseCombo(s.combo) }));
|
|
67
|
+
const mac = isMacPlatform();
|
|
68
|
+
return editor.registerCommand(KEY_DOWN_COMMAND, (event) => {
|
|
69
|
+
for (const { spec, combo } of parsed) {
|
|
70
|
+
if (matchesCombo(event, combo, mac)) {
|
|
71
|
+
if (spec.run(editor)) {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}, COMMAND_PRIORITY_NORMAL);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"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","sourcesContent":["// Keyboard-shortcut wiring: parse normalized chord strings and register them\n// through a single KEY_DOWN command on the editor.\n\nimport { COMMAND_PRIORITY_NORMAL, KEY_DOWN_COMMAND, type LexicalEditor } from 'lexical'\nimport type { ShortcutSpec } from './plugin.js'\n\n/** A parsed chord. `mod` means ⌘ on macOS / Ctrl elsewhere. */\nexport interface ParsedCombo {\n key: string\n mod: boolean\n shift: boolean\n alt: boolean\n ctrl: boolean\n}\n\n/** Parse a chord like `Mod-Shift-7` into its parts. Case-insensitive on\n * modifiers; the final segment is the key (lower-cased for letters). */\nexport function parseCombo(combo: string): ParsedCombo {\n const parts = combo\n .split('-')\n .map((p) => p.trim())\n .filter((p) => p.length > 0)\n const result: ParsedCombo = { key: '', mod: false, shift: false, alt: false, ctrl: false }\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i]!\n const isLast = i === parts.length - 1\n const lower = part.toLowerCase()\n if (!isLast && (lower === 'mod' || lower === 'cmd' || lower === 'meta')) result.mod = true\n else if (!isLast && lower === 'ctrl') result.ctrl = true\n else if (!isLast && lower === 'shift') result.shift = true\n else if (!isLast && (lower === 'alt' || lower === 'option' || lower === 'opt'))\n result.alt = true\n else result.key = part.length === 1 ? part.toLowerCase() : part\n }\n return result\n}\n\n/** Does a keyboard event satisfy a parsed chord? `mod` maps to ⌘ on macOS and\n * Ctrl elsewhere; all declared modifiers must match exactly (no extras). */\nexport function matchesCombo(event: KeyboardEvent, combo: ParsedCombo, isMac: boolean): boolean {\n const eventKey = event.key.length === 1 ? event.key.toLowerCase() : event.key\n if (eventKey !== combo.key) return false\n const modActive = isMac ? event.metaKey : event.ctrlKey\n const otherCtrl = isMac ? event.ctrlKey : event.metaKey\n if (combo.mod !== modActive) return false\n // An explicit `ctrl` on macOS targets the control key specifically.\n if (combo.ctrl !== (isMac ? event.ctrlKey : false)) {\n if (!(combo.mod && !isMac)) return false\n }\n if (!combo.mod && !combo.ctrl && otherCtrl) return false\n if (combo.shift !== event.shiftKey) return false\n if (combo.alt !== event.altKey) return false\n return true\n}\n\n/** Best-effort macOS detection (browser only; defaults to false off-DOM). */\nexport function isMacPlatform(): boolean {\n if (typeof navigator === 'undefined') return false\n const platform = navigator.platform ?? ''\n const ua = navigator.userAgent ?? ''\n return /mac|iphone|ipad|ipod/i.test(platform) || /mac/i.test(ua)\n}\n\n/** Register a set of shortcuts on the editor through one KEY_DOWN handler.\n * Returns a disposer. The first matching shortcut whose `run` returns `true`\n * wins and the event is consumed. */\nexport function registerShortcuts(\n editor: LexicalEditor,\n shortcuts: readonly ShortcutSpec[],\n): () => void {\n if (shortcuts.length === 0) return () => {}\n const parsed = shortcuts.map((s) => ({ spec: s, combo: parseCombo(s.combo) }))\n const mac = isMacPlatform()\n return editor.registerCommand(\n KEY_DOWN_COMMAND,\n (event: KeyboardEvent) => {\n for (const { spec, combo } of parsed) {\n if (matchesCombo(event, combo, mac)) {\n if (spec.run(editor)) {\n event.preventDefault()\n return true\n }\n }\n }\n return false\n },\n COMMAND_PRIORITY_NORMAL,\n )\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type LexicalEditor } from 'lexical';
|
|
2
|
+
/** Block kinds resolvable without list/code packages. Anything else → 'other',
|
|
3
|
+
* which the markdown layer refines (list, code, …). */
|
|
4
|
+
export type BaseBlockType = 'paragraph' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'quote' | 'other';
|
|
5
|
+
export type Alignment = 'left' | 'center' | 'right' | 'justify' | 'start' | 'end' | null;
|
|
6
|
+
/** The generic format surface at the current selection. */
|
|
7
|
+
export interface BaseFormat {
|
|
8
|
+
bold: boolean;
|
|
9
|
+
italic: boolean;
|
|
10
|
+
strikethrough: boolean;
|
|
11
|
+
underline: boolean;
|
|
12
|
+
code: boolean;
|
|
13
|
+
blockType: BaseBlockType;
|
|
14
|
+
alignment: Alignment;
|
|
15
|
+
/** The resolved top-level block element key (lets the markdown layer refine). */
|
|
16
|
+
blockKey: string | null;
|
|
17
|
+
hasSelection: boolean;
|
|
18
|
+
isCollapsed: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Read the base format at the current selection. Must run inside a Lexical
|
|
21
|
+
* read/update context (it calls `$`-prefixed APIs). */
|
|
22
|
+
export declare function $readBaseFormat(): BaseFormat;
|
|
23
|
+
/** Convenience wrapper that opens a read context on `editor`. */
|
|
24
|
+
export declare function readBaseFormat(editor: LexicalEditor): BaseFormat;
|
|
25
|
+
//# sourceMappingURL=selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../src/selection.ts"],"names":[],"mappings":"AAGA,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,SAAS,CAAA;AAGhB;uDACuD;AACvD,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,OAAO,GACP,OAAO,CAAA;AAEX,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,IAAI,CAAA;AAExF,2DAA2D;AAC3D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;IACf,aAAa,EAAE,OAAO,CAAA;IACtB,SAAS,EAAE,OAAO,CAAA;IAClB,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,aAAa,CAAA;IACxB,SAAS,EAAE,SAAS,CAAA;IACpB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,OAAO,CAAA;CACrB;AAkCD;uDACuD;AACvD,wBAAgB,eAAe,IAAI,UAAU,CA8B5C;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,UAAU,CAEhE"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Generic (rich-text level) selection → format reader. `@llui/markdown-editor`
|
|
2
|
+
// composes this with list/code detection to build its full toolbar FormatState.
|
|
3
|
+
import { $getSelection, $isElementNode, $isRangeSelection, $isRootOrShadowRoot, } from 'lexical';
|
|
4
|
+
import { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
|
|
5
|
+
const EMPTY = {
|
|
6
|
+
bold: false,
|
|
7
|
+
italic: false,
|
|
8
|
+
strikethrough: false,
|
|
9
|
+
underline: false,
|
|
10
|
+
code: false,
|
|
11
|
+
blockType: 'paragraph',
|
|
12
|
+
alignment: null,
|
|
13
|
+
blockKey: null,
|
|
14
|
+
hasSelection: false,
|
|
15
|
+
isCollapsed: true,
|
|
16
|
+
};
|
|
17
|
+
function mapAlignment(format) {
|
|
18
|
+
switch (format) {
|
|
19
|
+
case 'left':
|
|
20
|
+
return 'left';
|
|
21
|
+
case 'center':
|
|
22
|
+
return 'center';
|
|
23
|
+
case 'right':
|
|
24
|
+
return 'right';
|
|
25
|
+
case 'justify':
|
|
26
|
+
return 'justify';
|
|
27
|
+
case 'start':
|
|
28
|
+
return 'start';
|
|
29
|
+
case 'end':
|
|
30
|
+
return 'end';
|
|
31
|
+
default:
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Read the base format at the current selection. Must run inside a Lexical
|
|
36
|
+
* read/update context (it calls `$`-prefixed APIs). */
|
|
37
|
+
export function $readBaseFormat() {
|
|
38
|
+
const selection = $getSelection();
|
|
39
|
+
if (!$isRangeSelection(selection))
|
|
40
|
+
return EMPTY;
|
|
41
|
+
const anchorNode = selection.anchor.getNode();
|
|
42
|
+
let blockElement = anchorNode;
|
|
43
|
+
if (!$isRootOrShadowRoot(anchorNode)) {
|
|
44
|
+
const top = anchorNode.getTopLevelElement();
|
|
45
|
+
if (top !== null)
|
|
46
|
+
blockElement = top;
|
|
47
|
+
}
|
|
48
|
+
let blockType = 'other';
|
|
49
|
+
if ($isHeadingNode(blockElement))
|
|
50
|
+
blockType = blockElement.getTag();
|
|
51
|
+
else if ($isQuoteNode(blockElement))
|
|
52
|
+
blockType = 'quote';
|
|
53
|
+
else if (blockElement.getType() === 'paragraph')
|
|
54
|
+
blockType = 'paragraph';
|
|
55
|
+
const alignment = $isElementNode(blockElement) ? mapAlignment(blockElement.getFormatType()) : null;
|
|
56
|
+
return {
|
|
57
|
+
bold: selection.hasFormat('bold'),
|
|
58
|
+
italic: selection.hasFormat('italic'),
|
|
59
|
+
strikethrough: selection.hasFormat('strikethrough'),
|
|
60
|
+
underline: selection.hasFormat('underline'),
|
|
61
|
+
code: selection.hasFormat('code'),
|
|
62
|
+
blockType,
|
|
63
|
+
alignment,
|
|
64
|
+
blockKey: blockElement.getKey(),
|
|
65
|
+
hasSelection: true,
|
|
66
|
+
isCollapsed: selection.isCollapsed(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Convenience wrapper that opens a read context on `editor`. */
|
|
70
|
+
export function readBaseFormat(editor) {
|
|
71
|
+
return editor.getEditorState().read(() => $readBaseFormat());
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection.js","sourceRoot":"","sources":["../src/selection.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,gFAAgF;AAEhF,OAAO,EACL,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,mBAAmB,GAGpB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAgCjE,MAAM,KAAK,GAAe;IACxB,IAAI,EAAE,KAAK;IACX,MAAM,EAAE,KAAK;IACb,aAAa,EAAE,KAAK;IACpB,SAAS,EAAE,KAAK;IAChB,IAAI,EAAE,KAAK;IACX,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,IAAI;IACf,QAAQ,EAAE,IAAI;IACd,YAAY,EAAE,KAAK;IACnB,WAAW,EAAE,IAAI;CAClB,CAAA;AAED,SAAS,YAAY,CAAC,MAAyB;IAC7C,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,MAAM;YACT,OAAO,MAAM,CAAA;QACf,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAA;QACjB,KAAK,OAAO;YACV,OAAO,OAAO,CAAA;QAChB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAA;QAClB,KAAK,OAAO;YACV,OAAO,OAAO,CAAA;QAChB,KAAK,KAAK;YACR,OAAO,KAAK,CAAA;QACd;YACE,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC;AAED;uDACuD;AACvD,MAAM,UAAU,eAAe;IAC7B,MAAM,SAAS,GAAG,aAAa,EAAE,CAAA;IACjC,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAA;IAE/C,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,CAAA;IAC7C,IAAI,YAAY,GAAG,UAAU,CAAA;IAC7B,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAA;QAC3C,IAAI,GAAG,KAAK,IAAI;YAAE,YAAY,GAAG,GAAG,CAAA;IACtC,CAAC;IAED,IAAI,SAAS,GAAkB,OAAO,CAAA;IACtC,IAAI,cAAc,CAAC,YAAY,CAAC;QAAE,SAAS,GAAG,YAAY,CAAC,MAAM,EAAE,CAAA;SAC9D,IAAI,YAAY,CAAC,YAAY,CAAC;QAAE,SAAS,GAAG,OAAO,CAAA;SACnD,IAAI,YAAY,CAAC,OAAO,EAAE,KAAK,WAAW;QAAE,SAAS,GAAG,WAAW,CAAA;IAExE,MAAM,SAAS,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAElG,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC;QACjC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC;QACrC,aAAa,EAAE,SAAS,CAAC,SAAS,CAAC,eAAe,CAAC;QACnD,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC;QAC3C,IAAI,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC;QACjC,SAAS;QACT,SAAS;QACT,QAAQ,EAAE,YAAY,CAAC,MAAM,EAAE;QAC/B,YAAY,EAAE,IAAI;QAClB,WAAW,EAAE,SAAS,CAAC,WAAW,EAAE;KACrC,CAAA;AACH,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,MAAqB;IAClD,OAAO,MAAM,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAA;AAC9D,CAAC","sourcesContent":["// Generic (rich-text level) selection → format reader. `@llui/markdown-editor`\n// composes this with list/code detection to build its full toolbar FormatState.\n\nimport {\n $getSelection,\n $isElementNode,\n $isRangeSelection,\n $isRootOrShadowRoot,\n type ElementFormatType,\n type LexicalEditor,\n} from 'lexical'\nimport { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text'\n\n/** Block kinds resolvable without list/code packages. Anything else → 'other',\n * which the markdown layer refines (list, code, …). */\nexport type BaseBlockType =\n | 'paragraph'\n | 'h1'\n | 'h2'\n | 'h3'\n | 'h4'\n | 'h5'\n | 'h6'\n | 'quote'\n | 'other'\n\nexport type Alignment = 'left' | 'center' | 'right' | 'justify' | 'start' | 'end' | null\n\n/** The generic format surface at the current selection. */\nexport interface BaseFormat {\n bold: boolean\n italic: boolean\n strikethrough: boolean\n underline: boolean\n code: boolean\n blockType: BaseBlockType\n alignment: Alignment\n /** The resolved top-level block element key (lets the markdown layer refine). */\n blockKey: string | null\n hasSelection: boolean\n isCollapsed: boolean\n}\n\nconst EMPTY: BaseFormat = {\n bold: false,\n italic: false,\n strikethrough: false,\n underline: false,\n code: false,\n blockType: 'paragraph',\n alignment: null,\n blockKey: null,\n hasSelection: false,\n isCollapsed: true,\n}\n\nfunction mapAlignment(format: ElementFormatType): Alignment {\n switch (format) {\n case 'left':\n return 'left'\n case 'center':\n return 'center'\n case 'right':\n return 'right'\n case 'justify':\n return 'justify'\n case 'start':\n return 'start'\n case 'end':\n return 'end'\n default:\n return null\n }\n}\n\n/** Read the base format at the current selection. Must run inside a Lexical\n * read/update context (it calls `$`-prefixed APIs). */\nexport function $readBaseFormat(): BaseFormat {\n const selection = $getSelection()\n if (!$isRangeSelection(selection)) return EMPTY\n\n const anchorNode = selection.anchor.getNode()\n let blockElement = anchorNode\n if (!$isRootOrShadowRoot(anchorNode)) {\n const top = anchorNode.getTopLevelElement()\n if (top !== null) blockElement = top\n }\n\n let blockType: BaseBlockType = 'other'\n if ($isHeadingNode(blockElement)) blockType = blockElement.getTag()\n else if ($isQuoteNode(blockElement)) blockType = 'quote'\n else if (blockElement.getType() === 'paragraph') blockType = 'paragraph'\n\n const alignment = $isElementNode(blockElement) ? mapAlignment(blockElement.getFormatType()) : null\n\n return {\n bold: selection.hasFormat('bold'),\n italic: selection.hasFormat('italic'),\n strikethrough: selection.hasFormat('strikethrough'),\n underline: selection.hasFormat('underline'),\n code: selection.hasFormat('code'),\n blockType,\n alignment,\n blockKey: blockElement.getKey(),\n hasSelection: true,\n isCollapsed: selection.isCollapsed(),\n }\n}\n\n/** Convenience wrapper that opens a read context on `editor`. */\nexport function readBaseFormat(editor: LexicalEditor): BaseFormat {\n return editor.getEditorState().read(() => $readBaseFormat())\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@llui/lexical",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"@llui/dom": "^0.9.0",
|
|
15
|
+
"lexical": "^0.45.0",
|
|
16
|
+
"@lexical/history": "^0.45.0",
|
|
17
|
+
"@lexical/rich-text": "^0.45.0",
|
|
18
|
+
"@lexical/selection": "^0.45.0",
|
|
19
|
+
"@lexical/utils": "^0.45.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"lexical": "^0.45.0",
|
|
23
|
+
"@lexical/headless": "^0.45.0",
|
|
24
|
+
"@lexical/history": "^0.45.0",
|
|
25
|
+
"@lexical/rich-text": "^0.45.0",
|
|
26
|
+
"@lexical/selection": "^0.45.0",
|
|
27
|
+
"@lexical/utils": "^0.45.0",
|
|
28
|
+
"typescript": "^6.0.0",
|
|
29
|
+
"vitest": "^4.1.2",
|
|
30
|
+
"@llui/dom": "0.9.0"
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"description": "Low-level binding between Lexical and the LLui signal runtime — mount Lexical via foreign(), plugin contract, and the DecoratorNode ↔ LLui sub-view bridge",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"llui",
|
|
36
|
+
"lexical",
|
|
37
|
+
"editor",
|
|
38
|
+
"signal",
|
|
39
|
+
"tea"
|
|
40
|
+
],
|
|
41
|
+
"author": "Franco Ponticelli <franco.ponticelli@gmail.com>",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/fponticelli/llui.git",
|
|
46
|
+
"directory": "packages/lexical"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/fponticelli/llui/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/fponticelli/llui/tree/main/packages/lexical#readme",
|
|
52
|
+
"files": [
|
|
53
|
+
"dist"
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsc -p tsconfig.build.json",
|
|
57
|
+
"check": "tsc --noEmit -p tsconfig.check.json",
|
|
58
|
+
"lint": "eslint src",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
}
|
|
61
|
+
}
|