@sciflow/editor-start 0.0.1-beta
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/README.md +130 -0
- package/dist/bundle/sciflow-editor.css +1 -0
- package/dist/bundle/sciflow-editor.js +18739 -0
- package/dist/demo/demo.js +56 -0
- package/dist/demo/index.html +169 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/lib/citation-drop.d.ts +25 -0
- package/dist/lib/citation-drop.d.ts.map +1 -0
- package/dist/lib/citation-drop.js +53 -0
- package/dist/lib/citation-source-adapter.d.ts +56 -0
- package/dist/lib/citation-source-adapter.d.ts.map +1 -0
- package/dist/lib/citation-source-adapter.js +126 -0
- package/dist/lib/editor-element.d.ts +256 -0
- package/dist/lib/editor-element.d.ts.map +1 -0
- package/dist/lib/editor-element.js +681 -0
- package/dist/lib/format-bar.d.ts +98 -0
- package/dist/lib/format-bar.d.ts.map +1 -0
- package/dist/lib/format-bar.js +401 -0
- package/dist/lib/outline.d.ts +94 -0
- package/dist/lib/outline.d.ts.map +1 -0
- package/dist/lib/outline.js +491 -0
- package/dist/lib/reference-list.d.ts +58 -0
- package/dist/lib/reference-list.d.ts.map +1 -0
- package/dist/lib/reference-list.js +160 -0
- package/dist/lib/selection-editor.d.ts +94 -0
- package/dist/lib/selection-editor.d.ts.map +1 -0
- package/dist/lib/selection-editor.js +467 -0
- package/dist/lib/theme.d.ts +8 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +72 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +104 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module format-bar
|
|
3
|
+
*
|
|
4
|
+
* Developer overview:
|
|
5
|
+
* --------------------
|
|
6
|
+
* `SciFlowFormatBarElement` is the companion toolbar for `sciflow-editor`.
|
|
7
|
+
* It demonstrates how to consume the command runner exposed by the editor
|
|
8
|
+
* host and how to keep UI state in sync with selection changes.
|
|
9
|
+
*
|
|
10
|
+
* Re-implementing the toolbar in another framework largely boils down to:
|
|
11
|
+
*
|
|
12
|
+
* 1. Call `editor.getCommands()` to obtain the command runner.
|
|
13
|
+
* 2. Query `runner.available()` to enable/disable buttons.
|
|
14
|
+
* 3. Use `runner.flow()` to compose multi-step command sequences.
|
|
15
|
+
* 4. Listen to editor events (selection, ready) to refresh the toolbar UI.
|
|
16
|
+
*
|
|
17
|
+
* The component code below is annotated to map those steps to specific hooks.
|
|
18
|
+
*/
|
|
19
|
+
import { LitElement } from 'lit';
|
|
20
|
+
import type { SciFlowEditorElement } from './editor-element.js';
|
|
21
|
+
/**
|
|
22
|
+
* Custom element that renders the demo formatting toolbar.
|
|
23
|
+
*
|
|
24
|
+
* Responsibilities:
|
|
25
|
+
* - resolve the nearest `sciflow-editor` instance (or accept one via property)
|
|
26
|
+
* - enable/disable controls according to `runner.available()` responses
|
|
27
|
+
* - execute commands using either the immediate API or flow pipelines
|
|
28
|
+
* - surface an image insertion shortcut that exercises the feature command
|
|
29
|
+
*
|
|
30
|
+
* The Lit code mirrors the logic you would implement with other component
|
|
31
|
+
* frameworks—bind to editor events, keep local state, call commands on click.
|
|
32
|
+
*/
|
|
33
|
+
export declare class SciFlowFormatBarElement extends LitElement {
|
|
34
|
+
/** Reference to the editor element. May be passed directly via property binding. */
|
|
35
|
+
editor: SciFlowEditorElement | null;
|
|
36
|
+
/** ID reference to resolve an editor instance declaratively. */
|
|
37
|
+
for?: string;
|
|
38
|
+
private selectionVersion;
|
|
39
|
+
private resolvedEditor;
|
|
40
|
+
private selectionListener?;
|
|
41
|
+
private readyListener?;
|
|
42
|
+
private themeUnsub?;
|
|
43
|
+
private themeStyleElements;
|
|
44
|
+
static styles: import("lit").CSSResult;
|
|
45
|
+
/** Subscribe to editor events once the toolbar attaches to the DOM. */
|
|
46
|
+
connectedCallback(): void;
|
|
47
|
+
/** Remove listeners when the toolbar is detached. */
|
|
48
|
+
disconnectedCallback(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Watch for changes to the `editor`/`for` bindings and reconnect accordingly.
|
|
51
|
+
* Other frameworks can perform the same logic in an effect hook.
|
|
52
|
+
*/
|
|
53
|
+
protected updated(changed: Map<string | number | symbol, unknown>): void;
|
|
54
|
+
/**
|
|
55
|
+
* Render the toolbar. Commands are discovered dynamically through the
|
|
56
|
+
* command runner so the UI stays in sync with the currently loaded features.
|
|
57
|
+
*/
|
|
58
|
+
protected render(): import("lit-html").TemplateResult<1>;
|
|
59
|
+
/**
|
|
60
|
+
* Find the target editor element and attach listeners for selection changes.
|
|
61
|
+
* This mirrors the wiring a framework component would perform in `useEffect`.
|
|
62
|
+
*/
|
|
63
|
+
private resolveEditorReference;
|
|
64
|
+
private resolveEditor;
|
|
65
|
+
/** Clean up listeners attached to the last editor instance. */
|
|
66
|
+
private detachEditorListeners;
|
|
67
|
+
/** Convenience getter mirroring `editor.getCommands()`. */
|
|
68
|
+
private getCommandRunner;
|
|
69
|
+
/** Interpret the block-style dropdown and trigger the relevant commands. */
|
|
70
|
+
private handleStyleChange;
|
|
71
|
+
/** Apply a specific heading level after ensuring the editor can handle it. */
|
|
72
|
+
private handleSetHeading;
|
|
73
|
+
/** Reset the current block back to a paragraph. */
|
|
74
|
+
private handleSetParagraph;
|
|
75
|
+
/** Determine which block style is currently active for the selection. */
|
|
76
|
+
private getActiveBlockStyle;
|
|
77
|
+
/** Check if a style option can be executed against the current editor state. */
|
|
78
|
+
private canUseStyleOption;
|
|
79
|
+
/**
|
|
80
|
+
* Render a set of command buttons that share the same metadata group.
|
|
81
|
+
* Groups are sorted by each command's declared order so feature authors control layout.
|
|
82
|
+
*/
|
|
83
|
+
private renderCommandGroup;
|
|
84
|
+
/**
|
|
85
|
+
* Render a single command button, wiring up availability checks and active state handling.
|
|
86
|
+
*/
|
|
87
|
+
private renderCommandButton;
|
|
88
|
+
/**
|
|
89
|
+
* Determine which groups should be displayed and in which order based on command metadata.
|
|
90
|
+
*/
|
|
91
|
+
private getCommandGroups;
|
|
92
|
+
}
|
|
93
|
+
declare global {
|
|
94
|
+
interface HTMLElementTagNameMap {
|
|
95
|
+
'sciflow-formatbar': SciFlowFormatBarElement;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=format-bar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-bar.d.ts","sourceRoot":"","sources":["../../src/lib/format-bar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAa,UAAU,EAAa,MAAM,KAAK,CAAC;AAKvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAoBhE;;;;;;;;;;;GAWG;AACH,qBACa,uBAAwB,SAAQ,UAAU;IACrD,oFAAoF;IAEpF,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAAQ;IAE3C,gEAAgE;IAEhE,GAAG,CAAC,EAAE,MAAM,CAAC;IAGb,OAAO,CAAC,gBAAgB,CAAK;IAE7B,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,iBAAiB,CAAC,CAAgB;IAC1C,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,kBAAkB,CAA0B;IAEpD,OAAgB,MAAM,0BAAmC;IAEzD,uEAAuE;IAC9D,iBAAiB,IAAI,IAAI;IAYlC,qDAAqD;IAC5C,oBAAoB,IAAI,IAAI;IAOrC;;;OAGG;cACgB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAMjF;;;OAGG;cACgB,MAAM;IAiDzB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IA+B9B,OAAO,CAAC,aAAa;IAkBrB,+DAA+D;IAC/D,OAAO,CAAC,qBAAqB;IAc7B,2DAA2D;IAC3D,OAAO,CAAC,gBAAgB;IAIxB,4EAA4E;IAC5E,OAAO,CAAC,iBAAiB;IAiBzB,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;IA6BxB,mDAAmD;IACnD,OAAO,CAAC,kBAAkB;IA6B1B,yEAAyE;IACzE,OAAO,CAAC,mBAAmB;IA6C3B,gFAAgF;IAChF,OAAO,CAAC,iBAAiB;IAmBzB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuC3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;CA2BzB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,mBAAmB,EAAE,uBAAuB,CAAC;KAC9C;CACF"}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
/**
|
|
3
|
+
* @module format-bar
|
|
4
|
+
*
|
|
5
|
+
* Developer overview:
|
|
6
|
+
* --------------------
|
|
7
|
+
* `SciFlowFormatBarElement` is the companion toolbar for `sciflow-editor`.
|
|
8
|
+
* It demonstrates how to consume the command runner exposed by the editor
|
|
9
|
+
* host and how to keep UI state in sync with selection changes.
|
|
10
|
+
*
|
|
11
|
+
* Re-implementing the toolbar in another framework largely boils down to:
|
|
12
|
+
*
|
|
13
|
+
* 1. Call `editor.getCommands()` to obtain the command runner.
|
|
14
|
+
* 2. Query `runner.available()` to enable/disable buttons.
|
|
15
|
+
* 3. Use `runner.flow()` to compose multi-step command sequences.
|
|
16
|
+
* 4. Listen to editor events (selection, ready) to refresh the toolbar UI.
|
|
17
|
+
*
|
|
18
|
+
* The component code below is annotated to map those steps to specific hooks.
|
|
19
|
+
*/
|
|
20
|
+
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
21
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
22
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
23
|
+
import formatBarCss from './format-bar.css?inline';
|
|
24
|
+
import { applyThemeStylesToRoot, subscribeToSciFlowTheme } from './theme.js';
|
|
25
|
+
const STYLE_OPTIONS = [
|
|
26
|
+
{ label: 'Paragraph', value: 'paragraph' },
|
|
27
|
+
{ label: 'Heading 1', value: 'heading-1', level: 1 },
|
|
28
|
+
{ label: 'Heading 2', value: 'heading-2', level: 2 },
|
|
29
|
+
{ label: 'Heading 3', value: 'heading-3', level: 3 },
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Custom element that renders the demo formatting toolbar.
|
|
33
|
+
*
|
|
34
|
+
* Responsibilities:
|
|
35
|
+
* - resolve the nearest `sciflow-editor` instance (or accept one via property)
|
|
36
|
+
* - enable/disable controls according to `runner.available()` responses
|
|
37
|
+
* - execute commands using either the immediate API or flow pipelines
|
|
38
|
+
* - surface an image insertion shortcut that exercises the feature command
|
|
39
|
+
*
|
|
40
|
+
* The Lit code mirrors the logic you would implement with other component
|
|
41
|
+
* frameworks—bind to editor events, keep local state, call commands on click.
|
|
42
|
+
*/
|
|
43
|
+
let SciFlowFormatBarElement = class SciFlowFormatBarElement extends LitElement {
|
|
44
|
+
constructor() {
|
|
45
|
+
super(...arguments);
|
|
46
|
+
/** Reference to the editor element. May be passed directly via property binding. */
|
|
47
|
+
this.editor = null;
|
|
48
|
+
this.selectionVersion = 0;
|
|
49
|
+
this.resolvedEditor = null;
|
|
50
|
+
this.themeStyleElements = [];
|
|
51
|
+
}
|
|
52
|
+
static { this.styles = css `${unsafeCSS(formatBarCss)}`; }
|
|
53
|
+
/** Subscribe to editor events once the toolbar attaches to the DOM. */
|
|
54
|
+
connectedCallback() {
|
|
55
|
+
super.connectedCallback();
|
|
56
|
+
this.themeUnsub = subscribeToSciFlowTheme((cssTexts) => {
|
|
57
|
+
this.themeStyleElements = applyThemeStylesToRoot(this.renderRoot, this.themeStyleElements, cssTexts);
|
|
58
|
+
});
|
|
59
|
+
this.resolveEditorReference();
|
|
60
|
+
}
|
|
61
|
+
/** Remove listeners when the toolbar is detached. */
|
|
62
|
+
disconnectedCallback() {
|
|
63
|
+
this.detachEditorListeners();
|
|
64
|
+
this.themeUnsub?.();
|
|
65
|
+
this.themeUnsub = undefined;
|
|
66
|
+
super.disconnectedCallback();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Watch for changes to the `editor`/`for` bindings and reconnect accordingly.
|
|
70
|
+
* Other frameworks can perform the same logic in an effect hook.
|
|
71
|
+
*/
|
|
72
|
+
updated(changed) {
|
|
73
|
+
if (changed.has('for') || changed.has('editor')) {
|
|
74
|
+
this.resolveEditorReference();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Render the toolbar. Commands are discovered dynamically through the
|
|
79
|
+
* command runner so the UI stays in sync with the currently loaded features.
|
|
80
|
+
*/
|
|
81
|
+
render() {
|
|
82
|
+
const runner = this.getCommandRunner();
|
|
83
|
+
const availableCommands = runner?.available?.() ?? null;
|
|
84
|
+
const headingCommandsAvailable = typeof availableCommands?.setHeading === 'function' || typeof availableCommands?.setParagraph === 'function';
|
|
85
|
+
const activeStyle = this.getActiveBlockStyle();
|
|
86
|
+
const selectDisabled = !runner || !headingCommandsAvailable;
|
|
87
|
+
const commandMetadata = runner?.commandsMeta?.() ?? {};
|
|
88
|
+
const commandGroups = this.getCommandGroups(commandMetadata);
|
|
89
|
+
const tableGroup = 'table';
|
|
90
|
+
const primaryGroups = commandGroups.filter((group) => group !== tableGroup);
|
|
91
|
+
const tableGroups = commandGroups.filter((group) => group === tableGroup);
|
|
92
|
+
return html `
|
|
93
|
+
<select
|
|
94
|
+
class="style-select"
|
|
95
|
+
aria-label="Text style"
|
|
96
|
+
.value=${activeStyle}
|
|
97
|
+
?disabled=${selectDisabled}
|
|
98
|
+
@change=${this.handleStyleChange}
|
|
99
|
+
>
|
|
100
|
+
${STYLE_OPTIONS.map((option) => html `
|
|
101
|
+
<option
|
|
102
|
+
value=${option.value}
|
|
103
|
+
?selected=${option.value === activeStyle}
|
|
104
|
+
?disabled=${!this.canUseStyleOption(option, availableCommands)}
|
|
105
|
+
>
|
|
106
|
+
${option.label}
|
|
107
|
+
</option>
|
|
108
|
+
`)}
|
|
109
|
+
</select>
|
|
110
|
+
<div class="command-rows">
|
|
111
|
+
${primaryGroups.length
|
|
112
|
+
? html `<div class="command-row">
|
|
113
|
+
${primaryGroups.map((group) => this.renderCommandGroup(runner, commandMetadata, group))}
|
|
114
|
+
</div>`
|
|
115
|
+
: null}
|
|
116
|
+
${tableGroups.length
|
|
117
|
+
? html `<div class="command-row table-row">
|
|
118
|
+
${tableGroups.map((group) => this.renderCommandGroup(runner, commandMetadata, group))}
|
|
119
|
+
</div>`
|
|
120
|
+
: null}
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Find the target editor element and attach listeners for selection changes.
|
|
126
|
+
* This mirrors the wiring a framework component would perform in `useEffect`.
|
|
127
|
+
*/
|
|
128
|
+
resolveEditorReference() {
|
|
129
|
+
const previous = this.resolvedEditor;
|
|
130
|
+
const next = this.resolveEditor();
|
|
131
|
+
if (previous === next && this.selectionListener) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.detachEditorListeners();
|
|
135
|
+
this.resolvedEditor = next;
|
|
136
|
+
if (!next) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.selectionListener = () => {
|
|
140
|
+
this.selectionVersion++;
|
|
141
|
+
this.requestUpdate();
|
|
142
|
+
};
|
|
143
|
+
this.readyListener = () => {
|
|
144
|
+
this.selectionVersion++;
|
|
145
|
+
this.requestUpdate();
|
|
146
|
+
};
|
|
147
|
+
next.addEventListener('editor-selection-change', this.selectionListener);
|
|
148
|
+
next.addEventListener('editor-ready', this.readyListener);
|
|
149
|
+
// Prompt initial render state.
|
|
150
|
+
this.selectionVersion++;
|
|
151
|
+
this.requestUpdate();
|
|
152
|
+
}
|
|
153
|
+
resolveEditor() {
|
|
154
|
+
if (this.editor) {
|
|
155
|
+
return this.editor;
|
|
156
|
+
}
|
|
157
|
+
// When using declarative binding, walk the DOM to locate the target editor.
|
|
158
|
+
if (this.for) {
|
|
159
|
+
const root = this.getRootNode();
|
|
160
|
+
const candidate = root.getElementById(this.for);
|
|
161
|
+
if (candidate) {
|
|
162
|
+
return candidate;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const nearest = this.closest('sciflow-editor');
|
|
166
|
+
if (nearest) {
|
|
167
|
+
return nearest;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
/** Clean up listeners attached to the last editor instance. */
|
|
172
|
+
detachEditorListeners() {
|
|
173
|
+
if (this.resolvedEditor) {
|
|
174
|
+
if (this.selectionListener) {
|
|
175
|
+
this.resolvedEditor.removeEventListener('editor-selection-change', this.selectionListener);
|
|
176
|
+
}
|
|
177
|
+
if (this.readyListener) {
|
|
178
|
+
this.resolvedEditor.removeEventListener('editor-ready', this.readyListener);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
this.selectionListener = undefined;
|
|
182
|
+
this.readyListener = undefined;
|
|
183
|
+
this.resolvedEditor = null;
|
|
184
|
+
}
|
|
185
|
+
/** Convenience getter mirroring `editor.getCommands()`. */
|
|
186
|
+
getCommandRunner() {
|
|
187
|
+
return this.resolvedEditor?.commands ?? null;
|
|
188
|
+
}
|
|
189
|
+
/** Interpret the block-style dropdown and trigger the relevant commands. */
|
|
190
|
+
handleStyleChange(event) {
|
|
191
|
+
const select = event.target;
|
|
192
|
+
const value = select.value;
|
|
193
|
+
if (value === 'paragraph') {
|
|
194
|
+
this.handleSetParagraph();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const parsed = Number.parseInt(value.split('-')[1] ?? '', 10);
|
|
198
|
+
if (!Number.isInteger(parsed)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.handleSetHeading(parsed);
|
|
202
|
+
}
|
|
203
|
+
/** Apply a specific heading level after ensuring the editor can handle it. */
|
|
204
|
+
handleSetHeading(level) {
|
|
205
|
+
const runner = this.getCommandRunner();
|
|
206
|
+
if (!runner) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const available = runner.available?.();
|
|
210
|
+
if (available?.setHeading && !available.setHeading(level)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const flow = runner.flow?.();
|
|
214
|
+
if (flow) {
|
|
215
|
+
const chain = typeof flow.focus === 'function' ? flow.focus() : flow;
|
|
216
|
+
const next = chain.setHeading?.(level) ?? null;
|
|
217
|
+
if (next && typeof next.run === 'function' && next.run()) {
|
|
218
|
+
this.selectionVersion++;
|
|
219
|
+
this.requestUpdate();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const immediate = runner.commands?.setHeading;
|
|
224
|
+
if (typeof immediate === 'function' && immediate(level)) {
|
|
225
|
+
this.selectionVersion++;
|
|
226
|
+
this.requestUpdate();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Reset the current block back to a paragraph. */
|
|
230
|
+
handleSetParagraph() {
|
|
231
|
+
const runner = this.getCommandRunner();
|
|
232
|
+
if (!runner) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const available = runner.available?.();
|
|
236
|
+
if (available?.setParagraph && !available.setParagraph()) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const flow = runner.flow?.();
|
|
240
|
+
if (flow) {
|
|
241
|
+
const chain = typeof flow.focus === 'function' ? flow.focus() : flow;
|
|
242
|
+
const next = chain.setParagraph?.() ?? null;
|
|
243
|
+
if (next && typeof next.run === 'function' && next.run()) {
|
|
244
|
+
this.selectionVersion++;
|
|
245
|
+
this.requestUpdate();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const immediate = runner.commands?.setParagraph;
|
|
250
|
+
if (typeof immediate === 'function' && immediate()) {
|
|
251
|
+
this.selectionVersion++;
|
|
252
|
+
this.requestUpdate();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Determine which block style is currently active for the selection. */
|
|
256
|
+
getActiveBlockStyle() {
|
|
257
|
+
const view = this.resolvedEditor?.editorView;
|
|
258
|
+
if (!view) {
|
|
259
|
+
return 'paragraph';
|
|
260
|
+
}
|
|
261
|
+
const { state } = view;
|
|
262
|
+
const headingType = state.schema.nodes.heading;
|
|
263
|
+
if (!headingType) {
|
|
264
|
+
return 'paragraph';
|
|
265
|
+
}
|
|
266
|
+
const { from, to, empty, $from } = state.selection;
|
|
267
|
+
let detectedLevel = null;
|
|
268
|
+
state.doc.nodesBetween(from, to, (node) => {
|
|
269
|
+
if (node.type === headingType) {
|
|
270
|
+
const level = Number(node.attrs?.level);
|
|
271
|
+
if (Number.isInteger(level) && level >= 1 && level <= 6) {
|
|
272
|
+
detectedLevel = level;
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
});
|
|
278
|
+
if (!detectedLevel && empty && $from.parent.type === headingType) {
|
|
279
|
+
const level = Number($from.parent.attrs?.level);
|
|
280
|
+
if (Number.isInteger(level) && level >= 1 && level <= 6) {
|
|
281
|
+
detectedLevel = level;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
switch (detectedLevel) {
|
|
285
|
+
case 1:
|
|
286
|
+
return 'heading-1';
|
|
287
|
+
case 2:
|
|
288
|
+
return 'heading-2';
|
|
289
|
+
case 3:
|
|
290
|
+
return 'heading-3';
|
|
291
|
+
default:
|
|
292
|
+
return 'paragraph';
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** Check if a style option can be executed against the current editor state. */
|
|
296
|
+
canUseStyleOption(option, commands) {
|
|
297
|
+
if (!commands) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
if (option.value === 'paragraph') {
|
|
301
|
+
return commands.setParagraph?.() ?? false;
|
|
302
|
+
}
|
|
303
|
+
const level = option.level;
|
|
304
|
+
if (!level) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
return commands.setHeading?.(level) ?? false;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Render a set of command buttons that share the same metadata group.
|
|
311
|
+
* Groups are sorted by each command's declared order so feature authors control layout.
|
|
312
|
+
*/
|
|
313
|
+
renderCommandGroup(runner, metadata, group) {
|
|
314
|
+
return Object.entries(metadata)
|
|
315
|
+
.filter(([, meta]) => meta?.group === group && (meta.icon || meta.label))
|
|
316
|
+
.sort(([, a], [, b]) => (a?.order ?? 0) - (b?.order ?? 0))
|
|
317
|
+
.map(([name, meta]) => this.renderCommandButton(name, meta, runner));
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Render a single command button, wiring up availability checks and active state handling.
|
|
321
|
+
*/
|
|
322
|
+
renderCommandButton(name, meta, runner) {
|
|
323
|
+
const command = runner?.commands?.[name];
|
|
324
|
+
if (typeof command !== 'function') {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const availability = runner?.available?.();
|
|
328
|
+
const canExecute = availability?.[name] ? availability[name]() : true;
|
|
329
|
+
const view = this.resolvedEditor?.editorView ?? null;
|
|
330
|
+
const state = view?.state ?? null;
|
|
331
|
+
const isActive = state && meta.isActive ? meta.isActive(state, view) : false;
|
|
332
|
+
const handleClick = () => {
|
|
333
|
+
if (!canExecute) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const result = command();
|
|
337
|
+
if (result) {
|
|
338
|
+
this.selectionVersion++;
|
|
339
|
+
this.requestUpdate();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
return html `
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
class=${classMap({ active: isActive })}
|
|
346
|
+
aria-label=${meta.label ?? name}
|
|
347
|
+
title=${meta.description ?? meta.label ?? name}
|
|
348
|
+
?aria-pressed=${meta.isActive ? (isActive ? 'true' : 'false') : undefined}
|
|
349
|
+
?disabled=${!canExecute}
|
|
350
|
+
data-tooltip=${meta.description ?? meta.label ?? name}
|
|
351
|
+
@click=${handleClick}
|
|
352
|
+
>
|
|
353
|
+
${meta.icon
|
|
354
|
+
? html `<span class="material-symbols-rounded" aria-hidden="true">${meta.icon}</span>`
|
|
355
|
+
: meta.label ?? name}
|
|
356
|
+
</button>
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Determine which groups should be displayed and in which order based on command metadata.
|
|
361
|
+
*/
|
|
362
|
+
getCommandGroups(metadata) {
|
|
363
|
+
const groupOrder = new Map();
|
|
364
|
+
Object.values(metadata).forEach((meta) => {
|
|
365
|
+
if (!meta?.group) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const currentOrder = groupOrder.get(meta.group);
|
|
369
|
+
const candidateOrder = meta.order ?? Number.MAX_SAFE_INTEGER;
|
|
370
|
+
if (typeof currentOrder === 'number') {
|
|
371
|
+
if (candidateOrder < currentOrder) {
|
|
372
|
+
groupOrder.set(meta.group, candidateOrder);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
groupOrder.set(meta.group, candidateOrder);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
return Array.from(groupOrder.entries())
|
|
380
|
+
.sort((a, b) => {
|
|
381
|
+
if (a[1] === b[1]) {
|
|
382
|
+
return a[0].localeCompare(b[0]);
|
|
383
|
+
}
|
|
384
|
+
return a[1] - b[1];
|
|
385
|
+
})
|
|
386
|
+
.map(([group]) => group);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
__decorate([
|
|
390
|
+
property({ attribute: false })
|
|
391
|
+
], SciFlowFormatBarElement.prototype, "editor", void 0);
|
|
392
|
+
__decorate([
|
|
393
|
+
property({ type: String, attribute: 'for' })
|
|
394
|
+
], SciFlowFormatBarElement.prototype, "for", void 0);
|
|
395
|
+
__decorate([
|
|
396
|
+
state()
|
|
397
|
+
], SciFlowFormatBarElement.prototype, "selectionVersion", void 0);
|
|
398
|
+
SciFlowFormatBarElement = __decorate([
|
|
399
|
+
customElement('sciflow-formatbar')
|
|
400
|
+
], SciFlowFormatBarElement);
|
|
401
|
+
export { SciFlowFormatBarElement };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import type { Node as ProsemirrorNode } from 'prosemirror-model';
|
|
3
|
+
import { type SciFlowDocJSON } from '@sciflow/editor-core';
|
|
4
|
+
import type { SciFlowEditorElement } from './editor-element.js';
|
|
5
|
+
export type OutlineHeading = {
|
|
6
|
+
text: string;
|
|
7
|
+
level: number | null;
|
|
8
|
+
id: string | null;
|
|
9
|
+
position: number | null;
|
|
10
|
+
end: number | null;
|
|
11
|
+
};
|
|
12
|
+
export type OutlineCitation = {
|
|
13
|
+
id: string | null;
|
|
14
|
+
source: Array<Record<string, unknown>>;
|
|
15
|
+
};
|
|
16
|
+
export type DocumentOutline = {
|
|
17
|
+
headings: OutlineHeading[];
|
|
18
|
+
citations: OutlineCitation[];
|
|
19
|
+
};
|
|
20
|
+
export type OutlineClickBehavior = 'none' | 'select' | 'scroll';
|
|
21
|
+
/**
|
|
22
|
+
* Collect heading and citation metadata from a SciFlow document snapshot.
|
|
23
|
+
* When a ProseMirror document is available, positions and node boundaries are
|
|
24
|
+
* included so navigation and selection syncing can be precise.
|
|
25
|
+
*/
|
|
26
|
+
export declare function collectDocumentOutline(doc: SciFlowDocJSON | null | undefined, pmDoc?: ProsemirrorNode | null): DocumentOutline;
|
|
27
|
+
/**
|
|
28
|
+
* Helper used by the demo to expose outline data for debugging.
|
|
29
|
+
*/
|
|
30
|
+
export declare function logDocumentOutline(doc: SciFlowDocJSON | null | undefined, pmDoc?: ProsemirrorNode | null): DocumentOutline;
|
|
31
|
+
/**
|
|
32
|
+
* Web component that renders a document outline and stays in sync with a
|
|
33
|
+
* <sciflow-editor> instance. Hosts can either set the `for` attribute to point
|
|
34
|
+
* to an editor ID or pass the editor reference directly via the `editor`
|
|
35
|
+
* property.
|
|
36
|
+
*
|
|
37
|
+
* Configurable behaviors:
|
|
38
|
+
* - `levels` attribute filters which heading levels appear (e.g. "1-3" or "1,2")
|
|
39
|
+
* - `click-behavior` toggles navigation (`none` | `select` | `scroll`)
|
|
40
|
+
* - `renderHeadingLabel` property accepts a custom label renderer
|
|
41
|
+
*/
|
|
42
|
+
export declare class SciFlowOutlineElement extends LitElement {
|
|
43
|
+
/** Reference to the editor element. May be passed directly via property binding. */
|
|
44
|
+
editor: SciFlowEditorElement | null;
|
|
45
|
+
/** ID reference to resolve an editor instance declaratively. */
|
|
46
|
+
for?: string;
|
|
47
|
+
/** Optional document JSON source when not listening to an editor element. */
|
|
48
|
+
doc: SciFlowDocJSON | null;
|
|
49
|
+
/** Placeholder text displayed when no headings are present. */
|
|
50
|
+
emptyText: string;
|
|
51
|
+
/** Controls click behavior: disabled, set selection, or set selection + scroll. */
|
|
52
|
+
clickBehavior: OutlineClickBehavior;
|
|
53
|
+
/** Filter the outline to specific heading levels (e.g., "1-3" or "1,2,3"). */
|
|
54
|
+
levels: number[] | null;
|
|
55
|
+
/** Custom label renderer for headings. Receives the heading and its index. */
|
|
56
|
+
renderHeadingLabel?: (heading: OutlineHeading, index: number) => unknown;
|
|
57
|
+
private headings;
|
|
58
|
+
private activeHeadingKey;
|
|
59
|
+
private resolvedEditor;
|
|
60
|
+
private selectionListener?;
|
|
61
|
+
private changeListener?;
|
|
62
|
+
private readyListener?;
|
|
63
|
+
private lastSelection;
|
|
64
|
+
private themeUnsub?;
|
|
65
|
+
private themeStyleElements;
|
|
66
|
+
static styles: import("lit").CSSResult;
|
|
67
|
+
connectedCallback(): void;
|
|
68
|
+
disconnectedCallback(): void;
|
|
69
|
+
protected updated(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
70
|
+
protected render(): import("lit-html").TemplateResult<1>;
|
|
71
|
+
private renderHeadingItem;
|
|
72
|
+
private handleHeadingKeydown;
|
|
73
|
+
private handleHeadingClick;
|
|
74
|
+
private resolveEditorReference;
|
|
75
|
+
private resolveEditor;
|
|
76
|
+
private attachEditorListeners;
|
|
77
|
+
private detachEditorListeners;
|
|
78
|
+
private updateHeadingsFromDoc;
|
|
79
|
+
private applyLevelFilter;
|
|
80
|
+
private getLevelFilterSet;
|
|
81
|
+
private updateActiveHeading;
|
|
82
|
+
private findHeadingForSelection;
|
|
83
|
+
private headingKey;
|
|
84
|
+
private getHeadingSelectionPosition;
|
|
85
|
+
private extractDocFromEditor;
|
|
86
|
+
private normalizeClickBehavior;
|
|
87
|
+
private dispatchOutlineChange;
|
|
88
|
+
}
|
|
89
|
+
declare global {
|
|
90
|
+
interface HTMLElementTagNameMap {
|
|
91
|
+
'sciflow-outline': SciFlowOutlineElement;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=outline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../src/lib/outline.ts"],"names":[],"mappings":"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"}
|