@sciflow/editor-start 0.0.1-beta → 0.0.2
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 +83 -2
- package/THIRD_PARTY_LICENSES +9 -0
- package/bin/extract-translations.mjs +54 -0
- package/dist/bundle/sciflow-editor.css +1 -1
- package/dist/bundle/sciflow-editor.js +10590 -15324
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/lib/citation-source-adapter.d.ts +22 -2
- package/dist/lib/citation-source-adapter.d.ts.map +1 -1
- package/dist/lib/citation-source-adapter.js +104 -48
- package/dist/lib/drag-preview.d.ts +13 -0
- package/dist/lib/drag-preview.d.ts.map +1 -0
- package/dist/lib/drag-preview.js +43 -0
- package/dist/lib/editor-element.d.ts +30 -4
- package/dist/lib/editor-element.d.ts.map +1 -1
- package/dist/lib/editor-element.js +131 -30
- package/dist/lib/format-bar.d.ts +26 -2
- package/dist/lib/format-bar.d.ts.map +1 -1
- package/dist/lib/format-bar.js +369 -56
- package/dist/lib/ghost-cursor.d.ts +10 -0
- package/dist/lib/ghost-cursor.d.ts.map +1 -0
- package/dist/lib/ghost-cursor.js +59 -0
- package/dist/lib/icons-generated.d.ts +14 -0
- package/dist/lib/icons-generated.d.ts.map +1 -0
- package/dist/lib/icons-generated.js +60 -0
- package/dist/lib/outline.d.ts +18 -10
- package/dist/lib/outline.d.ts.map +1 -1
- package/dist/lib/outline.js +184 -28
- package/dist/lib/reference-list.d.ts +10 -2
- package/dist/lib/reference-list.d.ts.map +1 -1
- package/dist/lib/reference-list.js +47 -9
- package/dist/lib/selection-editor.d.ts +83 -5
- package/dist/lib/selection-editor.d.ts.map +1 -1
- package/dist/lib/selection-editor.js +1225 -147
- package/dist/lib/selection-transform-registry.d.ts +16 -0
- package/dist/lib/selection-transform-registry.d.ts.map +1 -0
- package/dist/lib/selection-transform-registry.js +24 -0
- package/dist/lib/selection-ui-renderer.d.ts +22 -0
- package/dist/lib/selection-ui-renderer.d.ts.map +1 -0
- package/dist/lib/selection-ui-renderer.js +244 -0
- package/dist/lib/selection-widget-host.d.ts +67 -0
- package/dist/lib/selection-widget-host.d.ts.map +1 -0
- package/dist/lib/selection-widget-host.js +151 -0
- package/dist/lib/selection-widget-registry.d.ts +16 -0
- package/dist/lib/selection-widget-registry.d.ts.map +1 -0
- package/dist/lib/selection-widget-registry.js +30 -0
- package/dist/lib/test-fixtures/soak-documents.d.ts +13 -0
- package/dist/lib/test-fixtures/soak-documents.d.ts.map +1 -0
- package/dist/lib/test-fixtures/soak-documents.js +69 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +22 -13
- package/dist/demo/demo.js +0 -56
- package/dist/demo/index.html +0 -169
- package/dist/lib/citation-drop.d.ts +0 -25
- package/dist/lib/citation-drop.d.ts.map +0 -1
- package/dist/lib/citation-drop.js +0 -53
|
@@ -16,8 +16,18 @@ import { __decorate } from "tslib";
|
|
|
16
16
|
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
17
17
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
18
18
|
import { ref } from 'lit/directives/ref.js';
|
|
19
|
-
import { SourceField } from '@sciflow/editor-core';
|
|
19
|
+
import { SourceField, t, LOCALE_CHANGE_EVENT } from '@sciflow/editor-core';
|
|
20
|
+
import { getMarkRange } from '@sciflow/editor-core/commands';
|
|
21
|
+
import { renderTexToSvg } from '@sciflow/editor-core/features/math';
|
|
22
|
+
import { Fragment as PMFragment } from 'prosemirror-model';
|
|
23
|
+
import { EditorState, NodeSelection } from 'prosemirror-state';
|
|
24
|
+
import { EditorView } from 'prosemirror-view';
|
|
25
|
+
import { baseKeymap, toggleMark } from 'prosemirror-commands';
|
|
26
|
+
import { keymap } from 'prosemirror-keymap';
|
|
20
27
|
import { getCitationSourceAdapter, } from './citation-source-adapter.js';
|
|
28
|
+
import { collectDocumentOutline } from './outline.js';
|
|
29
|
+
import { renderNodeFromUiSchema } from './selection-ui-renderer.js';
|
|
30
|
+
import './selection-widget-host.js';
|
|
21
31
|
import selectionEditorCss from './selection-editor.css?inline';
|
|
22
32
|
import { applyThemeStylesToRoot, subscribeToSciFlowTheme } from './theme.js';
|
|
23
33
|
/**
|
|
@@ -37,13 +47,37 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
37
47
|
/** Custom adapter for editing citation source. When unset, uses the global adapter. */
|
|
38
48
|
this.citationSourceAdapter = null;
|
|
39
49
|
this.elementInfo = null;
|
|
50
|
+
/** Draft values for math sidebar (tex, style, label). Synced from node attrs when math is selected. */
|
|
51
|
+
this.mathDraft = null;
|
|
52
|
+
/** Last preview result for math (error/warnings). SVG is rendered into mathPreviewContainer. */
|
|
53
|
+
this.mathPreviewResult = null;
|
|
54
|
+
/** Draft values for citation sidebar (content + source). Applied only on Update. */
|
|
55
|
+
this.citationDraft = null;
|
|
56
|
+
/** Draft values for cross-reference sidebar (display text + target href). */
|
|
57
|
+
this.crossReferenceDraft = null;
|
|
58
|
+
/** Draft values for hyperlink (anchor mark) sidebar. */
|
|
59
|
+
this.hyperlinkDraft = null;
|
|
60
|
+
this.draftAttrs = null;
|
|
61
|
+
this.draftDirty = false;
|
|
62
|
+
this.pinSelection = false;
|
|
63
|
+
this.showAllAttrs = false;
|
|
40
64
|
this.resolvedEditor = null;
|
|
41
65
|
this.themeStyleElements = [];
|
|
42
66
|
this.citationSourceContainer = null;
|
|
67
|
+
this.mathPreviewContainer = null;
|
|
68
|
+
this.mathPreviewDebounce = null;
|
|
69
|
+
this.citationContentEditorContainer = null;
|
|
70
|
+
this.citationContentView = null;
|
|
71
|
+
this.citationContentEditorPosition = null;
|
|
72
|
+
this.selectionDebounce = null;
|
|
73
|
+
this.outlineTargetOptionsCacheDoc = null;
|
|
74
|
+
this.outlineTargetOptionsCache = [];
|
|
43
75
|
}
|
|
44
76
|
static { this.styles = css `${unsafeCSS(selectionEditorCss)}`; }
|
|
45
77
|
connectedCallback() {
|
|
46
78
|
super.connectedCallback();
|
|
79
|
+
this.localeListener = () => this.requestUpdate();
|
|
80
|
+
document.addEventListener(LOCALE_CHANGE_EVENT, this.localeListener);
|
|
47
81
|
this.themeUnsub = subscribeToSciFlowTheme((cssTexts) => {
|
|
48
82
|
this.themeStyleElements = applyThemeStylesToRoot(this.renderRoot, this.themeStyleElements, cssTexts);
|
|
49
83
|
});
|
|
@@ -51,7 +85,16 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
51
85
|
}
|
|
52
86
|
disconnectedCallback() {
|
|
53
87
|
super.disconnectedCallback();
|
|
88
|
+
this.destroyCitationContentView();
|
|
54
89
|
this.detachEditorListeners();
|
|
90
|
+
if (this.selectionDebounce) {
|
|
91
|
+
clearTimeout(this.selectionDebounce);
|
|
92
|
+
this.selectionDebounce = null;
|
|
93
|
+
}
|
|
94
|
+
if (this.localeListener) {
|
|
95
|
+
document.removeEventListener(LOCALE_CHANGE_EVENT, this.localeListener);
|
|
96
|
+
this.localeListener = undefined;
|
|
97
|
+
}
|
|
55
98
|
this.themeUnsub?.();
|
|
56
99
|
this.themeUnsub = undefined;
|
|
57
100
|
}
|
|
@@ -60,92 +103,216 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
60
103
|
if (changedProperties.has('editor') || changedProperties.has('for')) {
|
|
61
104
|
this.resolveEditorReference();
|
|
62
105
|
}
|
|
63
|
-
if (changedProperties.has('elementInfo')
|
|
64
|
-
this.
|
|
65
|
-
this.elementInfo?.type === '
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
if (changedProperties.has('elementInfo')) {
|
|
107
|
+
this.showAllAttrs = false;
|
|
108
|
+
if (this.elementInfo?.type === 'math') {
|
|
109
|
+
const attrs = this.elementInfo.attrs;
|
|
110
|
+
this.mathDraft = {
|
|
111
|
+
tex: attrs?.tex ?? '',
|
|
112
|
+
style: attrs?.style === 'display' ? 'display' : 'inline',
|
|
113
|
+
label: attrs?.label ?? '',
|
|
114
|
+
};
|
|
115
|
+
this.scheduleMathPreview();
|
|
116
|
+
this.citationDraft = null;
|
|
117
|
+
this.crossReferenceDraft = null;
|
|
118
|
+
this.resetDraftAttrs();
|
|
119
|
+
}
|
|
120
|
+
else if (this.elementInfo?.type === 'citation') {
|
|
121
|
+
const attrs = this.elementInfo.attrs;
|
|
122
|
+
const rawSource = attrs?.source;
|
|
123
|
+
const sourceEncoded = typeof rawSource === 'string' ? rawSource : SourceField.toString(rawSource ?? []) ?? null;
|
|
124
|
+
const content = this.getCitationNodeTextContent();
|
|
125
|
+
this.citationDraft = {
|
|
126
|
+
content: content ?? '',
|
|
127
|
+
sourceEncoded,
|
|
128
|
+
};
|
|
129
|
+
this.crossReferenceDraft = null;
|
|
130
|
+
this.mathDraft = null;
|
|
131
|
+
this.mathPreviewResult = null;
|
|
132
|
+
this.resetDraftAttrs();
|
|
133
|
+
if (this.mathPreviewDebounce) {
|
|
134
|
+
clearTimeout(this.mathPreviewDebounce);
|
|
135
|
+
this.mathPreviewDebounce = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (this.elementInfo?.type === 'link') {
|
|
139
|
+
const attrs = this.elementInfo.attrs;
|
|
140
|
+
const href = typeof attrs?.href === 'string' ? attrs.href : '';
|
|
141
|
+
this.crossReferenceDraft = {
|
|
142
|
+
content: this.getLinkNodeTextContent() ?? '',
|
|
143
|
+
href,
|
|
144
|
+
};
|
|
145
|
+
this.mathDraft = null;
|
|
146
|
+
this.mathPreviewResult = null;
|
|
147
|
+
this.citationDraft = null;
|
|
148
|
+
this.hyperlinkDraft = null;
|
|
149
|
+
this.resetDraftAttrs();
|
|
150
|
+
this.destroyCitationContentView();
|
|
151
|
+
if (this.mathPreviewDebounce) {
|
|
152
|
+
clearTimeout(this.mathPreviewDebounce);
|
|
153
|
+
this.mathPreviewDebounce = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (this.elementInfo?.type === 'anchor') {
|
|
157
|
+
const attrs = this.elementInfo.attrs;
|
|
158
|
+
this.hyperlinkDraft = {
|
|
159
|
+
href: typeof attrs?.href === 'string' ? attrs.href : '',
|
|
160
|
+
};
|
|
161
|
+
this.mathDraft = null;
|
|
162
|
+
this.mathPreviewResult = null;
|
|
163
|
+
this.citationDraft = null;
|
|
164
|
+
this.crossReferenceDraft = null;
|
|
165
|
+
this.resetDraftAttrs();
|
|
166
|
+
this.destroyCitationContentView();
|
|
167
|
+
if (this.mathPreviewDebounce) {
|
|
168
|
+
clearTimeout(this.mathPreviewDebounce);
|
|
169
|
+
this.mathPreviewDebounce = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.mathDraft = null;
|
|
174
|
+
this.mathPreviewResult = null;
|
|
175
|
+
this.citationDraft = null;
|
|
176
|
+
this.crossReferenceDraft = null;
|
|
177
|
+
this.hyperlinkDraft = null;
|
|
178
|
+
this.resetDraftAttrs();
|
|
179
|
+
this.destroyCitationContentView();
|
|
180
|
+
if (this.mathPreviewDebounce) {
|
|
181
|
+
clearTimeout(this.mathPreviewDebounce);
|
|
182
|
+
this.mathPreviewDebounce = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (changedProperties.has('elementInfo') ||
|
|
187
|
+
changedProperties.has('citationDraft')) {
|
|
188
|
+
if (this.citationSourceContainer &&
|
|
189
|
+
this.elementInfo?.type === 'citation' &&
|
|
190
|
+
this.citationDraft) {
|
|
191
|
+
const items = SourceField.fromString(this.citationDraft.sourceEncoded ?? '') ?? [];
|
|
192
|
+
const adapter = this.citationSourceAdapter ?? getCitationSourceAdapter();
|
|
193
|
+
const refs = this.resolvedEditor?.references ?? [];
|
|
194
|
+
const context = {
|
|
195
|
+
onSourceChange: (encoded) => {
|
|
196
|
+
if (this.citationDraft) {
|
|
197
|
+
this.citationDraft = { ...this.citationDraft, sourceEncoded: encoded };
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
getReferences: () => refs.map((r) => ({ id: r.id, rawReference: r.rawReference })),
|
|
201
|
+
};
|
|
202
|
+
if (adapter.update) {
|
|
203
|
+
adapter.update(this.citationSourceContainer, items, context);
|
|
204
|
+
}
|
|
82
205
|
}
|
|
83
206
|
}
|
|
84
207
|
}
|
|
85
208
|
render() {
|
|
86
209
|
if (!this.elementInfo || !this.elementInfo.type) {
|
|
87
210
|
return html `
|
|
88
|
-
<div class="selection-editor-container">
|
|
211
|
+
<div class="selection-editor-container" @keydown=${this.handleGenericKeydown}>
|
|
89
212
|
<p class="selection-editor-placeholder">
|
|
90
|
-
|
|
213
|
+
${t('selectionEditor.noElement')}
|
|
91
214
|
</p>
|
|
92
215
|
</div>
|
|
93
216
|
`;
|
|
94
217
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
218
|
+
if (this.elementInfo.type === 'math' && this.mathDraft) {
|
|
219
|
+
return this.renderMathSidebar();
|
|
220
|
+
}
|
|
221
|
+
if (this.elementInfo.type === 'citation' && this.citationDraft) {
|
|
222
|
+
return this.renderCitationSidebar();
|
|
223
|
+
}
|
|
224
|
+
if (this.elementInfo.type === 'link' && this.crossReferenceDraft) {
|
|
225
|
+
return this.renderCrossReferenceSidebar();
|
|
226
|
+
}
|
|
227
|
+
if (this.elementInfo.type === 'anchor' && this.hyperlinkDraft) {
|
|
228
|
+
return this.renderHyperlinkSidebar();
|
|
229
|
+
}
|
|
230
|
+
if (this.elementInfo.type === 'figure') {
|
|
231
|
+
return this.renderFigureSidebar();
|
|
232
|
+
}
|
|
233
|
+
const uiSchema = this.resolvedEditor?.getSelectionUiSchema?.();
|
|
234
|
+
const nodeFragment = uiSchema?.properties?.nodes?.[this.elementInfo.type];
|
|
235
|
+
const useSchemaDriven = !!nodeFragment;
|
|
236
|
+
if (useSchemaDriven && nodeFragment) {
|
|
237
|
+
const attrs = this.getEffectiveAttrs();
|
|
238
|
+
return html `
|
|
239
|
+
<div class="selection-editor-container">
|
|
240
|
+
${this.renderSelectionControls()}
|
|
241
|
+
<p class="selection-editor-type-header">${this.getNodeTypeLabel(this.elementInfo.type)}</p>
|
|
242
|
+
${renderNodeFromUiSchema(nodeFragment, {
|
|
243
|
+
attrs,
|
|
244
|
+
onAttrChange: (attr, value) => this.setDraftAttr(attr, value),
|
|
245
|
+
showHidden: this.showAllAttrs,
|
|
246
|
+
getOptionsSource: (id) => {
|
|
247
|
+
if (id === 'references') {
|
|
248
|
+
const refs = this.resolvedEditor?.references ?? [];
|
|
249
|
+
return refs.map((r) => ({ id: r.id, label: r.rawReference ?? r.id }));
|
|
250
|
+
}
|
|
251
|
+
if (id === 'outlineTargets') {
|
|
252
|
+
return this.getOutlineTargetOptions();
|
|
253
|
+
}
|
|
254
|
+
return [];
|
|
255
|
+
},
|
|
256
|
+
})}
|
|
257
|
+
${this.renderDraftActions()}
|
|
258
|
+
<button
|
|
259
|
+
class="selection-editor-expand-btn"
|
|
260
|
+
@click=${() => { this.showAllAttrs = !this.showAllAttrs; }}
|
|
261
|
+
>${this.showAllAttrs ? t('selectionEditor.collapseAttributes') : t('selectionEditor.expandAttributes')}</button>
|
|
101
262
|
</div>
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
return html `
|
|
266
|
+
<div class="selection-editor-container" @keydown=${this.handleGenericKeydown}>
|
|
267
|
+
${this.renderSelectionControls()}
|
|
102
268
|
|
|
103
|
-
<!--
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<code class="selection-editor-value">${this.elementInfo.id}</code>
|
|
109
|
-
</div>
|
|
110
|
-
`
|
|
111
|
-
: ''}
|
|
269
|
+
<!-- ELEMENT section -->
|
|
270
|
+
<div class="selection-editor-section">
|
|
271
|
+
<span class="selection-editor-section-label">${t('selectionEditor.sectionElement')}</span>
|
|
272
|
+
<p class="selection-editor-type-header">${this.getNodeTypeLabel(this.elementInfo.type)}</p>
|
|
273
|
+
</div>
|
|
112
274
|
|
|
113
|
-
<!--
|
|
275
|
+
<!-- FORMATTING / Attributes -->
|
|
114
276
|
${this.elementInfo.attrs && Object.keys(this.elementInfo.attrs).length > 0
|
|
115
|
-
?
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
277
|
+
? (() => {
|
|
278
|
+
const attrs = this.getEffectiveAttrs();
|
|
279
|
+
const filtered = Object.entries(attrs).filter(([key]) => key !== 'id' &&
|
|
280
|
+
key !== 'data' &&
|
|
281
|
+
!(this.elementInfo.type === 'citation' && key === 'source'));
|
|
282
|
+
// Split into primary (formatting) attributes and the rest
|
|
283
|
+
const primaryKeys = ['text-align', 'level', 'type'];
|
|
284
|
+
const primary = filtered.filter(([k]) => primaryKeys.includes(k));
|
|
285
|
+
const rest = filtered.filter(([k]) => !primaryKeys.includes(k));
|
|
286
|
+
return html `
|
|
287
|
+
${primary.length > 0
|
|
288
|
+
? html `
|
|
289
|
+
<div class="selection-editor-section">
|
|
290
|
+
<span class="selection-editor-section-label">${t('selectionEditor.sectionFormatting')}</span>
|
|
291
|
+
<div class="selection-editor-attrs">
|
|
292
|
+
${primary.map(([attrName, attrValue]) => this.renderAttributeField(attrName, attrValue))}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
`
|
|
296
|
+
: ''}
|
|
297
|
+
${rest.length > 0 || primary.length === 0
|
|
298
|
+
? html `
|
|
299
|
+
<details class="selection-editor-details">
|
|
300
|
+
<summary class="selection-editor-details-summary">
|
|
301
|
+
<span class="selection-editor-section-label">${t('selectionEditor.sectionAttributes')}</span>
|
|
302
|
+
<svg class="selection-editor-details-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
|
303
|
+
</summary>
|
|
304
|
+
<div class="selection-editor-details-body">
|
|
305
|
+
${rest.length > 0
|
|
306
|
+
? rest.map(([attrName, attrValue]) => this.renderAttributeField(attrName, attrValue))
|
|
307
|
+
: html `<span class="selection-editor-placeholder">${t('selectionEditor.noElement')}</span>`}
|
|
308
|
+
</div>
|
|
309
|
+
</details>
|
|
310
|
+
`
|
|
311
|
+
: ''}
|
|
312
|
+
`;
|
|
313
|
+
})()
|
|
148
314
|
: ''}
|
|
315
|
+
${this.renderDraftActions()}
|
|
149
316
|
</div>
|
|
150
317
|
`;
|
|
151
318
|
}
|
|
@@ -165,13 +332,13 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
165
332
|
}
|
|
166
333
|
this.selectionListener = (event) => {
|
|
167
334
|
const selection = event.detail;
|
|
168
|
-
this.
|
|
335
|
+
this.scheduleSelectionUpdate(selection);
|
|
169
336
|
};
|
|
170
337
|
this.readyListener = () => {
|
|
171
338
|
// Re-evaluate selection when editor becomes ready
|
|
172
339
|
if (this.resolvedEditor?.editorView) {
|
|
173
340
|
const selection = this.resolvedEditor.editorView.state.selection;
|
|
174
|
-
this.
|
|
341
|
+
this.scheduleSelectionUpdate({
|
|
175
342
|
anchor: selection.anchor,
|
|
176
343
|
head: selection.head,
|
|
177
344
|
});
|
|
@@ -182,7 +349,7 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
182
349
|
// Initial evaluation
|
|
183
350
|
if (next.editorView) {
|
|
184
351
|
const selection = next.editorView.state.selection;
|
|
185
|
-
this.
|
|
352
|
+
this.scheduleSelectionUpdate({
|
|
186
353
|
anchor: selection.anchor,
|
|
187
354
|
head: selection.head,
|
|
188
355
|
});
|
|
@@ -220,6 +387,18 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
220
387
|
this.readyListener = undefined;
|
|
221
388
|
this.resolvedEditor = null;
|
|
222
389
|
}
|
|
390
|
+
scheduleSelectionUpdate(selection) {
|
|
391
|
+
if (this.pinSelection) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (this.selectionDebounce) {
|
|
395
|
+
clearTimeout(this.selectionDebounce);
|
|
396
|
+
}
|
|
397
|
+
this.selectionDebounce = setTimeout(() => {
|
|
398
|
+
this.selectionDebounce = null;
|
|
399
|
+
this.updateElementInfo(selection);
|
|
400
|
+
}, 100);
|
|
401
|
+
}
|
|
223
402
|
/**
|
|
224
403
|
* Extract element information from the current selection.
|
|
225
404
|
*/
|
|
@@ -229,32 +408,92 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
229
408
|
this.elementInfo = null;
|
|
230
409
|
return;
|
|
231
410
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
elementInfo = {
|
|
241
|
-
type: nodeAtPos.type.name,
|
|
242
|
-
id: nodeAtPos.attrs?.id ?? null,
|
|
243
|
-
attrs: nodeAtPos.attrs ?? null,
|
|
244
|
-
position: nodePosition,
|
|
411
|
+
const sel = view.state.selection;
|
|
412
|
+
if (sel instanceof NodeSelection && sel.node.type.name === 'math') {
|
|
413
|
+
const attrs = sel.node.attrs;
|
|
414
|
+
this.elementInfo = {
|
|
415
|
+
type: 'math',
|
|
416
|
+
id: attrs?.id ?? null,
|
|
417
|
+
attrs: { ...attrs },
|
|
418
|
+
position: sel.from,
|
|
245
419
|
};
|
|
420
|
+
return;
|
|
246
421
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
422
|
+
if (sel instanceof NodeSelection && sel.node.type.name === 'citation') {
|
|
423
|
+
const attrs = sel.node.attrs;
|
|
424
|
+
this.elementInfo = {
|
|
425
|
+
type: 'citation',
|
|
426
|
+
id: attrs?.id ?? null,
|
|
427
|
+
attrs: { ...attrs },
|
|
428
|
+
position: sel.from,
|
|
429
|
+
};
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (sel instanceof NodeSelection && sel.node.type.name === 'figure') {
|
|
433
|
+
const attrs = sel.node.attrs;
|
|
434
|
+
this.elementInfo = {
|
|
435
|
+
type: 'figure',
|
|
436
|
+
id: attrs?.id ?? null,
|
|
437
|
+
attrs: { ...attrs },
|
|
438
|
+
position: sel.from,
|
|
439
|
+
};
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (sel instanceof NodeSelection && sel.node.type.name === 'link') {
|
|
443
|
+
const attrs = sel.node.attrs;
|
|
444
|
+
this.elementInfo = {
|
|
445
|
+
type: 'link',
|
|
446
|
+
id: attrs?.id ?? null,
|
|
447
|
+
attrs: { ...attrs },
|
|
448
|
+
position: sel.from,
|
|
449
|
+
};
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Check for anchor mark (hyperlink) at cursor or in selection
|
|
453
|
+
const anchorMarkType = view.state.schema.marks['anchor'];
|
|
454
|
+
if (anchorMarkType) {
|
|
455
|
+
const { from, to, empty } = sel;
|
|
456
|
+
let anchorMark = null;
|
|
457
|
+
if (empty) {
|
|
458
|
+
const marks = view.state.storedMarks ?? sel.$from.marks();
|
|
459
|
+
anchorMark = marks.find((m) => m.type === anchorMarkType) ?? null;
|
|
460
|
+
if (!anchorMark) {
|
|
461
|
+
// Also check the character before the cursor
|
|
462
|
+
const marksBefore = sel.$from.marks();
|
|
463
|
+
anchorMark = marksBefore.find((m) => m.type === anchorMarkType) ?? null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
view.state.doc.nodesBetween(from, to, (node) => {
|
|
468
|
+
if (!anchorMark && node.isText) {
|
|
469
|
+
anchorMark = node.marks.find((m) => m.type === anchorMarkType) ?? null;
|
|
470
|
+
}
|
|
471
|
+
return !anchorMark;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (anchorMark) {
|
|
475
|
+
this.elementInfo = {
|
|
476
|
+
type: 'anchor',
|
|
477
|
+
id: null,
|
|
478
|
+
attrs: { ...anchorMark.attrs },
|
|
479
|
+
position: null,
|
|
480
|
+
};
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
let elementInfo = null;
|
|
485
|
+
let nodePosition;
|
|
486
|
+
const anchor = selection?.anchor ?? 0;
|
|
487
|
+
const $pos = view.state.doc.resolve(anchor);
|
|
488
|
+
const citationType = view.state.schema.nodes.citation;
|
|
489
|
+
// Prefer citation (inline) when cursor is inside one
|
|
490
|
+
if (citationType) {
|
|
251
491
|
for (let depth = $pos.depth; depth > 0; depth--) {
|
|
252
492
|
const node = $pos.node(depth);
|
|
253
|
-
|
|
254
|
-
if (node.type !== view.state.schema.topNodeType && node.type.spec.group === 'block') {
|
|
493
|
+
if (node.type === citationType) {
|
|
255
494
|
nodePosition = $pos.before(depth);
|
|
256
495
|
elementInfo = {
|
|
257
|
-
type:
|
|
496
|
+
type: 'citation',
|
|
258
497
|
id: node.attrs?.id ?? null,
|
|
259
498
|
attrs: node.attrs ?? null,
|
|
260
499
|
position: nodePosition,
|
|
@@ -262,19 +501,217 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
262
501
|
break;
|
|
263
502
|
}
|
|
264
503
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
504
|
+
}
|
|
505
|
+
// Prefer figure when cursor is inside one (e.g. in caption)
|
|
506
|
+
if (!elementInfo) {
|
|
507
|
+
const figureType = view.state.schema.nodes.figure;
|
|
508
|
+
if (figureType) {
|
|
509
|
+
for (let depth = $pos.depth; depth > 0; depth--) {
|
|
510
|
+
const node = $pos.node(depth);
|
|
511
|
+
if (node.type === figureType) {
|
|
512
|
+
nodePosition = $pos.before(depth);
|
|
513
|
+
elementInfo = {
|
|
514
|
+
type: 'figure',
|
|
515
|
+
id: node.attrs?.id ?? null,
|
|
516
|
+
attrs: node.attrs ?? null,
|
|
517
|
+
position: nodePosition,
|
|
518
|
+
};
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Prefer link (cross-reference) when cursor is inside one
|
|
525
|
+
if (!elementInfo) {
|
|
526
|
+
const linkType = view.state.schema.nodes.link;
|
|
527
|
+
if (linkType) {
|
|
528
|
+
for (let depth = $pos.depth; depth > 0; depth--) {
|
|
529
|
+
const node = $pos.node(depth);
|
|
530
|
+
if (node.type === linkType) {
|
|
531
|
+
nodePosition = $pos.before(depth);
|
|
532
|
+
elementInfo = {
|
|
533
|
+
type: 'link',
|
|
534
|
+
id: node.attrs?.id ?? null,
|
|
535
|
+
attrs: node.attrs ?? null,
|
|
536
|
+
position: nodePosition,
|
|
537
|
+
};
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (!elementInfo) {
|
|
544
|
+
const nodeAtPos = view.state.doc.nodeAt(anchor);
|
|
545
|
+
if (nodeAtPos && !nodeAtPos.isText) {
|
|
546
|
+
nodePosition = anchor;
|
|
268
547
|
elementInfo = {
|
|
269
|
-
type:
|
|
270
|
-
id:
|
|
271
|
-
attrs:
|
|
548
|
+
type: nodeAtPos.type.name,
|
|
549
|
+
id: nodeAtPos.attrs?.id ?? null,
|
|
550
|
+
attrs: nodeAtPos.attrs ?? null,
|
|
272
551
|
position: nodePosition,
|
|
273
552
|
};
|
|
274
553
|
}
|
|
554
|
+
else {
|
|
555
|
+
for (let depth = $pos.depth; depth > 0; depth--) {
|
|
556
|
+
const node = $pos.node(depth);
|
|
557
|
+
if (node.type !== view.state.schema.topNodeType && node.type.spec.group === 'block') {
|
|
558
|
+
nodePosition = $pos.before(depth);
|
|
559
|
+
elementInfo = {
|
|
560
|
+
type: node.type.name,
|
|
561
|
+
id: node.attrs?.id ?? null,
|
|
562
|
+
attrs: node.attrs ?? null,
|
|
563
|
+
position: nodePosition,
|
|
564
|
+
};
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!elementInfo && $pos.parent && $pos.parent.type !== view.state.schema.topNodeType) {
|
|
569
|
+
nodePosition = $pos.before($pos.depth);
|
|
570
|
+
elementInfo = {
|
|
571
|
+
type: $pos.parent.type.name,
|
|
572
|
+
id: $pos.parent.attrs?.id ?? null,
|
|
573
|
+
attrs: $pos.parent.attrs ?? null,
|
|
574
|
+
position: nodePosition,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
275
578
|
}
|
|
276
579
|
this.elementInfo = elementInfo;
|
|
277
580
|
}
|
|
581
|
+
/** Get the selected citation node's text content from the main document (for draft init). */
|
|
582
|
+
getCitationNodeTextContent() {
|
|
583
|
+
const node = this.getCitationNode();
|
|
584
|
+
return node?.textContent ?? '';
|
|
585
|
+
}
|
|
586
|
+
/** Get the selected citation node's content as a Fragment (same schema as citation allows). */
|
|
587
|
+
getCitationNodeContentFragment() {
|
|
588
|
+
const node = this.getCitationNode();
|
|
589
|
+
return node?.content ?? PMFragment.empty;
|
|
590
|
+
}
|
|
591
|
+
/** Get the citation node at the current selection, or null. */
|
|
592
|
+
getCitationNode() {
|
|
593
|
+
const view = this.resolvedEditor?.editorView;
|
|
594
|
+
if (!view || !this.elementInfo || this.elementInfo.type !== 'citation' || this.elementInfo.position == null) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const doc = view.state.doc;
|
|
598
|
+
const pos = this.elementInfo.position;
|
|
599
|
+
const nodeAt = doc.nodeAt(pos);
|
|
600
|
+
const citationType = view.state.schema.nodes.citation;
|
|
601
|
+
if (nodeAt?.type === citationType) {
|
|
602
|
+
return nodeAt;
|
|
603
|
+
}
|
|
604
|
+
const $pos = doc.resolve(pos);
|
|
605
|
+
for (let d = $pos.depth; d > 0; d--) {
|
|
606
|
+
const node = $pos.node(d);
|
|
607
|
+
if (node.type === citationType) {
|
|
608
|
+
return node;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
/** Get the selected cross-reference link node's text content from the main document. */
|
|
614
|
+
getLinkNodeTextContent() {
|
|
615
|
+
const node = this.getLinkNode();
|
|
616
|
+
return node?.textContent ?? '';
|
|
617
|
+
}
|
|
618
|
+
/** Get the selected cross-reference link node at current selection, or null. */
|
|
619
|
+
getLinkNode() {
|
|
620
|
+
const view = this.resolvedEditor?.editorView;
|
|
621
|
+
if (!view || !this.elementInfo || this.elementInfo.type !== 'link' || this.elementInfo.position == null) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
const doc = view.state.doc;
|
|
625
|
+
const pos = this.elementInfo.position;
|
|
626
|
+
const nodeAt = doc.nodeAt(pos);
|
|
627
|
+
const linkType = view.state.schema.nodes.link;
|
|
628
|
+
if (nodeAt?.type === linkType) {
|
|
629
|
+
return nodeAt;
|
|
630
|
+
}
|
|
631
|
+
const $pos = doc.resolve(pos);
|
|
632
|
+
for (let d = $pos.depth; d > 0; d--) {
|
|
633
|
+
const node = $pos.node(d);
|
|
634
|
+
if (node.type === linkType) {
|
|
635
|
+
return node;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
/** Build outline target options for link href dropdown (headings and figures with id). */
|
|
641
|
+
getOutlineTargetOptions() {
|
|
642
|
+
const view = this.resolvedEditor?.editorView;
|
|
643
|
+
const pmDoc = view?.state?.doc ?? null;
|
|
644
|
+
if (!pmDoc) {
|
|
645
|
+
this.outlineTargetOptionsCacheDoc = null;
|
|
646
|
+
this.outlineTargetOptionsCache = [];
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
if (this.outlineTargetOptionsCacheDoc === pmDoc) {
|
|
650
|
+
return this.outlineTargetOptionsCache;
|
|
651
|
+
}
|
|
652
|
+
const outline = collectDocumentOutline(null, pmDoc);
|
|
653
|
+
const options = [];
|
|
654
|
+
for (const h of outline.headings) {
|
|
655
|
+
const id = h.id?.trim();
|
|
656
|
+
if (!id)
|
|
657
|
+
continue;
|
|
658
|
+
const level = h.level != null ? `H${h.level}` : 'Heading';
|
|
659
|
+
const text = h.text?.trim() || id;
|
|
660
|
+
options.push({ id: `#${id}`, label: `${level}: ${text}` });
|
|
661
|
+
}
|
|
662
|
+
for (const f of outline.figures) {
|
|
663
|
+
const id = f.id?.trim();
|
|
664
|
+
if (!id)
|
|
665
|
+
continue;
|
|
666
|
+
const text = f.text?.trim() || id;
|
|
667
|
+
options.push({ id: `#${id}`, label: `${t('outline.figure')}: ${text}` });
|
|
668
|
+
}
|
|
669
|
+
this.outlineTargetOptionsCacheDoc = pmDoc;
|
|
670
|
+
this.outlineTargetOptionsCache = options;
|
|
671
|
+
return options;
|
|
672
|
+
}
|
|
673
|
+
destroyCitationContentView() {
|
|
674
|
+
if (this.citationContentView) {
|
|
675
|
+
this.citationContentView.destroy();
|
|
676
|
+
this.citationContentView = null;
|
|
677
|
+
}
|
|
678
|
+
this.citationContentEditorContainer = null;
|
|
679
|
+
this.citationContentEditorPosition = null;
|
|
680
|
+
}
|
|
681
|
+
mountCitationContentEditor(container) {
|
|
682
|
+
const view = this.resolvedEditor?.editorView;
|
|
683
|
+
if (!view || !this.elementInfo || this.elementInfo.type !== 'citation' || this.elementInfo.position == null) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const schema = view.state.schema;
|
|
687
|
+
const paragraph = schema.nodes.paragraph;
|
|
688
|
+
const docNode = schema.nodes.doc;
|
|
689
|
+
if (!paragraph || !docNode) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const pos = this.elementInfo.position;
|
|
693
|
+
if (this.citationContentView && this.citationContentEditorPosition === pos && this.citationContentEditorContainer === container) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
this.destroyCitationContentView();
|
|
697
|
+
this.citationContentEditorContainer = container;
|
|
698
|
+
this.citationContentEditorPosition = pos;
|
|
699
|
+
const initialContent = this.getCitationNodeContentFragment();
|
|
700
|
+
const paragraphNode = paragraph.create(null, initialContent);
|
|
701
|
+
const stateDoc = docNode.create(null, [paragraphNode]);
|
|
702
|
+
const strong = schema.marks.strong;
|
|
703
|
+
const em = schema.marks.em;
|
|
704
|
+
const plugins = [
|
|
705
|
+
keymap(baseKeymap),
|
|
706
|
+
...(strong ? [keymap({ 'Mod-b': toggleMark(strong) })] : []),
|
|
707
|
+
...(em ? [keymap({ 'Mod-i': toggleMark(em) })] : []),
|
|
708
|
+
];
|
|
709
|
+
const state = EditorState.create({ doc: stateDoc, schema, plugins });
|
|
710
|
+
this.citationContentView = new EditorView(container, {
|
|
711
|
+
state,
|
|
712
|
+
editable: () => true,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
278
715
|
/** Convenience getter mirroring `editor.getCommands()`. */
|
|
279
716
|
getCommandRunner() {
|
|
280
717
|
return this.resolvedEditor?.commands ?? null;
|
|
@@ -282,28 +719,71 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
282
719
|
/**
|
|
283
720
|
* Render a single attribute field, decoding known encoded values for readability.
|
|
284
721
|
*/
|
|
722
|
+
getNodeTypeLabel(type) {
|
|
723
|
+
const known = {
|
|
724
|
+
figure: () => t('selectionEditor.figure'),
|
|
725
|
+
math: () => t('selectionEditor.math'),
|
|
726
|
+
citation: () => t('selectionEditor.citation'),
|
|
727
|
+
anchor: () => t('selectionEditor.hyperlink'),
|
|
728
|
+
link: () => t('selectionEditor.crossReference'),
|
|
729
|
+
heading: () => t('selectionEditor.heading'),
|
|
730
|
+
paragraph: () => t('selectionEditor.paragraph'),
|
|
731
|
+
};
|
|
732
|
+
const label = known[type]?.() ?? type.replace(/_/g, ' ');
|
|
733
|
+
return label.replace(/^\w/, (m) => m.toUpperCase());
|
|
734
|
+
}
|
|
285
735
|
renderAttributeField(attrName, attrValue) {
|
|
286
736
|
const decodedValue = this.decodeSpecialAttribute(attrName, attrValue);
|
|
287
737
|
const renderedValue = decodedValue === null || decodedValue === undefined ? '' : String(decodedValue);
|
|
738
|
+
const friendlyLabel = this.getAttributeLabel(attrName);
|
|
739
|
+
if (attrName === 'level' && this.elementInfo?.type === 'heading') {
|
|
740
|
+
const level = Math.min(6, Math.max(1, parseInt(renderedValue, 10) || 1));
|
|
741
|
+
return html `
|
|
742
|
+
<div class="selection-editor-attr-field">
|
|
743
|
+
<label
|
|
744
|
+
class="selection-editor-attr-label"
|
|
745
|
+
for="selection-editor-attr-${attrName}"
|
|
746
|
+
>
|
|
747
|
+
${friendlyLabel}
|
|
748
|
+
</label>
|
|
749
|
+
<select
|
|
750
|
+
id="selection-editor-attr-${attrName}"
|
|
751
|
+
class="selection-editor-input selection-editor-select"
|
|
752
|
+
.value=${String(level)}
|
|
753
|
+
@change=${(e) => this.handleDraftAttrChange(e, attrName)}
|
|
754
|
+
>
|
|
755
|
+
${[1, 2, 3, 4, 5, 6].map((n) => html `<option value="${n}">${n}</option>`)}
|
|
756
|
+
</select>
|
|
757
|
+
</div>
|
|
758
|
+
`;
|
|
759
|
+
}
|
|
288
760
|
return html `
|
|
289
761
|
<div class="selection-editor-attr-field">
|
|
290
762
|
<label
|
|
291
763
|
class="selection-editor-attr-label"
|
|
292
764
|
for="selection-editor-attr-${attrName}"
|
|
293
765
|
>
|
|
294
|
-
${
|
|
766
|
+
${friendlyLabel}
|
|
295
767
|
</label>
|
|
296
768
|
<input
|
|
297
769
|
type="text"
|
|
298
770
|
id="selection-editor-attr-${attrName}"
|
|
299
771
|
class="selection-editor-input"
|
|
300
772
|
.value=${renderedValue}
|
|
301
|
-
placeholder
|
|
302
|
-
@change=${(e) => this.
|
|
773
|
+
placeholder=${t('selectionEditor.defaultValue')}
|
|
774
|
+
@change=${(e) => this.handleDraftAttrChange(e, attrName)}
|
|
303
775
|
/>
|
|
304
776
|
</div>
|
|
305
777
|
`;
|
|
306
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* Convert a raw attribute name like 'text-align' to a human-readable label.
|
|
781
|
+
*/
|
|
782
|
+
getAttributeLabel(attrName) {
|
|
783
|
+
return attrName
|
|
784
|
+
.replace(/[-_]/g, ' ')
|
|
785
|
+
.replace(/^\w/, (m) => m.toUpperCase());
|
|
786
|
+
}
|
|
307
787
|
/**
|
|
308
788
|
* Decode known encoded attributes for more readable display/editing.
|
|
309
789
|
*/
|
|
@@ -325,25 +805,33 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
325
805
|
}
|
|
326
806
|
/**
|
|
327
807
|
* Mount the citation source adapter UI when a citation node is selected.
|
|
808
|
+
* Uses draft context (onSourceChange) so changes apply only on Update.
|
|
328
809
|
*/
|
|
329
810
|
mountCitationSourceAdapter(container) {
|
|
330
811
|
if (!this.elementInfo || this.elementInfo.type !== 'citation') {
|
|
331
812
|
return;
|
|
332
813
|
}
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
814
|
+
const items = this.citationDraft != null
|
|
815
|
+
? (SourceField.fromString(this.citationDraft.sourceEncoded ?? '') ?? [])
|
|
816
|
+
: (() => {
|
|
817
|
+
const rawSource = this.elementInfo.attrs?.source;
|
|
818
|
+
return typeof rawSource === 'string'
|
|
819
|
+
? SourceField.fromString(rawSource)
|
|
820
|
+
: Array.isArray(rawSource)
|
|
821
|
+
? rawSource
|
|
822
|
+
: [];
|
|
823
|
+
})();
|
|
339
824
|
const adapter = this.citationSourceAdapter ?? getCitationSourceAdapter();
|
|
825
|
+
const refs = this.resolvedEditor?.references ?? [];
|
|
340
826
|
const context = {
|
|
341
|
-
|
|
342
|
-
this.
|
|
343
|
-
|
|
827
|
+
onSourceChange: (encoded) => {
|
|
828
|
+
if (this.citationDraft) {
|
|
829
|
+
this.citationDraft = { ...this.citationDraft, sourceEncoded: encoded };
|
|
830
|
+
}
|
|
344
831
|
},
|
|
832
|
+
getReferences: () => refs.map((r) => ({ id: r.id, rawReference: r.rawReference })),
|
|
345
833
|
};
|
|
346
|
-
adapter.render(container, items, context);
|
|
834
|
+
adapter.render(container, items.length > 0 ? items : [{ id: '' }], context);
|
|
347
835
|
}
|
|
348
836
|
/**
|
|
349
837
|
* Unmount the citation source adapter and clean up.
|
|
@@ -357,16 +845,16 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
357
845
|
container.innerHTML = '';
|
|
358
846
|
}
|
|
359
847
|
}
|
|
360
|
-
|
|
361
|
-
* Handle attribute field changes.
|
|
362
|
-
*/
|
|
363
|
-
handleAttrChange(event, attrName) {
|
|
848
|
+
handleDraftAttrChange(event, attrName) {
|
|
364
849
|
const input = event.target;
|
|
365
850
|
const value = input.value.trim();
|
|
366
851
|
let parsedValue = value;
|
|
367
852
|
if (attrName === 'source') {
|
|
368
853
|
parsedValue = this.encodeSourceField(value);
|
|
369
854
|
}
|
|
855
|
+
else if (attrName === 'alt') {
|
|
856
|
+
parsedValue = value;
|
|
857
|
+
}
|
|
370
858
|
else if (value !== '') {
|
|
371
859
|
const numValue = parseFloat(value);
|
|
372
860
|
if (!isNaN(numValue) && isFinite(numValue)) {
|
|
@@ -379,49 +867,612 @@ let SciFlowSelectionEditorElement = class SciFlowSelectionEditorElement extends
|
|
|
379
867
|
else {
|
|
380
868
|
parsedValue = null;
|
|
381
869
|
}
|
|
382
|
-
this.
|
|
870
|
+
this.setDraftAttr(attrName, parsedValue);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Render the figure sidebar: type, id, alt text.
|
|
874
|
+
*/
|
|
875
|
+
renderFigureSidebar() {
|
|
876
|
+
const attrs = this.getEffectiveAttrs();
|
|
877
|
+
const alt = typeof attrs.alt === 'string' ? attrs.alt : '';
|
|
878
|
+
return html `
|
|
879
|
+
<div class="selection-editor-container selection-editor-figure" role="form" aria-label=${t('selectionEditor.editFigure')} @keydown=${this.handleGenericKeydown}>
|
|
880
|
+
${this.renderSelectionControls()}
|
|
881
|
+
<p class="selection-editor-type-header">${this.getNodeTypeLabel(this.elementInfo.type)}</p>
|
|
882
|
+
<div class="selection-editor-field">
|
|
883
|
+
<label class="selection-editor-label" for="figure-alt">${t('selectionEditor.altText')}</label>
|
|
884
|
+
<input
|
|
885
|
+
type="text"
|
|
886
|
+
id="figure-alt"
|
|
887
|
+
class="selection-editor-input"
|
|
888
|
+
.value=${alt}
|
|
889
|
+
placeholder=${t('selectionEditor.altTextPlaceholder')}
|
|
890
|
+
@change=${(e) => this.handleDraftAttrChange(e, 'alt')}
|
|
891
|
+
/>
|
|
892
|
+
</div>
|
|
893
|
+
${this.renderDraftActions()}
|
|
894
|
+
</div>
|
|
895
|
+
`;
|
|
383
896
|
}
|
|
384
897
|
/**
|
|
385
|
-
*
|
|
386
|
-
* Preserves the 'data' attribute if it exists, even though it's not displayed.
|
|
898
|
+
* Render the math equation sidebar: TeX source, style, label, preview, status, Apply/Cancel.
|
|
387
899
|
*/
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
900
|
+
renderMathSidebar() {
|
|
901
|
+
const d = this.mathDraft;
|
|
902
|
+
const previewErr = this.mathPreviewResult?.error;
|
|
903
|
+
const previewWarnings = this.mathPreviewResult?.warnings ?? [];
|
|
904
|
+
return html `
|
|
905
|
+
<div
|
|
906
|
+
class="selection-editor-container selection-editor-math"
|
|
907
|
+
role="form"
|
|
908
|
+
aria-label=${t('selectionEditor.editEquation')}
|
|
909
|
+
@keydown=${this.handleMathKeydown}
|
|
910
|
+
>
|
|
911
|
+
${this.renderSelectionControls()}
|
|
912
|
+
<p class="selection-editor-type-header">${this.getNodeTypeLabel(this.elementInfo.type)}</p>
|
|
913
|
+
<div class="selection-editor-field">
|
|
914
|
+
<label class="selection-editor-label" for="math-tex">${t('selectionEditor.tex')}</label>
|
|
915
|
+
<textarea
|
|
916
|
+
id="math-tex"
|
|
917
|
+
class="selection-editor-textarea"
|
|
918
|
+
.value=${d.tex}
|
|
919
|
+
placeholder=${t('selectionEditor.texPlaceholder')}
|
|
920
|
+
rows="4"
|
|
921
|
+
aria-describedby=${previewErr ? 'math-preview-error' : previewWarnings.length ? 'math-preview-warnings' : undefined}
|
|
922
|
+
@input=${(e) => {
|
|
923
|
+
const t = e.target.value;
|
|
924
|
+
this.mathDraft = { ...this.mathDraft, tex: t };
|
|
925
|
+
this.scheduleMathPreview();
|
|
926
|
+
}}
|
|
927
|
+
></textarea>
|
|
928
|
+
</div>
|
|
929
|
+
<div class="selection-editor-field">
|
|
930
|
+
<label class="selection-editor-label" for="math-style">${t('selectionEditor.style')}</label>
|
|
931
|
+
<select
|
|
932
|
+
id="math-style"
|
|
933
|
+
class="selection-editor-input"
|
|
934
|
+
.value=${d.style}
|
|
935
|
+
@change=${(e) => {
|
|
936
|
+
this.mathDraft = { ...this.mathDraft, style: e.target.value };
|
|
937
|
+
this.scheduleMathPreview();
|
|
938
|
+
}}
|
|
939
|
+
>
|
|
940
|
+
<option value="inline">${t('selectionEditor.inline')}</option>
|
|
941
|
+
<option value="display">${t('selectionEditor.display')}</option>
|
|
942
|
+
</select>
|
|
943
|
+
</div>
|
|
944
|
+
<div class="selection-editor-field">
|
|
945
|
+
<label class="selection-editor-label" for="math-label">${t('selectionEditor.labelOptional')}</label>
|
|
946
|
+
<input
|
|
947
|
+
id="math-label"
|
|
948
|
+
type="text"
|
|
949
|
+
class="selection-editor-input"
|
|
950
|
+
.value=${d.label}
|
|
951
|
+
placeholder=${t('selectionEditor.labelPlaceholder')}
|
|
952
|
+
@input=${(e) => {
|
|
953
|
+
this.mathDraft = { ...this.mathDraft, label: e.target.value };
|
|
954
|
+
}}
|
|
955
|
+
/>
|
|
956
|
+
</div>
|
|
957
|
+
<div class="selection-editor-field">
|
|
958
|
+
<span class="selection-editor-label">${t('selectionEditor.preview')}</span>
|
|
959
|
+
<div
|
|
960
|
+
class="selection-editor-math-preview"
|
|
961
|
+
${ref((el) => {
|
|
962
|
+
this.mathPreviewContainer = el ?? null;
|
|
963
|
+
if (el) {
|
|
964
|
+
this.runMathPreview();
|
|
965
|
+
}
|
|
966
|
+
})}
|
|
967
|
+
role="img"
|
|
968
|
+
aria-label=${t('selectionEditor.equationPreview')}
|
|
969
|
+
></div>
|
|
970
|
+
${previewErr
|
|
971
|
+
? html `<p id="math-preview-error" class="selection-editor-math-error" role="alert">${previewErr}</p>`
|
|
972
|
+
: ''}
|
|
973
|
+
${previewWarnings.length
|
|
974
|
+
? html `
|
|
975
|
+
<p id="math-preview-warnings" class="selection-editor-math-warnings" role="status">
|
|
976
|
+
${previewWarnings.join(' ')}
|
|
977
|
+
</p>
|
|
978
|
+
`
|
|
979
|
+
: ''}
|
|
980
|
+
</div>
|
|
981
|
+
<div class="selection-editor-math-actions">
|
|
982
|
+
<button
|
|
983
|
+
type="button"
|
|
984
|
+
class="selection-editor-button selection-editor-button--primary"
|
|
985
|
+
@click=${this.applyMathDraft}
|
|
986
|
+
>
|
|
987
|
+
${t('selectionEditor.apply')}
|
|
988
|
+
</button>
|
|
989
|
+
<span class="selection-editor-math-hint">${t('selectionEditor.mathHint')}</span>
|
|
990
|
+
</div>
|
|
991
|
+
<p class="selection-editor-math-docs">
|
|
992
|
+
<a
|
|
993
|
+
href="https://docs.mathjax.org/en/v4.0"
|
|
994
|
+
target="_blank"
|
|
995
|
+
rel="noopener noreferrer"
|
|
996
|
+
class="selection-editor-math-docs-link"
|
|
997
|
+
>${t('selectionEditor.mathDocsLink')}</a>
|
|
998
|
+
</p>
|
|
999
|
+
</div>
|
|
1000
|
+
`;
|
|
1001
|
+
}
|
|
1002
|
+
scheduleMathPreview() {
|
|
1003
|
+
if (this.mathPreviewDebounce) {
|
|
1004
|
+
clearTimeout(this.mathPreviewDebounce);
|
|
1005
|
+
}
|
|
1006
|
+
this.mathPreviewDebounce = setTimeout(() => {
|
|
1007
|
+
this.mathPreviewDebounce = null;
|
|
1008
|
+
this.runMathPreview();
|
|
1009
|
+
}, 300);
|
|
1010
|
+
}
|
|
1011
|
+
runMathPreview() {
|
|
1012
|
+
if (!this.mathDraft || !this.mathPreviewContainer) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const tex = this.mathDraft.tex.trim() || '\\phantom{x}';
|
|
1016
|
+
const display = this.mathDraft.style === 'display';
|
|
1017
|
+
renderTexToSvg(tex, { display, metricsNode: this.mathPreviewContainer }).then((result) => {
|
|
1018
|
+
if (!this.mathPreviewContainer) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
this.mathPreviewContainer.innerHTML = '';
|
|
1022
|
+
if (result.svg) {
|
|
1023
|
+
this.mathPreviewContainer.appendChild(result.svg);
|
|
1024
|
+
this.mathPreviewResult = result.warnings?.length ? { warnings: result.warnings } : null;
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
this.mathPreviewResult = {
|
|
1028
|
+
...(result.error ? { error: result.error } : {}),
|
|
1029
|
+
...(result.warnings?.length ? { warnings: result.warnings } : {}),
|
|
1030
|
+
};
|
|
1031
|
+
if (result.unavailable && result.error) {
|
|
1032
|
+
const p = document.createElement('p');
|
|
1033
|
+
p.className = 'selection-editor-math-error';
|
|
1034
|
+
p.setAttribute('role', 'alert');
|
|
1035
|
+
p.textContent = result.error;
|
|
1036
|
+
this.mathPreviewContainer.appendChild(p);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
this.requestUpdate();
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
applyMathDraft() {
|
|
1043
|
+
if (!this.elementInfo || this.elementInfo.type !== 'math' || this.elementInfo.position === null || !this.mathDraft) {
|
|
391
1044
|
return;
|
|
392
1045
|
}
|
|
393
1046
|
const runner = this.getCommandRunner();
|
|
394
|
-
|
|
395
|
-
|
|
1047
|
+
const setNodeAttrs = runner?.commands?.setNodeAttrs;
|
|
1048
|
+
if (!setNodeAttrs) {
|
|
396
1049
|
return;
|
|
397
1050
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
1051
|
+
const ok = setNodeAttrs(this.elementInfo.position, {
|
|
1052
|
+
tex: this.mathDraft.tex,
|
|
1053
|
+
style: this.mathDraft.style,
|
|
1054
|
+
label: this.mathDraft.label || undefined,
|
|
1055
|
+
});
|
|
1056
|
+
if (ok && this.resolvedEditor?.editorView) {
|
|
1057
|
+
const sel = this.resolvedEditor.editorView.state.selection;
|
|
1058
|
+
this.updateElementInfo({ anchor: sel.anchor, head: sel.head });
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
cancelMathDraft() {
|
|
1062
|
+
if (this.elementInfo?.type === 'math' && this.elementInfo.attrs) {
|
|
1063
|
+
const attrs = this.elementInfo.attrs;
|
|
1064
|
+
this.mathDraft = {
|
|
1065
|
+
tex: attrs.tex ?? '',
|
|
1066
|
+
style: attrs.style === 'display' ? 'display' : 'inline',
|
|
1067
|
+
label: attrs.label ?? '',
|
|
403
1068
|
};
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
1069
|
+
this.scheduleMathPreview();
|
|
1070
|
+
this.mathPreviewResult = null;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
handleMathKeydown(e) {
|
|
1074
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
1075
|
+
e.preventDefault();
|
|
1076
|
+
this.applyMathDraft();
|
|
1077
|
+
}
|
|
1078
|
+
else if (e.key === 'Escape') {
|
|
1079
|
+
e.preventDefault();
|
|
1080
|
+
this.cancelMathDraft();
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Render the citation sidebar: content editor, source fields, Update button.
|
|
1085
|
+
*/
|
|
1086
|
+
renderCitationSidebar() {
|
|
1087
|
+
return html `
|
|
1088
|
+
<div
|
|
1089
|
+
class="selection-editor-container selection-editor-citation"
|
|
1090
|
+
role="form"
|
|
1091
|
+
aria-label=${t('selectionEditor.editCitation')}
|
|
1092
|
+
@keydown=${this.handleCitationKeydown}
|
|
1093
|
+
>
|
|
1094
|
+
${this.renderSelectionControls()}
|
|
1095
|
+
<p class="selection-editor-type-header">${this.getNodeTypeLabel(this.elementInfo.type)}</p>
|
|
1096
|
+
<div class="selection-editor-field">
|
|
1097
|
+
<label class="selection-editor-label" for="citation-content">${t('selectionEditor.inTextCitation')}</label>
|
|
1098
|
+
<div
|
|
1099
|
+
id="citation-content"
|
|
1100
|
+
class="selection-editor-input selection-editor-citation-content selection-editor-citation-pm"
|
|
1101
|
+
role="textbox"
|
|
1102
|
+
aria-label=${t('selectionEditor.inTextCitationAriaLabel')}
|
|
1103
|
+
${ref((el) => {
|
|
1104
|
+
if (el) {
|
|
1105
|
+
this.mountCitationContentEditor(el);
|
|
410
1106
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1107
|
+
else {
|
|
1108
|
+
this.destroyCitationContentView();
|
|
1109
|
+
}
|
|
1110
|
+
})}
|
|
1111
|
+
></div>
|
|
1112
|
+
</div>
|
|
1113
|
+
<div class="selection-editor-attr-field">
|
|
1114
|
+
<span class="selection-editor-label">${t('selectionEditor.semanticReference')}</span>
|
|
1115
|
+
<div
|
|
1116
|
+
class="citation-source-adapter-root"
|
|
1117
|
+
${ref((el) => {
|
|
1118
|
+
if (el) {
|
|
1119
|
+
this.citationSourceContainer = el;
|
|
1120
|
+
this.mountCitationSourceAdapter(el);
|
|
418
1121
|
}
|
|
419
|
-
else if (
|
|
420
|
-
|
|
1122
|
+
else if (this.citationSourceContainer) {
|
|
1123
|
+
this.unmountCitationSourceAdapter(this.citationSourceContainer);
|
|
1124
|
+
this.citationSourceContainer = null;
|
|
421
1125
|
}
|
|
1126
|
+
})}
|
|
1127
|
+
></div>
|
|
1128
|
+
</div>
|
|
1129
|
+
<div class="selection-editor-citation-actions">
|
|
1130
|
+
<button
|
|
1131
|
+
type="button"
|
|
1132
|
+
class="selection-editor-button selection-editor-button--primary"
|
|
1133
|
+
@click=${this.applyCitationDraft}
|
|
1134
|
+
>
|
|
1135
|
+
${t('selectionEditor.update')}
|
|
1136
|
+
</button>
|
|
1137
|
+
<span class="selection-editor-citation-hint">${t('selectionEditor.updateHint')}</span>
|
|
1138
|
+
</div>
|
|
1139
|
+
</div>
|
|
1140
|
+
`;
|
|
1141
|
+
}
|
|
1142
|
+
applyCitationDraft() {
|
|
1143
|
+
if (!this.elementInfo ||
|
|
1144
|
+
this.elementInfo.type !== 'citation' ||
|
|
1145
|
+
this.elementInfo.position === null ||
|
|
1146
|
+
!this.citationDraft) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const runner = this.getCommandRunner();
|
|
1150
|
+
const updateCitationNode = runner?.commands?.updateCitationNode;
|
|
1151
|
+
if (!updateCitationNode) {
|
|
1152
|
+
console.warn('[sciflow-selection-editor] updateCitationNode command not available');
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const content = this.citationContentView?.state?.doc?.firstChild?.content ?? this.citationDraft.content;
|
|
1156
|
+
const ok = updateCitationNode(this.elementInfo.position, {
|
|
1157
|
+
attrs: { source: this.citationDraft.sourceEncoded },
|
|
1158
|
+
content,
|
|
1159
|
+
});
|
|
1160
|
+
if (ok && this.resolvedEditor?.editorView) {
|
|
1161
|
+
const sel = this.resolvedEditor.editorView.state.selection;
|
|
1162
|
+
this.updateElementInfo({ anchor: sel.anchor, head: sel.head });
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
handleCitationKeydown(e) {
|
|
1166
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
1167
|
+
e.preventDefault();
|
|
1168
|
+
this.applyCitationDraft();
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Render the cross-reference sidebar: display text + target, Update button.
|
|
1173
|
+
*/
|
|
1174
|
+
renderCrossReferenceSidebar() {
|
|
1175
|
+
const d = this.crossReferenceDraft;
|
|
1176
|
+
const options = this.getOutlineTargetOptions();
|
|
1177
|
+
const hasCurrentTarget = options.some((o) => o.id === d.href);
|
|
1178
|
+
return html `
|
|
1179
|
+
<div
|
|
1180
|
+
class="selection-editor-container selection-editor-citation"
|
|
1181
|
+
role="form"
|
|
1182
|
+
aria-label=${t('selectionEditor.editCrossReference')}
|
|
1183
|
+
@keydown=${this.handleCrossReferenceKeydown}
|
|
1184
|
+
>
|
|
1185
|
+
${this.renderSelectionControls()}
|
|
1186
|
+
<p class="selection-editor-type-header">${this.getNodeTypeLabel(this.elementInfo.type)}</p>
|
|
1187
|
+
<div class="selection-editor-field">
|
|
1188
|
+
<label class="selection-editor-label" for="crossref-content">${t('selectionEditor.displayText')}</label>
|
|
1189
|
+
<input
|
|
1190
|
+
id="crossref-content"
|
|
1191
|
+
type="text"
|
|
1192
|
+
class="selection-editor-input"
|
|
1193
|
+
.value=${d.content}
|
|
1194
|
+
placeholder=${t('selectionEditor.displayTextPlaceholder')}
|
|
1195
|
+
@input=${(e) => {
|
|
1196
|
+
this.crossReferenceDraft = {
|
|
1197
|
+
...this.crossReferenceDraft,
|
|
1198
|
+
content: e.target.value,
|
|
1199
|
+
};
|
|
1200
|
+
}}
|
|
1201
|
+
/>
|
|
1202
|
+
</div>
|
|
1203
|
+
<div class="selection-editor-field">
|
|
1204
|
+
<label class="selection-editor-label" for="crossref-target">${t('selectionEditor.target')}</label>
|
|
1205
|
+
<select
|
|
1206
|
+
id="crossref-target"
|
|
1207
|
+
class="selection-editor-input selection-editor-select"
|
|
1208
|
+
.value=${d.href}
|
|
1209
|
+
@change=${(e) => {
|
|
1210
|
+
this.crossReferenceDraft = {
|
|
1211
|
+
...this.crossReferenceDraft,
|
|
1212
|
+
href: e.target.value,
|
|
1213
|
+
};
|
|
1214
|
+
}}
|
|
1215
|
+
>
|
|
1216
|
+
${!hasCurrentTarget && d.href
|
|
1217
|
+
? html `<option value="${d.href}">${d.href}</option>`
|
|
1218
|
+
: ''}
|
|
1219
|
+
${options.map((option) => html `<option value="${option.id}">${option.label ?? option.id}</option>`)}
|
|
1220
|
+
</select>
|
|
1221
|
+
</div>
|
|
1222
|
+
<div class="selection-editor-citation-actions">
|
|
1223
|
+
<button
|
|
1224
|
+
type="button"
|
|
1225
|
+
class="selection-editor-button selection-editor-button--primary"
|
|
1226
|
+
@click=${this.applyCrossReferenceDraft}
|
|
1227
|
+
>
|
|
1228
|
+
${t('selectionEditor.update')}
|
|
1229
|
+
</button>
|
|
1230
|
+
<span class="selection-editor-citation-hint">${t('selectionEditor.updateHint')}</span>
|
|
1231
|
+
</div>
|
|
1232
|
+
</div>
|
|
1233
|
+
`;
|
|
1234
|
+
}
|
|
1235
|
+
applyCrossReferenceDraft() {
|
|
1236
|
+
if (!this.elementInfo ||
|
|
1237
|
+
this.elementInfo.type !== 'link' ||
|
|
1238
|
+
this.elementInfo.position === null ||
|
|
1239
|
+
!this.crossReferenceDraft) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const runner = this.getCommandRunner();
|
|
1243
|
+
const updateCrossReferenceNode = runner?.commands?.updateCrossReferenceNode;
|
|
1244
|
+
if (!updateCrossReferenceNode) {
|
|
1245
|
+
console.warn('[sciflow-selection-editor] updateCrossReferenceNode command not available');
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const ok = updateCrossReferenceNode(this.elementInfo.position, {
|
|
1249
|
+
attrs: { href: this.crossReferenceDraft.href },
|
|
1250
|
+
content: this.crossReferenceDraft.content,
|
|
1251
|
+
});
|
|
1252
|
+
if (ok && this.resolvedEditor?.editorView) {
|
|
1253
|
+
const sel = this.resolvedEditor.editorView.state.selection;
|
|
1254
|
+
this.updateElementInfo({ anchor: sel.anchor, head: sel.head });
|
|
422
1255
|
}
|
|
423
|
-
|
|
424
|
-
|
|
1256
|
+
}
|
|
1257
|
+
handleCrossReferenceKeydown(e) {
|
|
1258
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
1259
|
+
e.preventDefault();
|
|
1260
|
+
this.applyCrossReferenceDraft();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Render the hyperlink (anchor mark) sidebar: URL input, Apply and Remove buttons.
|
|
1265
|
+
*/
|
|
1266
|
+
renderHyperlinkSidebar() {
|
|
1267
|
+
const d = this.hyperlinkDraft;
|
|
1268
|
+
return html `
|
|
1269
|
+
<div
|
|
1270
|
+
class="selection-editor-container selection-editor-citation"
|
|
1271
|
+
role="form"
|
|
1272
|
+
aria-label=${t('selectionEditor.editHyperlink')}
|
|
1273
|
+
@keydown=${this.handleHyperlinkKeydown}
|
|
1274
|
+
>
|
|
1275
|
+
${this.renderSelectionControls()}
|
|
1276
|
+
<p class="selection-editor-type-header">${this.getNodeTypeLabel('anchor')}</p>
|
|
1277
|
+
<div class="selection-editor-field">
|
|
1278
|
+
<label class="selection-editor-label" for="hyperlink-href">${t('selectionEditor.url')}</label>
|
|
1279
|
+
<input
|
|
1280
|
+
id="hyperlink-href"
|
|
1281
|
+
type="url"
|
|
1282
|
+
class="selection-editor-input"
|
|
1283
|
+
.value=${d.href}
|
|
1284
|
+
placeholder=${t('selectionEditor.urlPlaceholder')}
|
|
1285
|
+
@input=${(e) => {
|
|
1286
|
+
this.hyperlinkDraft = { href: e.target.value };
|
|
1287
|
+
}}
|
|
1288
|
+
/>
|
|
1289
|
+
</div>
|
|
1290
|
+
<div class="selection-editor-citation-actions">
|
|
1291
|
+
<button
|
|
1292
|
+
type="button"
|
|
1293
|
+
class="selection-editor-button selection-editor-button--primary"
|
|
1294
|
+
@click=${this.applyHyperlinkDraft}
|
|
1295
|
+
>
|
|
1296
|
+
${t('selectionEditor.update')}
|
|
1297
|
+
</button>
|
|
1298
|
+
<button
|
|
1299
|
+
type="button"
|
|
1300
|
+
class="selection-editor-button"
|
|
1301
|
+
@click=${this.removeHyperlink}
|
|
1302
|
+
>
|
|
1303
|
+
${t('selectionEditor.removeLink')}
|
|
1304
|
+
</button>
|
|
1305
|
+
<span class="selection-editor-citation-hint">${t('selectionEditor.updateHint')}</span>
|
|
1306
|
+
</div>
|
|
1307
|
+
</div>
|
|
1308
|
+
`;
|
|
1309
|
+
}
|
|
1310
|
+
applyHyperlinkDraft() {
|
|
1311
|
+
const view = this.resolvedEditor?.editorView;
|
|
1312
|
+
if (!view || !this.hyperlinkDraft)
|
|
1313
|
+
return;
|
|
1314
|
+
const { state } = view;
|
|
1315
|
+
const anchorMarkType = state.schema.marks['anchor'];
|
|
1316
|
+
if (!anchorMarkType)
|
|
1317
|
+
return;
|
|
1318
|
+
const newMark = anchorMarkType.create({ href: this.hyperlinkDraft.href, title: null, id: null });
|
|
1319
|
+
// Find a document position known to be inside the anchor mark so that
|
|
1320
|
+
// getMarkRange can resolve the full extent, regardless of where the
|
|
1321
|
+
// selection boundaries fall relative to the marked text.
|
|
1322
|
+
const { from, to, empty } = state.selection;
|
|
1323
|
+
let posInsideMark = null;
|
|
1324
|
+
if (!empty) {
|
|
1325
|
+
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
1326
|
+
if (posInsideMark === null && node.isText && node.marks.some((m) => m.type === anchorMarkType)) {
|
|
1327
|
+
posInsideMark = pos + 1; // step into the text node
|
|
1328
|
+
}
|
|
1329
|
+
return posInsideMark === null;
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
const resolvedPos = posInsideMark !== null
|
|
1333
|
+
? state.doc.resolve(posInsideMark)
|
|
1334
|
+
: state.selection.$from;
|
|
1335
|
+
const range = getMarkRange(resolvedPos, anchorMarkType, null);
|
|
1336
|
+
if (!range) {
|
|
1337
|
+
// Mark exists only in storedMarks; update it in place.
|
|
1338
|
+
view.dispatch(state.tr.addStoredMark(newMark));
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const tr = state.tr
|
|
1342
|
+
.removeMark(range.from, range.to, anchorMarkType)
|
|
1343
|
+
.addMark(range.from, range.to, newMark);
|
|
1344
|
+
view.dispatch(tr);
|
|
1345
|
+
}
|
|
1346
|
+
removeHyperlink() {
|
|
1347
|
+
const view = this.resolvedEditor?.editorView;
|
|
1348
|
+
if (!view)
|
|
1349
|
+
return;
|
|
1350
|
+
const { state } = view;
|
|
1351
|
+
const anchorMarkType = state.schema.marks['anchor'];
|
|
1352
|
+
if (!anchorMarkType)
|
|
1353
|
+
return;
|
|
1354
|
+
const { from, to, empty } = state.selection;
|
|
1355
|
+
if (empty) {
|
|
1356
|
+
const range = getMarkRange(state.selection.$from, anchorMarkType, null);
|
|
1357
|
+
if (!range) {
|
|
1358
|
+
// Mark exists only in storedMarks; clear it.
|
|
1359
|
+
view.dispatch(state.tr.removeStoredMark(anchorMarkType));
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
view.dispatch(state.tr.removeMark(range.from, range.to, anchorMarkType));
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
view.dispatch(state.tr.removeMark(from, to, anchorMarkType));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
handleHyperlinkKeydown(e) {
|
|
1369
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
1370
|
+
e.preventDefault();
|
|
1371
|
+
this.applyHyperlinkDraft();
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
resetDraftAttrs() {
|
|
1375
|
+
this.draftAttrs = null;
|
|
1376
|
+
this.draftDirty = false;
|
|
1377
|
+
}
|
|
1378
|
+
getEffectiveAttrs() {
|
|
1379
|
+
const base = (this.elementInfo?.attrs ?? {});
|
|
1380
|
+
if (!this.draftAttrs || Object.keys(this.draftAttrs).length === 0) {
|
|
1381
|
+
return base;
|
|
1382
|
+
}
|
|
1383
|
+
return { ...base, ...this.draftAttrs };
|
|
1384
|
+
}
|
|
1385
|
+
setDraftAttr(attrName, value) {
|
|
1386
|
+
const currentAttrs = (this.elementInfo?.attrs ?? {});
|
|
1387
|
+
const previous = currentAttrs[attrName] ?? null;
|
|
1388
|
+
const nextDraft = { ...(this.draftAttrs ?? {}) };
|
|
1389
|
+
if (previous === value) {
|
|
1390
|
+
delete nextDraft[attrName];
|
|
1391
|
+
}
|
|
1392
|
+
else {
|
|
1393
|
+
nextDraft[attrName] = value;
|
|
1394
|
+
}
|
|
1395
|
+
this.draftAttrs = Object.keys(nextDraft).length > 0 ? nextDraft : null;
|
|
1396
|
+
this.draftDirty = Boolean(this.draftAttrs && Object.keys(this.draftAttrs).length > 0);
|
|
1397
|
+
}
|
|
1398
|
+
applyDraftAttrs() {
|
|
1399
|
+
if (!this.draftAttrs || !this.draftDirty || !this.elementInfo || this.elementInfo.position === null) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const runner = this.getCommandRunner();
|
|
1403
|
+
const setNodeAttrsCommand = runner?.commands?.setNodeAttrs;
|
|
1404
|
+
if (!setNodeAttrsCommand) {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const success = setNodeAttrsCommand(this.elementInfo.position, this.draftAttrs);
|
|
1408
|
+
if (success && this.resolvedEditor?.editorView) {
|
|
1409
|
+
const selection = this.resolvedEditor.editorView.state.selection;
|
|
1410
|
+
this.resetDraftAttrs();
|
|
1411
|
+
this.updateElementInfo({
|
|
1412
|
+
anchor: selection.anchor,
|
|
1413
|
+
head: selection.head,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
revertDraftAttrs() {
|
|
1418
|
+
this.resetDraftAttrs();
|
|
1419
|
+
}
|
|
1420
|
+
renderDraftActions() {
|
|
1421
|
+
if (!this.draftDirty) {
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
return html `
|
|
1425
|
+
<div class="selection-editor-actions">
|
|
1426
|
+
<button
|
|
1427
|
+
type="button"
|
|
1428
|
+
class="selection-editor-button selection-editor-button--primary"
|
|
1429
|
+
@click=${this.applyDraftAttrs}
|
|
1430
|
+
>
|
|
1431
|
+
${t('selectionEditor.apply')}
|
|
1432
|
+
</button>
|
|
1433
|
+
<button
|
|
1434
|
+
type="button"
|
|
1435
|
+
class="selection-editor-button"
|
|
1436
|
+
@click=${this.revertDraftAttrs}
|
|
1437
|
+
>
|
|
1438
|
+
${t('selectionEditor.revert')}
|
|
1439
|
+
</button>
|
|
1440
|
+
<span class="selection-editor-hint">${t('selectionEditor.draftHint')}</span>
|
|
1441
|
+
</div>
|
|
1442
|
+
`;
|
|
1443
|
+
}
|
|
1444
|
+
renderSelectionControls() {
|
|
1445
|
+
return html `
|
|
1446
|
+
<div class="selection-editor-controls">
|
|
1447
|
+
<button
|
|
1448
|
+
type="button"
|
|
1449
|
+
class="selection-editor-button selection-editor-button--secondary"
|
|
1450
|
+
@click=${this.togglePinSelection}
|
|
1451
|
+
aria-pressed=${this.pinSelection ? 'true' : 'false'}
|
|
1452
|
+
>
|
|
1453
|
+
${this.pinSelection ? t('selectionEditor.unpinSelection') : t('selectionEditor.pinSelection')}
|
|
1454
|
+
</button>
|
|
1455
|
+
</div>
|
|
1456
|
+
`;
|
|
1457
|
+
}
|
|
1458
|
+
togglePinSelection() {
|
|
1459
|
+
this.pinSelection = !this.pinSelection;
|
|
1460
|
+
if (!this.pinSelection && this.resolvedEditor?.editorView) {
|
|
1461
|
+
const selection = this.resolvedEditor.editorView.state.selection;
|
|
1462
|
+
this.scheduleSelectionUpdate({
|
|
1463
|
+
anchor: selection.anchor,
|
|
1464
|
+
head: selection.head,
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
handleGenericKeydown(e) {
|
|
1469
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
1470
|
+
e.preventDefault();
|
|
1471
|
+
this.applyDraftAttrs();
|
|
1472
|
+
}
|
|
1473
|
+
else if (e.key === 'Escape') {
|
|
1474
|
+
e.preventDefault();
|
|
1475
|
+
this.revertDraftAttrs();
|
|
425
1476
|
}
|
|
426
1477
|
}
|
|
427
1478
|
/**
|
|
@@ -461,6 +1512,33 @@ __decorate([
|
|
|
461
1512
|
__decorate([
|
|
462
1513
|
state()
|
|
463
1514
|
], SciFlowSelectionEditorElement.prototype, "elementInfo", void 0);
|
|
1515
|
+
__decorate([
|
|
1516
|
+
state()
|
|
1517
|
+
], SciFlowSelectionEditorElement.prototype, "mathDraft", void 0);
|
|
1518
|
+
__decorate([
|
|
1519
|
+
state()
|
|
1520
|
+
], SciFlowSelectionEditorElement.prototype, "mathPreviewResult", void 0);
|
|
1521
|
+
__decorate([
|
|
1522
|
+
state()
|
|
1523
|
+
], SciFlowSelectionEditorElement.prototype, "citationDraft", void 0);
|
|
1524
|
+
__decorate([
|
|
1525
|
+
state()
|
|
1526
|
+
], SciFlowSelectionEditorElement.prototype, "crossReferenceDraft", void 0);
|
|
1527
|
+
__decorate([
|
|
1528
|
+
state()
|
|
1529
|
+
], SciFlowSelectionEditorElement.prototype, "hyperlinkDraft", void 0);
|
|
1530
|
+
__decorate([
|
|
1531
|
+
state()
|
|
1532
|
+
], SciFlowSelectionEditorElement.prototype, "draftAttrs", void 0);
|
|
1533
|
+
__decorate([
|
|
1534
|
+
state()
|
|
1535
|
+
], SciFlowSelectionEditorElement.prototype, "draftDirty", void 0);
|
|
1536
|
+
__decorate([
|
|
1537
|
+
state()
|
|
1538
|
+
], SciFlowSelectionEditorElement.prototype, "pinSelection", void 0);
|
|
1539
|
+
__decorate([
|
|
1540
|
+
state()
|
|
1541
|
+
], SciFlowSelectionEditorElement.prototype, "showAllAttrs", void 0);
|
|
464
1542
|
SciFlowSelectionEditorElement = __decorate([
|
|
465
1543
|
customElement('sciflow-selection-editor')
|
|
466
1544
|
], SciFlowSelectionEditorElement);
|