@kubex/zinc 1.1.118 → 1.1.119
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/dist/custom-elements.json +1743 -1609
- package/dist/vscode.html-custom-data.json +138 -133
- package/dist/web-types.json +313 -303
- package/dist/zn.d.ts +54 -21
- package/dist/zn.min.js +334 -317
- package/docs/pages/components/form-group.md +2 -0
- package/package.json +1 -1
- package/src/components/form-group/form-group.component.ts +51 -86
- package/src/components/form-group/form-group.test.ts +21 -1
- package/src/components/remarkd-editor/actions.ts +2 -1
- package/src/components/remarkd-editor/remarkd-editor.component.ts +251 -9
- package/src/components/remarkd-editor/remarkd-editor.scss +49 -0
- package/src/components/remarkd-editor/remarkd-editor.test.ts +329 -5
|
@@ -49,9 +49,22 @@ interface IncludeOption {
|
|
|
49
49
|
url?: string;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
interface LinkOption {
|
|
53
|
+
ref: string;
|
|
54
|
+
kind: string;
|
|
55
|
+
title: string;
|
|
56
|
+
context?: string;
|
|
57
|
+
status?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
/** remarkd's include directive on a line of its own: `include::<target>[<label>]`. */
|
|
53
61
|
const INCLUDE_LINE = /^include::([^[]+)\[(.*)]\s*$/;
|
|
54
62
|
|
|
63
|
+
/** The reference a content link carries: `kb:<kind>/<id>`. */
|
|
64
|
+
const CONTENT_LINK_PATTERN = 'kb:(?:document|category|page)\\/[A-Za-z0-9_-]+';
|
|
65
|
+
const CONTENT_LINK_HREF = new RegExp(`^${CONTENT_LINK_PATTERN}$`);
|
|
66
|
+
const CONTENT_LINK_REFS = new RegExp(CONTENT_LINK_PATTERN, 'g');
|
|
67
|
+
|
|
55
68
|
/** A document attribute definition: `:name: value`, `:flag:`, or `:flag!:` to unset. */
|
|
56
69
|
const ATTRIBUTE_LINE = /^:([A-Za-z0-9_-]+)(!)?:\s*(.*)$/;
|
|
57
70
|
|
|
@@ -119,12 +132,28 @@ const CONDITIONAL_INLINE = /^(ifdef|ifndef|iftrue|iffalse|ifempty|ifnempty)::([^
|
|
|
119
132
|
* directive, so they are replaced rather than escaped.
|
|
120
133
|
*/
|
|
121
134
|
function includeMarker(id: string, title: string): string {
|
|
135
|
+
// Collapses a run of newlines to one space, unlike contentLinkMarkup below —
|
|
136
|
+
// that one replaces char-for-char to stay byte-for-byte with app-kb's
|
|
137
|
+
// model.ContentLinkMarkup; don't merge the two formatters.
|
|
122
138
|
const label = title.replace(/[\r\n]+/g, ' ').replace(/\[/g, '(').replace(/]/g, ')').trim();
|
|
123
139
|
return `include::${id}[${label}]`;
|
|
124
140
|
}
|
|
125
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The markup a body carries for a link to other knowledge-base content.
|
|
144
|
+
* Mirrors app-kb's `model.ContentLinkMarkup`: brackets and newlines in the
|
|
145
|
+
* label would end the link early, so they are replaced rather than escaped.
|
|
146
|
+
*/
|
|
147
|
+
function contentLinkMarkup(ref: string, label: string): string {
|
|
148
|
+
const text = label.replace(/[\r\n]/g, ' ').replace(/\[/g, '(').replace(/]/g, ')').trim();
|
|
149
|
+
return `[${text}](${ref})`;
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
const SLASH_ITEMS = slashItems(EDITOR_ACTIONS);
|
|
127
153
|
|
|
154
|
+
/** Keystrokes settle before the link endpoint is asked again. */
|
|
155
|
+
const LINK_SEARCH_DEBOUNCE = 200;
|
|
156
|
+
|
|
128
157
|
/**
|
|
129
158
|
* For an asymmetric mark whose closer embeds a variable payload in parentheses — Link's
|
|
130
159
|
* `](https://)`, Tooltip's `}(Explanation)` — matches the closer by its fixed prefix rather
|
|
@@ -331,9 +360,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
331
360
|
menu: () => this.mountSlashMenu(),
|
|
332
361
|
// The menu only belongs in a block that is nothing but the slash command — a
|
|
333
362
|
// block prefix like "## " is not valid remarkd part-way through a line.
|
|
334
|
-
items: () => this.isSlashBlock()
|
|
335
|
-
? SLASH_ITEMS.filter(item => item.action !== 'include' || !!this.includeUrl)
|
|
336
|
-
: [],
|
|
363
|
+
items: () => this.isSlashBlock() ? SLASH_ITEMS.filter(item => this.slashItemAvailable(item)) : [],
|
|
337
364
|
onSelect: item => this.handleSlashSelect(item)
|
|
338
365
|
});
|
|
339
366
|
|
|
@@ -371,6 +398,17 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
371
398
|
@state() private includeLoadFailed = false;
|
|
372
399
|
@state() private includePickerIndex: number | null = null;
|
|
373
400
|
@state() private includeQuery = '';
|
|
401
|
+
@state() private linkPickerOpen = false;
|
|
402
|
+
@state() private linkQuery = '';
|
|
403
|
+
@state() private linkResults: LinkOption[] | null = null;
|
|
404
|
+
@state() private linkSearchFailed = false;
|
|
405
|
+
private linkSelection: [number, number] | null = null;
|
|
406
|
+
private linkSearchTimer?: ReturnType<typeof setTimeout>;
|
|
407
|
+
private linkSearchToken = 0;
|
|
408
|
+
|
|
409
|
+
/** Resolved link targets by reference; a null value is one the app does not know. */
|
|
410
|
+
private linkRefs = new Map<string, LinkOption | null>();
|
|
411
|
+
private linkRefsPending = false;
|
|
374
412
|
|
|
375
413
|
private pendingDragHandle: HTMLElement | null = null;
|
|
376
414
|
private dragStartX = 0;
|
|
@@ -405,6 +443,14 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
405
443
|
*/
|
|
406
444
|
@property({attribute: 'include-url'}) includeUrl = '';
|
|
407
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Endpoint the article link picker searches, as
|
|
448
|
+
* `{"items":[{ref,kind,title,context,status}]}`. Queried with `?q=<term>` as
|
|
449
|
+
* the author types and with `?refs=a,b` to resolve the references a body
|
|
450
|
+
* already carries.
|
|
451
|
+
*/
|
|
452
|
+
@property({attribute: 'link-url'}) linkUrl = '';
|
|
453
|
+
|
|
408
454
|
/** Adds a toolbar toggle that swaps the block view for the full remarkd source. */
|
|
409
455
|
@property({type: Boolean, attribute: 'allow-raw', reflect: true}) allowRaw = false;
|
|
410
456
|
|
|
@@ -465,6 +511,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
465
511
|
super.firstUpdated(_changedProperties);
|
|
466
512
|
this.formControlController.updateValidity();
|
|
467
513
|
if (this.hasIncludeBlock()) void this.loadIncludeOptions();
|
|
514
|
+
this.resolveContentLinks();
|
|
468
515
|
}
|
|
469
516
|
|
|
470
517
|
protected updated(changedProperties: PropertyValues<this>) {
|
|
@@ -472,6 +519,8 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
472
519
|
// Scoped to the parser-rendered branch only — see the class comment on the rendered div.
|
|
473
520
|
this.shadowRoot?.querySelectorAll('.remarkd-editor__rendered--parsed')
|
|
474
521
|
.forEach(rendered => this.markVariables(rendered));
|
|
522
|
+
this.shadowRoot?.querySelectorAll('.remarkd-editor__rendered--parsed')
|
|
523
|
+
.forEach(rendered => this.markContentLinks(rendered));
|
|
475
524
|
}
|
|
476
525
|
|
|
477
526
|
disconnectedCallback() {
|
|
@@ -487,6 +536,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
487
536
|
}
|
|
488
537
|
this.blocks = this.splitBlocks(this.value || '');
|
|
489
538
|
if (this.hasUpdated && this.hasIncludeBlock()) void this.loadIncludeOptions();
|
|
539
|
+
if (this.hasUpdated) this.resolveContentLinks();
|
|
490
540
|
}
|
|
491
541
|
|
|
492
542
|
@watch('includeUrl', {waitUntilFirstUpdate: true})
|
|
@@ -497,6 +547,13 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
497
547
|
if (this.hasIncludeBlock()) void this.loadIncludeOptions();
|
|
498
548
|
}
|
|
499
549
|
|
|
550
|
+
@watch('linkUrl', {waitUntilFirstUpdate: true})
|
|
551
|
+
handleLinkUrlChange() {
|
|
552
|
+
this.linkRefs.clear();
|
|
553
|
+
this.linkRefsPending = false;
|
|
554
|
+
this.resolveContentLinks();
|
|
555
|
+
}
|
|
556
|
+
|
|
500
557
|
/**
|
|
501
558
|
* Splits remarkd source into blocks on blank lines, keeping fenced /
|
|
502
559
|
* delimited containers (``` ==== !!!! .... ---- ____ **** ////) as single blocks.
|
|
@@ -682,6 +739,8 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
682
739
|
this.editingDraft = draft ?? this.blocks[index] ?? '';
|
|
683
740
|
this.editingIndex = index;
|
|
684
741
|
this.imageEdit = null;
|
|
742
|
+
this.linkPickerOpen = false;
|
|
743
|
+
this.linkSelection = null;
|
|
685
744
|
this.editShell = this.computeEditShell(this.editingDraft);
|
|
686
745
|
this.slashController.close();
|
|
687
746
|
void this.focusInput(align, caretOffset);
|
|
@@ -923,6 +982,12 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
923
982
|
|
|
924
983
|
/** Returns false for items the controller should not insert text for. */
|
|
925
984
|
private handleSlashSelect(item: SlashMenuItem): boolean {
|
|
985
|
+
if (item.action === 'link') {
|
|
986
|
+
// The controller strips the "/…" text first, leaving the caret where the
|
|
987
|
+
// command was — which is where the link belongs.
|
|
988
|
+
void this.updateComplete.then(() => this.pickLink());
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
926
991
|
if (item.action !== 'image' && item.action !== 'include') return true;
|
|
927
992
|
|
|
928
993
|
const index = this.editingIndex ?? this.blocks.length;
|
|
@@ -1122,6 +1187,66 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1122
1187
|
void this.revealAfterUpdate('.remarkd-editor__include-picker', 'center');
|
|
1123
1188
|
}
|
|
1124
1189
|
|
|
1190
|
+
/**
|
|
1191
|
+
* Opens the picker over the block being edited. The caret range is captured
|
|
1192
|
+
* now: the picker's own filter takes focus, so the textarea's selection is
|
|
1193
|
+
* gone by the time an option is chosen.
|
|
1194
|
+
*/
|
|
1195
|
+
private pickLink() {
|
|
1196
|
+
if (this.disabled || this.readonly || !this.linkUrl || this.editingIndex === null) return;
|
|
1197
|
+
const input = this.shadowRoot?.querySelector<HTMLTextAreaElement>('.remarkd-editor__input');
|
|
1198
|
+
if (!input) return;
|
|
1199
|
+
this.linkSelection = [input.selectionStart, input.selectionEnd];
|
|
1200
|
+
this.linkQuery = input.value.slice(input.selectionStart, input.selectionEnd).trim();
|
|
1201
|
+
this.linkResults = null;
|
|
1202
|
+
this.linkSearchFailed = false;
|
|
1203
|
+
this.linkPickerOpen = true;
|
|
1204
|
+
// The picker renders inside the editing block and its filter takes focus,
|
|
1205
|
+
// so the textarea's blur must not commit the edit — that would unmount the
|
|
1206
|
+
// picker mid-interaction.
|
|
1207
|
+
this.suppressBlurCommit = true;
|
|
1208
|
+
this.searchLinks(this.linkQuery);
|
|
1209
|
+
void this.updateComplete.then(() => {
|
|
1210
|
+
this.shadowRoot?.querySelector<HTMLInputElement>('.remarkd-editor__link-filter')?.focus();
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private closeLinkPicker = () => {
|
|
1215
|
+
this.linkPickerOpen = false;
|
|
1216
|
+
this.linkSelection = null;
|
|
1217
|
+
this.suppressBlurCommit = false;
|
|
1218
|
+
clearTimeout(this.linkSearchTimer);
|
|
1219
|
+
void this.updateComplete.then(() => {
|
|
1220
|
+
this.shadowRoot?.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')
|
|
1221
|
+
?.focus({preventScroll: true});
|
|
1222
|
+
});
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
/** Debounced; only the newest response is kept. */
|
|
1226
|
+
private searchLinks(term: string) {
|
|
1227
|
+
clearTimeout(this.linkSearchTimer);
|
|
1228
|
+
const token = ++this.linkSearchToken;
|
|
1229
|
+
this.linkSearchTimer = setTimeout(() => {
|
|
1230
|
+
const url = `${this.linkUrl}${this.linkUrl.includes('?') ? '&' : '?'}q=${encodeURIComponent(term)}`;
|
|
1231
|
+
fetch(url, {headers: {Accept: 'application/json'}})
|
|
1232
|
+
.then(res => {
|
|
1233
|
+
if (!res.ok) throw new Error(`link search failed: ${res.status}`);
|
|
1234
|
+
return res.json() as Promise<{items?: LinkOption[]}>;
|
|
1235
|
+
})
|
|
1236
|
+
.then(data => {
|
|
1237
|
+
if (token !== this.linkSearchToken) return;
|
|
1238
|
+
this.linkResults = data.items ?? [];
|
|
1239
|
+
this.linkSearchFailed = false;
|
|
1240
|
+
})
|
|
1241
|
+
.catch(error => {
|
|
1242
|
+
console.error('[zn-remarkd-editor] link search failed', error);
|
|
1243
|
+
if (token !== this.linkSearchToken) return;
|
|
1244
|
+
this.linkResults = null;
|
|
1245
|
+
this.linkSearchFailed = true;
|
|
1246
|
+
});
|
|
1247
|
+
}, LINK_SEARCH_DEBOUNCE);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1125
1250
|
private closeIncludePicker = () => {
|
|
1126
1251
|
this.includePickerIndex = null;
|
|
1127
1252
|
};
|
|
@@ -1225,6 +1350,50 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1225
1350
|
return this.includeRequest;
|
|
1226
1351
|
}
|
|
1227
1352
|
|
|
1353
|
+
/**
|
|
1354
|
+
* Resolves the references in the body that have not been resolved yet, so a
|
|
1355
|
+
* link whose target is gone can be marked. A failed request records nothing:
|
|
1356
|
+
* an unanswered reference is not a broken one.
|
|
1357
|
+
*/
|
|
1358
|
+
private resolveContentLinks() {
|
|
1359
|
+
if (!this.linkUrl || this.linkRefsPending) return;
|
|
1360
|
+
const refs = [...new Set((this.value || '').match(CONTENT_LINK_REFS) ?? [])]
|
|
1361
|
+
.filter(ref => !this.linkRefs.has(ref));
|
|
1362
|
+
if (!refs.length) return;
|
|
1363
|
+
|
|
1364
|
+
this.linkRefsPending = true;
|
|
1365
|
+
const url = `${this.linkUrl}${this.linkUrl.includes('?') ? '&' : '?'}refs=${encodeURIComponent(refs.join(','))}`;
|
|
1366
|
+
fetch(url, {headers: {Accept: 'application/json'}})
|
|
1367
|
+
.then(res => {
|
|
1368
|
+
if (!res.ok) throw new Error(`link resolve failed: ${res.status}`);
|
|
1369
|
+
return res.json() as Promise<{items?: LinkOption[]}>;
|
|
1370
|
+
})
|
|
1371
|
+
.then(data => {
|
|
1372
|
+
const known = new Map((data.items ?? []).map(item => [item.ref, item]));
|
|
1373
|
+
for (const ref of refs) this.linkRefs.set(ref, known.get(ref) ?? null);
|
|
1374
|
+
this.requestUpdate();
|
|
1375
|
+
})
|
|
1376
|
+
.catch(error => {
|
|
1377
|
+
console.error('[zn-remarkd-editor] link resolve failed', error);
|
|
1378
|
+
})
|
|
1379
|
+
.finally(() => {
|
|
1380
|
+
// Don't retry a failed request here: these refs are still absent from
|
|
1381
|
+
// linkRefs, so the next value change calls this again — retrying in
|
|
1382
|
+
// finally instead would loop forever on a persistent failure.
|
|
1383
|
+
this.linkRefsPending = false;
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
private markContentLinks(root: Element) {
|
|
1388
|
+
root.querySelectorAll<HTMLAnchorElement>('a[href^="kb:"]').forEach(anchor => {
|
|
1389
|
+
const match = CONTENT_LINK_HREF.exec(anchor.getAttribute('href') ?? '');
|
|
1390
|
+
const missing = !!match && this.linkRefs.get(match[0]) === null;
|
|
1391
|
+
anchor.classList.toggle('remarkd-editor__link--missing', missing);
|
|
1392
|
+
if (missing) anchor.title = 'This article is no longer available';
|
|
1393
|
+
else anchor.removeAttribute('title');
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1228
1397
|
private autosize(input: HTMLTextAreaElement) {
|
|
1229
1398
|
input.style.height = 'auto';
|
|
1230
1399
|
input.style.height = `${input.scrollHeight}px`;
|
|
@@ -1351,7 +1520,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1351
1520
|
this.pickImage(index);
|
|
1352
1521
|
} else if (action.opens === 'include') {
|
|
1353
1522
|
this.pickInclude(index);
|
|
1354
|
-
} else {
|
|
1523
|
+
} else if (!action.opens) {
|
|
1355
1524
|
this.insertDraftBlock(index, action.prefix ?? '', action.caretOffset);
|
|
1356
1525
|
}
|
|
1357
1526
|
}
|
|
@@ -1552,6 +1721,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1552
1721
|
@paste=${this.handleEditPaste}
|
|
1553
1722
|
@blur=${this.handleEditBlur}></textarea>
|
|
1554
1723
|
</div>
|
|
1724
|
+
${this.linkPickerOpen ? this.renderLinkPicker() : ''}
|
|
1555
1725
|
</div>`;
|
|
1556
1726
|
}
|
|
1557
1727
|
|
|
@@ -1601,16 +1771,31 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1601
1771
|
</div>`;
|
|
1602
1772
|
}
|
|
1603
1773
|
|
|
1604
|
-
/**
|
|
1774
|
+
/** Inline marks and the article link both apply into an open block, not a new one. */
|
|
1605
1775
|
private isActionDisabled(action: EditorAction): boolean {
|
|
1606
|
-
return !!action.inline && this.editingIndex === null;
|
|
1776
|
+
return (!!action.inline || action.opens === 'link') && this.editingIndex === null;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/** A picker action with no endpoint configured is not offered at all. */
|
|
1780
|
+
private actionAvailable(action: EditorAction): boolean {
|
|
1781
|
+
if (action.opens === 'include') return !!this.includeUrl;
|
|
1782
|
+
if (action.opens === 'link') return !!this.linkUrl;
|
|
1783
|
+
return true;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
private slashItemAvailable(item: SlashMenuItem): boolean {
|
|
1787
|
+
if (item.action === 'include') return !!this.includeUrl;
|
|
1788
|
+
if (item.action === 'link') return !!this.linkUrl;
|
|
1789
|
+
return true;
|
|
1607
1790
|
}
|
|
1608
1791
|
|
|
1609
1792
|
/** Routes a toolbar/menu action to the inline or block insert path — the one place both
|
|
1610
1793
|
* `renderAction` and `renderMenuAction` call, so the bar and the overflow menu cannot
|
|
1611
1794
|
* drift out of sync on what a given action actually does. */
|
|
1612
1795
|
private activateAction(action: EditorAction) {
|
|
1613
|
-
if (action.
|
|
1796
|
+
if (action.opens === 'link') {
|
|
1797
|
+
this.pickLink();
|
|
1798
|
+
} else if (action.inline) {
|
|
1614
1799
|
this.applyInline(action.inline);
|
|
1615
1800
|
} else {
|
|
1616
1801
|
this.handleToolbarInsert(action);
|
|
@@ -1656,7 +1841,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1656
1841
|
.map(group => ({
|
|
1657
1842
|
group,
|
|
1658
1843
|
actions: EDITOR_ACTIONS.filter(action => action.group === group.id
|
|
1659
|
-
&& (action
|
|
1844
|
+
&& this.actionAvailable(action)),
|
|
1660
1845
|
}))
|
|
1661
1846
|
.filter(entry => entry.actions.length);
|
|
1662
1847
|
const visible = this.toolbarOverflow.visibleCount;
|
|
@@ -1675,7 +1860,11 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1675
1860
|
}}
|
|
1676
1861
|
@zn-hide=${(e: Event) => {
|
|
1677
1862
|
if (e.target !== e.currentTarget) return;
|
|
1678
|
-
this
|
|
1863
|
+
// Choosing "Insert link" from this menu opens the link picker,
|
|
1864
|
+
// which sets suppressBlurCommit itself; clearing it here
|
|
1865
|
+
// unconditionally would race the picker's own flag and blur
|
|
1866
|
+
// the textarea out from under it.
|
|
1867
|
+
if (!this.linkPickerOpen) this.suppressBlurCommit = false;
|
|
1679
1868
|
}}>
|
|
1680
1869
|
<zn-button slot="trigger" type="button" icon-button plain icon="ellipsis@lu"
|
|
1681
1870
|
icon-size="18" tooltip="More"
|
|
@@ -1771,6 +1960,59 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
|
|
|
1771
1960
|
</div>`;
|
|
1772
1961
|
}
|
|
1773
1962
|
|
|
1963
|
+
private renderLinkPicker() {
|
|
1964
|
+
const kinds: Record<string, string> = {document: 'Article', category: 'Category', page: 'Page'};
|
|
1965
|
+
return html`
|
|
1966
|
+
<div part="link-picker" class="remarkd-editor__link-picker">
|
|
1967
|
+
<div class="remarkd-editor__link-picker-head">
|
|
1968
|
+
<input class="remarkd-editor__link-filter"
|
|
1969
|
+
placeholder="Find an article"
|
|
1970
|
+
.value=${this.linkQuery}
|
|
1971
|
+
@input=${(e: Event) => {
|
|
1972
|
+
this.linkQuery = (e.target as HTMLInputElement).value;
|
|
1973
|
+
this.searchLinks(this.linkQuery);
|
|
1974
|
+
}}>
|
|
1975
|
+
<zn-button type="button" icon-button="small" plain icon="x@lu"
|
|
1976
|
+
tooltip="Cancel" @click=${this.closeLinkPicker}></zn-button>
|
|
1977
|
+
</div>
|
|
1978
|
+
${this.linkResults === null
|
|
1979
|
+
? html`<div class="remarkd-editor__link-picker-empty">${
|
|
1980
|
+
this.linkSearchFailed ? 'Could not search for articles' : 'Searching…'}</div>`
|
|
1981
|
+
: this.linkResults.length
|
|
1982
|
+
? this.linkResults.map(item => html`
|
|
1983
|
+
<button type="button" class="remarkd-editor__link-option"
|
|
1984
|
+
@click=${() => this.insertLink(item)}>
|
|
1985
|
+
<span class="remarkd-editor__link-option-title">${item.title}</span>
|
|
1986
|
+
<span class="remarkd-editor__link-option-meta">${
|
|
1987
|
+
[kinds[item.kind] ?? item.kind, item.context, item.status === 'published' ? '' : item.status]
|
|
1988
|
+
.filter(Boolean).join(' · ')}</span>
|
|
1989
|
+
</button>`)
|
|
1990
|
+
: html`<div class="remarkd-editor__link-picker-empty">No articles found</div>`}
|
|
1991
|
+
</div>`;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
private insertLink(item: LinkOption) {
|
|
1995
|
+
const input = this.shadowRoot?.querySelector<HTMLTextAreaElement>('.remarkd-editor__input');
|
|
1996
|
+
const range = this.linkSelection;
|
|
1997
|
+
this.linkPickerOpen = false;
|
|
1998
|
+
this.linkSelection = null;
|
|
1999
|
+
this.suppressBlurCommit = false;
|
|
2000
|
+
if (!input || !range) return;
|
|
2001
|
+
|
|
2002
|
+
const [start, end] = range;
|
|
2003
|
+
const selected = input.value.slice(start, end).trim();
|
|
2004
|
+
const markup = contentLinkMarkup(item.ref, selected || item.title);
|
|
2005
|
+
const next = input.value.slice(0, start) + markup + input.value.slice(end);
|
|
2006
|
+
|
|
2007
|
+
this.editingDraft = next;
|
|
2008
|
+
input.value = next;
|
|
2009
|
+
input.setSelectionRange(start + markup.length, start + markup.length);
|
|
2010
|
+
this.editShell = this.computeEditShell(next);
|
|
2011
|
+
this.autosize(input);
|
|
2012
|
+
input.focus({preventScroll: true});
|
|
2013
|
+
this.emit('zn-input');
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1774
2016
|
private renderImagePicker() {
|
|
1775
2017
|
return html`
|
|
1776
2018
|
<div class="remarkd-editor__image-picker">
|
|
@@ -485,3 +485,52 @@
|
|
|
485
485
|
border: 0;
|
|
486
486
|
padding: 0;
|
|
487
487
|
}
|
|
488
|
+
|
|
489
|
+
.remarkd-editor__link-picker {
|
|
490
|
+
display: flex;
|
|
491
|
+
flex-direction: column;
|
|
492
|
+
gap: var(--zn-spacing-x-small);
|
|
493
|
+
margin-top: var(--zn-spacing-x-small);
|
|
494
|
+
padding: var(--zn-spacing-small);
|
|
495
|
+
border: 1px solid rgb(var(--zn-border-color));
|
|
496
|
+
border-radius: var(--zn-border-radius);
|
|
497
|
+
background: var(--zn-color-neutral-0, white);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.remarkd-editor__link-picker-head {
|
|
501
|
+
display: flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
gap: var(--zn-spacing-x-small);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.remarkd-editor__link-filter {
|
|
507
|
+
flex: 1 1 auto;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.remarkd-editor__link-option {
|
|
511
|
+
display: flex;
|
|
512
|
+
justify-content: space-between;
|
|
513
|
+
gap: var(--zn-spacing-small);
|
|
514
|
+
padding: var(--zn-spacing-x-small) var(--zn-spacing-small);
|
|
515
|
+
border: 0;
|
|
516
|
+
border-radius: var(--zn-border-radius);
|
|
517
|
+
background: none;
|
|
518
|
+
cursor: pointer;
|
|
519
|
+
text-align: left;
|
|
520
|
+
|
|
521
|
+
&:hover {
|
|
522
|
+
background: var(--zn-color-neutral-100);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.remarkd-editor__link-option-meta,
|
|
527
|
+
.remarkd-editor__link-picker-empty {
|
|
528
|
+
color: var(--zn-color-neutral-400, #9aa4b2);
|
|
529
|
+
font-size: var(--zn-font-size-small);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.remarkd-editor__link--missing {
|
|
533
|
+
text-decoration-line: underline;
|
|
534
|
+
text-decoration-style: wavy;
|
|
535
|
+
text-decoration-color: rgb(var(--zn-color-error));
|
|
536
|
+
}
|