@threadlabs/looma 0.28.6 → 0.28.8
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/components/ui-editor-block-actions/ui-editor-block-actions.html +20 -0
- package/components/ui-editor-block-actions/ui-editor-block-actions.js +14 -0
- package/components/ui-editor-image-toolbar/ui-editor-image-toolbar.html +7 -6
- package/components/ui-editor-image-toolbar/ui-editor-image-toolbar.js +2 -1
- package/components/ui-editor-link-card/ui-editor-link-card.html +57 -0
- package/components/ui-editor-link-card/ui-editor-link-card.js +27 -0
- package/components/ui-editor-link-form/ui-editor-link-form.html +118 -0
- package/components/ui-editor-link-form/ui-editor-link-form.js +71 -0
- package/dist/index.js +37 -34
- package/package.json +1 -1
- package/styles/ui-editor-block-actions.css +1 -0
- package/styles/ui-editor-link-card.css +1 -0
- package/styles/ui-editor-link-form.css +1 -0
- package/vanilla/UiEditorBlockActions.d.ts +16 -0
- package/vanilla/UiEditorBlockActions.js +41 -0
- package/vanilla/UiEditorImageToolbar.js +1 -1
- package/vanilla/UiEditorLinkCard.d.ts +20 -0
- package/vanilla/UiEditorLinkCard.js +153 -0
- package/vanilla/UiEditorLinkForm.d.ts +32 -0
- package/vanilla/UiEditorLinkForm.js +354 -0
- package/vanilla/index.js +3 -0
- package/vue/UiEditorBlockActions.d.ts +33 -0
- package/vue/UiEditorBlockActions.js +2 -0
- package/vue/UiEditorBlockActions.vue +125 -0
- package/vue/UiEditorImageToolbar.vue +5 -6
- package/vue/UiEditorLinkCard.d.ts +58 -0
- package/vue/UiEditorLinkCard.js +2 -0
- package/vue/UiEditorLinkCard.vue +319 -0
- package/vue/UiEditorLinkForm.d.ts +165 -0
- package/vue/UiEditorLinkForm.js +2 -0
- package/vue/UiEditorLinkForm.vue +557 -0
- package/vue/chunks/UiAffordanceScope.js +1 -1
- package/vue/chunks/UiAvatar.js +1 -1
- package/vue/chunks/UiAvatarGroup.js +1 -1
- package/vue/chunks/UiBadge.js +1 -1
- package/vue/chunks/UiBreadcrumbItem.js +1 -1
- package/vue/chunks/UiBreadcrumbs.js +1 -1
- package/vue/chunks/UiButton.js +1 -1
- package/vue/chunks/UiCallout.js +1 -1
- package/vue/chunks/UiCard.js +1 -1
- package/vue/chunks/UiCheckbox.js +1 -1
- package/vue/chunks/UiCluster.js +1 -1
- package/vue/chunks/UiCombobox.js +1 -1
- package/vue/chunks/UiContainer.js +1 -1
- package/vue/chunks/UiContextMenu.js +1 -1
- package/vue/chunks/UiCover.js +1 -1
- package/vue/chunks/UiDescriptionItem.js +1 -1
- package/vue/chunks/UiDescriptionList.js +1 -1
- package/vue/chunks/UiDialog.js +1 -1
- package/vue/chunks/UiDisclosure.js +1 -1
- package/vue/chunks/UiEditable.js +1 -1
- package/vue/chunks/UiEditorBlockActions.js +127 -0
- package/vue/chunks/UiEditorImageToolbar.js +7 -25
- package/vue/chunks/UiEditorInsertTableGrid.js +1 -1
- package/vue/chunks/UiEditorLinkCard.js +356 -0
- package/vue/chunks/UiEditorLinkForm.js +669 -0
- package/vue/chunks/UiEditorMentionMenu.js +1 -1
- package/vue/chunks/UiEditorMentionMenuItem.js +1 -1
- package/vue/chunks/UiEditorSlashMenu.js +1 -1
- package/vue/chunks/UiEditorSlashMenuGroup.js +1 -1
- package/vue/chunks/UiEditorSlashMenuItem.js +1 -1
- package/vue/chunks/UiEditorSurface.js +1 -1
- package/vue/chunks/UiEditorTableContextMenu.js +1 -1
- package/vue/chunks/UiEditorTableOverlay.js +1 -1
- package/vue/chunks/UiEditorTableToolbar.js +1 -1
- package/vue/chunks/UiEditorToolbar.js +1 -1
- package/vue/chunks/UiFloatingActionButton.js +1 -1
- package/vue/chunks/UiFormField.js +1 -1
- package/vue/chunks/UiGrid.js +1 -1
- package/vue/chunks/UiIcon.js +1 -1
- package/vue/chunks/UiIconButton.js +1 -1
- package/vue/chunks/UiImage.js +1 -1
- package/vue/chunks/UiInput.js +1 -1
- package/vue/chunks/UiInputGroup.js +1 -1
- package/vue/chunks/UiList.js +1 -1
- package/vue/chunks/UiListItem.js +1 -1
- package/vue/chunks/UiListbox.js +1 -1
- package/vue/chunks/UiMenu.js +1 -1
- package/vue/chunks/UiMenuGroup.js +1 -1
- package/vue/chunks/UiMenuItem.js +1 -1
- package/vue/chunks/UiMeter.js +1 -1
- package/vue/chunks/UiNavItem.js +1 -1
- package/vue/chunks/UiPageHeader.js +1 -1
- package/vue/chunks/UiPopover.js +1 -1
- package/vue/chunks/UiRadio.js +1 -1
- package/vue/chunks/UiRadioGroup.js +1 -1
- package/vue/chunks/UiReel.js +1 -1
- package/vue/chunks/UiScrollArea.js +1 -1
- package/vue/chunks/UiSearchResultRow.js +1 -1
- package/vue/chunks/UiSearchShell.js +1 -1
- package/vue/chunks/UiSection.js +1 -1
- package/vue/chunks/UiSelect.js +1 -1
- package/vue/chunks/UiSeparator.js +1 -1
- package/vue/chunks/UiSidebar.js +1 -1
- package/vue/chunks/UiSpinner.js +1 -1
- package/vue/chunks/UiStack.js +1 -1
- package/vue/chunks/UiStatusMessage.js +1 -1
- package/vue/chunks/UiSwitch.js +1 -1
- package/vue/chunks/UiSwitcher.js +1 -1
- package/vue/chunks/UiTable.js +1 -1
- package/vue/chunks/UiTabs.js +1 -1
- package/vue/chunks/UiText.js +1 -1
- package/vue/chunks/UiTextarea.js +1 -1
- package/vue/chunks/UiToast.js +1 -1
- package/vue/chunks/UiToastRegion.js +1 -1
- package/vue/chunks/UiTooltip.js +1 -1
- package/vue/chunks/UiTree.js +1 -1
- package/vue/chunks/UiTreeItem.js +1 -1
- package/vue/chunks/_plugin-vue_export-helper.js +1 -1
- package/vue/components.css +2886 -2886
- package/vue/editor/index.js +98 -250
- package/vue/editor/primitives.d.ts +24 -2
- package/vue/index.d.ts +3 -0
- package/vue/index.js +8 -5
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<link rel="component" href="../ui-menu/ui-menu.html">
|
|
2
|
+
<link rel="component" href="../ui-menu-item/ui-menu-item.html">
|
|
3
|
+
<template component="ui-editor-block-actions" controller="./ui-editor-block-actions.js">
|
|
4
|
+
<defs>
|
|
5
|
+
<prop name="for" type="string" default="">The id of the button that opens the menu and anchors it.</prop>
|
|
6
|
+
<prop name="placement" type="keyword" values="bottom-start, top-start" default="bottom-start"
|
|
7
|
+
>Where the menu opens beside its button; top-start suits a toolbar docked at the bottom of the screen.</prop>
|
|
8
|
+
<event name="action" type="object">
|
|
9
|
+
<prop name="action" required type="keyword" values="insert-below, duplicate, delete"></prop>
|
|
10
|
+
Requests an action on the block that holds the caret.
|
|
11
|
+
</event>
|
|
12
|
+
</defs>
|
|
13
|
+
<div>
|
|
14
|
+
<ui-menu from:for="$for" from:placement="$placement" aria-label="Block actions">
|
|
15
|
+
<ui-menu-item value="insert-below">Insert paragraph below</ui-menu-item>
|
|
16
|
+
<ui-menu-item value="duplicate">Duplicate block</ui-menu-item>
|
|
17
|
+
<ui-menu-item value="delete">Delete block</ui-menu-item>
|
|
18
|
+
</ui-menu>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Reports the menu's choice as a block action.
|
|
2
|
+
function connect(host) {
|
|
3
|
+
const element = host.element;
|
|
4
|
+
const onSelect = (event) => {
|
|
5
|
+
if (event.target !== element && element.contains(event.target)) host.dispatch("action", { action: event.detail.value });
|
|
6
|
+
};
|
|
7
|
+
element.addEventListener("select", onSelect);
|
|
8
|
+
return () => element.removeEventListener("select", onSelect);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Keep DOM setup and its cleanup tied to each connection, including reconnects. */
|
|
12
|
+
export default function controller(host) {
|
|
13
|
+
host.on("connect", () => connect(host));
|
|
14
|
+
}
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
</defs>
|
|
32
32
|
<div>
|
|
33
33
|
<ui-text as="div" font="sans">
|
|
34
|
+
<!-- from:floating="true": a bare boolean converts to floating="" in Vue (HTML Next gap 2). data-image-*: the root reflects
|
|
35
|
+
props as data-<prop>, so data-placement would collide (gap 4). html-next #236 fixes both for the next alpha. -->
|
|
34
36
|
<ui-editor-toolbar from:floating="true" aria-label="Image actions">
|
|
35
37
|
<ui-icon-button
|
|
36
38
|
from:id="concat($base, '-block')"
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
data-image-placement="block"
|
|
40
42
|
from:variant="{ true: 'outline', false: 'ghost' }[concat($placement = 'block')]"
|
|
41
43
|
from:aria-pressed="concat($placement = 'block')"
|
|
42
|
-
><ui-icon name="panel-top"
|
|
44
|
+
><ui-icon name="panel-top"></ui-icon></ui-icon-button>
|
|
43
45
|
<ui-icon-button
|
|
44
46
|
from:id="concat($base, '-center')"
|
|
45
47
|
label="Center image"
|
|
@@ -47,7 +49,7 @@
|
|
|
47
49
|
data-image-placement="center"
|
|
48
50
|
from:variant="{ true: 'outline', false: 'ghost' }[concat($placement = 'center')]"
|
|
49
51
|
from:aria-pressed="concat($placement = 'center')"
|
|
50
|
-
><ui-icon name="align-center"
|
|
52
|
+
><ui-icon name="align-center"></ui-icon></ui-icon-button>
|
|
51
53
|
<ui-icon-button
|
|
52
54
|
from:id="concat($base, '-wrap-left')"
|
|
53
55
|
label="Wrap text to the right"
|
|
@@ -55,7 +57,7 @@
|
|
|
55
57
|
data-image-placement="wrap-left"
|
|
56
58
|
from:variant="{ true: 'outline', false: 'ghost' }[concat($placement = 'wrap-left')]"
|
|
57
59
|
from:aria-pressed="concat($placement = 'wrap-left')"
|
|
58
|
-
><ui-icon name="panel-left"
|
|
60
|
+
><ui-icon name="panel-left"></ui-icon></ui-icon-button>
|
|
59
61
|
<ui-icon-button
|
|
60
62
|
from:id="concat($base, '-wrap-right')"
|
|
61
63
|
label="Wrap text to the left"
|
|
@@ -63,11 +65,10 @@
|
|
|
63
65
|
data-image-placement="wrap-right"
|
|
64
66
|
from:variant="{ true: 'outline', false: 'ghost' }[concat($placement = 'wrap-right')]"
|
|
65
67
|
from:aria-pressed="concat($placement = 'wrap-right')"
|
|
66
|
-
><ui-icon name="panel-right"
|
|
67
|
-
<ui-icon-button from:id="$base" label="Image description" size="sm" variant="ghost"><ui-icon name="file-text"
|
|
68
|
+
><ui-icon name="panel-right"></ui-icon></ui-icon-button>
|
|
69
|
+
<ui-icon-button from:id="$base" label="Image description" size="sm" variant="ghost"><ui-icon name="file-text"></ui-icon></ui-icon-button>
|
|
68
70
|
<ui-icon-button from:id="concat($base, '-delete')" label="Delete image" size="sm" variant="ghost" data-image-action="delete"><ui-icon
|
|
69
71
|
name="trash"
|
|
70
|
-
size="sm"
|
|
71
72
|
></ui-icon></ui-icon-button>
|
|
72
73
|
</ui-editor-toolbar>
|
|
73
74
|
<ui-tooltip from:for="concat($base, '-block')">Image in line</ui-tooltip>
|
|
@@ -4,7 +4,8 @@ function connect(host) {
|
|
|
4
4
|
const element = host.element;
|
|
5
5
|
if (host.state.base === "ui-editor-image-toolbar") host.state.base = `ui-editor-image-toolbar-${Math.random().toString(36).slice(2, 11)}`;
|
|
6
6
|
let target = null;
|
|
7
|
-
// Marked by attribute: the converted Vue component's host.refs has no entry for a nested component
|
|
7
|
+
// Marked by attribute: the converted Vue component's host.refs has no entry for a nested component
|
|
8
|
+
// (HTML Next gap 1, fixed by html-next #236 for the next alpha).
|
|
8
9
|
const isDescription = (event) => event.target.hasAttribute?.("data-image-description") && element.contains(event.target);
|
|
9
10
|
const onClick = (event) => {
|
|
10
11
|
const control = event.target.closest?.("[data-image-placement], [data-image-action]");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<link rel="component" href="../ui-card/ui-card.html">
|
|
2
|
+
<link rel="component" href="../ui-cluster/ui-cluster.html">
|
|
3
|
+
<link rel="component" href="../ui-stack/ui-stack.html">
|
|
4
|
+
<link rel="component" href="../ui-text/ui-text.html">
|
|
5
|
+
<link rel="component" href="../ui-icon/ui-icon.html">
|
|
6
|
+
<link rel="component" href="../ui-button/ui-button.html">
|
|
7
|
+
<link rel="component" href="../ui-icon-button/ui-icon-button.html">
|
|
8
|
+
<link rel="component" href="../ui-tooltip/ui-tooltip.html">
|
|
9
|
+
<template component="ui-editor-link-card" controller="./ui-editor-link-card.js">
|
|
10
|
+
<defs>
|
|
11
|
+
<prop name="href" type="string" default="">The link's destination, shown when there is no label.</prop>
|
|
12
|
+
<prop name="openHref" type="string" default=""
|
|
13
|
+
>The address Open link opens in a new tab; without it the card offers no Open link. The editor passes the destination only when it is an http, https,
|
|
14
|
+
mailto, tel, or relative address.</prop>
|
|
15
|
+
<prop name="label" type="string" default="">The resolved destination's name, such as a page title, shown in place of the address.</prop>
|
|
16
|
+
<prop name="detail" type="string" default="">A second line under the label, such as where the destination lives.</prop>
|
|
17
|
+
<prop name="targetLabel" type="string" default="Item">What a resolved destination is called, as in "Page: Guide" for a screen reader.</prop>
|
|
18
|
+
<state name="base" type="string" value="ui-editor-link-card"></state>
|
|
19
|
+
<computed name="resolved" from="$label != ''"></computed>
|
|
20
|
+
<computed name="shown" from="{ true: $label, false: $href }[concat($resolved)]"></computed>
|
|
21
|
+
<event name="edit" type="object">Requests the link editor for this link.</event>
|
|
22
|
+
<event name="remove" type="object">Requests removing this link.</event>
|
|
23
|
+
</defs>
|
|
24
|
+
<div>
|
|
25
|
+
<ui-card variant="elevated" padding="xs" size="content">
|
|
26
|
+
<ui-cluster gap="xs" wrap="nowrap" role="group" aria-label="Link actions">
|
|
27
|
+
<ui-icon from:name="{ true: 'file-text', false: 'link' }[concat($resolved)]"></ui-icon>
|
|
28
|
+
<ui-stack gap="none" from:title="$href" from:aria-label="concat({ true: $targetLabel, false: 'URL' }[concat($resolved)], ': ', $shown)">
|
|
29
|
+
<ui-text as="strong" size="sm" weight="medium" font="sans" wrap="anywhere">{$shown}</ui-text>
|
|
30
|
+
<ui-text $if="$detail != ''" as="small" size="xs" tone="secondary" font="sans">{$detail}</ui-text>
|
|
31
|
+
</ui-stack>
|
|
32
|
+
<template $if="$openHref != ''">
|
|
33
|
+
<ui-button
|
|
34
|
+
from:id="concat($base, '-open')"
|
|
35
|
+
as="a"
|
|
36
|
+
variant="ghost"
|
|
37
|
+
tone="neutral"
|
|
38
|
+
size="sm"
|
|
39
|
+
from:href="$openHref"
|
|
40
|
+
target="_blank"
|
|
41
|
+
rel="noopener noreferrer"
|
|
42
|
+
aria-label="Open link"
|
|
43
|
+
><ui-cluster gap="xs" wrap="nowrap"><ui-icon name="external-link"></ui-icon>Open link</ui-cluster></ui-button>
|
|
44
|
+
<ui-tooltip from:for="concat($base, '-open')">Open link</ui-tooltip>
|
|
45
|
+
</template>
|
|
46
|
+
<ui-icon-button from:id="concat($base, '-edit')" label="Edit link" variant="ghost" size="sm" data-link-action="edit"><ui-icon
|
|
47
|
+
name="pencil"
|
|
48
|
+
></ui-icon></ui-icon-button>
|
|
49
|
+
<ui-tooltip from:for="concat($base, '-edit')">Edit link</ui-tooltip>
|
|
50
|
+
<ui-icon-button from:id="concat($base, '-remove')" label="Remove link" variant="ghost" size="sm" data-link-action="remove"><ui-icon
|
|
51
|
+
name="unlink"
|
|
52
|
+
></ui-icon></ui-icon-button>
|
|
53
|
+
<ui-tooltip from:for="concat($base, '-remove')">Remove link</ui-tooltip>
|
|
54
|
+
</ui-cluster>
|
|
55
|
+
</ui-card>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Gives each card its own tooltip ids and reports edit and remove requests without taking focus
|
|
2
|
+
// from the editor.
|
|
3
|
+
function connect(host) {
|
|
4
|
+
const element = host.element;
|
|
5
|
+
if (host.state.base === "ui-editor-link-card") host.state.base = `ui-editor-link-card-${Math.random().toString(36).slice(2, 11)}`;
|
|
6
|
+
const action = (event) => {
|
|
7
|
+
const control = event.target.closest?.("[data-link-action]");
|
|
8
|
+
return control && element.contains(control) ? control.dataset.linkAction : null;
|
|
9
|
+
};
|
|
10
|
+
// Keep the editor's selection while a command is pressed.
|
|
11
|
+
const onPointerDown = (event) => { if (action(event)) event.preventDefault(); };
|
|
12
|
+
const onClick = (event) => {
|
|
13
|
+
const name = action(event);
|
|
14
|
+
if (name) host.dispatch(name, {});
|
|
15
|
+
};
|
|
16
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
17
|
+
element.addEventListener("click", onClick);
|
|
18
|
+
return () => {
|
|
19
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
20
|
+
element.removeEventListener("click", onClick);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Keep DOM setup and its cleanup tied to each connection, including reconnects. */
|
|
25
|
+
export default function controller(host) {
|
|
26
|
+
host.on("connect", () => connect(host));
|
|
27
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<link rel="component" href="../ui-stack/ui-stack.html">
|
|
2
|
+
<link rel="component" href="../ui-text/ui-text.html">
|
|
3
|
+
<link rel="component" href="../ui-form-field/ui-form-field.html">
|
|
4
|
+
<link rel="component" href="../ui-input/ui-input.html">
|
|
5
|
+
<link rel="component" href="../ui-input-group/ui-input-group.html">
|
|
6
|
+
<link rel="component" href="../ui-icon/ui-icon.html">
|
|
7
|
+
<link rel="component" href="../ui-icon-button/ui-icon-button.html">
|
|
8
|
+
<link rel="component" href="../ui-scroll-area/ui-scroll-area.html">
|
|
9
|
+
<link rel="component" href="../ui-search-result-row/ui-search-result-row.html">
|
|
10
|
+
<link rel="component" href="../ui-checkbox/ui-checkbox.html">
|
|
11
|
+
<template component="ui-editor-link-form" controller="./ui-editor-link-form.js">
|
|
12
|
+
<defs>
|
|
13
|
+
<prop name="existing" type="boolean" default="false">Whether the link already exists, so the heading reads Edit link rather than Add link.</prop>
|
|
14
|
+
<prop name="searchable" type="boolean" default="false"
|
|
15
|
+
>Whether the destination field searches for a destination as well as taking an address. A searchable field has a search icon and a Clear search
|
|
16
|
+
button.</prop>
|
|
17
|
+
<prop name="mode" type="keyword" values="url, target" default="url"
|
|
18
|
+
>What the destination field holds: an address, which offers Open in new tab, or a search, which lists results.</prop>
|
|
19
|
+
<prop name="value" type="string" default="">The destination field's text: the address, or the search.</prop>
|
|
20
|
+
<prop name="placeholder" type="string" default="https://example.com">The destination field's placeholder.</prop>
|
|
21
|
+
<prop name="newTab" type="boolean" default="false">Whether Open in new tab is checked.</prop>
|
|
22
|
+
<prop name="textEditable" type="boolean" default="false">Shows a Text field, for a link added where no text is selected.</prop>
|
|
23
|
+
<prop name="text" type="string" default="">The Text field's value.</prop>
|
|
24
|
+
<prop name="error" type="string" default="">A problem to show under the fields, announced as an alert.</prop>
|
|
25
|
+
<prop name="status" type="string" default=""
|
|
26
|
+
>A line above the results, such as Searching… or No results found., announced politely. Shown only for a search.</prop>
|
|
27
|
+
<!-- default="[]": without it the generated Vue v-for receives null (HTML Next item 7; html-next #236 treats a missing list as empty for the next alpha). -->
|
|
28
|
+
<prop name="results" type="list(object({ id: string, label: string, detail?: string }))" default="[]">The destinations a search found.</prop>
|
|
29
|
+
<prop name="selected" type="string" default="">The id of the chosen result, which shows a check.</prop>
|
|
30
|
+
<state name="base" type="string" value="ui-editor-link-form"></state>
|
|
31
|
+
<computed name="heading" from="{ true: 'Edit link', false: 'Add link' }[concat($existing)]"></computed>
|
|
32
|
+
<computed name="fieldLabel" from="{ true: 'Link destination', false: 'URL' }[concat($searchable)]"></computed>
|
|
33
|
+
<event name="destination" type="object">
|
|
34
|
+
<prop name="value" required type="string"></prop>
|
|
35
|
+
The destination field changed.
|
|
36
|
+
</event>
|
|
37
|
+
<event name="text" type="object">
|
|
38
|
+
<prop name="text" required type="string"></prop>
|
|
39
|
+
The Text field changed.
|
|
40
|
+
</event>
|
|
41
|
+
<event name="new-tab" type="object">
|
|
42
|
+
<prop name="newTab" required type="boolean"></prop>
|
|
43
|
+
Open in new tab changed.
|
|
44
|
+
</event>
|
|
45
|
+
<event name="choose" type="object">
|
|
46
|
+
<prop name="id" required type="string"></prop>
|
|
47
|
+
A result was chosen.
|
|
48
|
+
</event>
|
|
49
|
+
<event name="clear" type="object">Clear search was pressed.</event>
|
|
50
|
+
<event name="save" type="object">Enter was pressed in a field, to keep the link and close the form.</event>
|
|
51
|
+
<event name="dismiss" type="object">Escape was pressed, to close the form.</event>
|
|
52
|
+
</defs>
|
|
53
|
+
<form aria-label="Edit link">
|
|
54
|
+
<ui-stack gap="s">
|
|
55
|
+
<ui-text as="h3" size="sm" weight="semibold">{$heading}</ui-text>
|
|
56
|
+
<ui-stack gap="s">
|
|
57
|
+
<ui-form-field from:invalid="$error != ''">
|
|
58
|
+
<label slot="label" from:for="concat($base, '-destination')">{$fieldLabel}</label>
|
|
59
|
+
<ui-input-group $if="$searchable" prefix-kind="icon">
|
|
60
|
+
<ui-icon slot="prefix" from:name="{ true: 'search', false: 'link' }[concat($mode = 'target')]"></ui-icon>
|
|
61
|
+
<ui-input
|
|
62
|
+
data-link-destination
|
|
63
|
+
data-link-search
|
|
64
|
+
from:id="concat($base, '-destination')"
|
|
65
|
+
type="text"
|
|
66
|
+
role="searchbox"
|
|
67
|
+
autocomplete="off"
|
|
68
|
+
from:placeholder="$placeholder"
|
|
69
|
+
from:value="$value"
|
|
70
|
+
></ui-input>
|
|
71
|
+
<ui-icon-button $if="$value != ''" slot="action" label="Clear search" variant="ghost" size="sm" data-link-clear><ui-icon
|
|
72
|
+
name="x"
|
|
73
|
+
></ui-icon></ui-icon-button>
|
|
74
|
+
</ui-input-group>
|
|
75
|
+
<template $if="not $searchable">
|
|
76
|
+
<ui-input
|
|
77
|
+
data-link-destination
|
|
78
|
+
from:id="concat($base, '-destination')"
|
|
79
|
+
type="text"
|
|
80
|
+
inputmode="url"
|
|
81
|
+
size="sm"
|
|
82
|
+
from:placeholder="$placeholder"
|
|
83
|
+
from:value="$value"
|
|
84
|
+
></ui-input>
|
|
85
|
+
</template>
|
|
86
|
+
</ui-form-field>
|
|
87
|
+
<ui-scroll-area $if="$mode = 'target'" size="sm" aria-label="Destination search results">
|
|
88
|
+
<ui-text $if="$status != ''" size="sm" tone="secondary" role="status">{$status}</ui-text>
|
|
89
|
+
<ui-search-result-row
|
|
90
|
+
$each="result of $results"
|
|
91
|
+
$key="$result.id"
|
|
92
|
+
density="compact"
|
|
93
|
+
from:selected="$result.id = $selected"
|
|
94
|
+
data-link-result
|
|
95
|
+
from:data-link-id="$result.id"
|
|
96
|
+
>
|
|
97
|
+
<ui-icon slot="leading" name="file-text"></ui-icon>
|
|
98
|
+
<!-- from:truncate="true": a bare boolean converts to truncate="" in Vue (HTML Next gap 2, fixed by html-next #236 for the next alpha). -->
|
|
99
|
+
<ui-text slot="title" as="strong" size="sm" weight="medium" from:truncate="true" from:title="$result.label">{$result.label}</ui-text>
|
|
100
|
+
<ui-text $if="$result.detail" slot="meta" as="span" size="xs" tone="secondary" from:truncate="true" from:title="$result.detail"
|
|
101
|
+
>{$result.detail}</ui-text>
|
|
102
|
+
<ui-icon $if="$result.id = $selected" slot="trailing" name="check"></ui-icon>
|
|
103
|
+
</ui-search-result-row>
|
|
104
|
+
</ui-scroll-area>
|
|
105
|
+
<template $if="$mode = 'url'">
|
|
106
|
+
<ui-checkbox size="sm" from:checked="$newTab" data-link-new-tab>Open in new tab</ui-checkbox>
|
|
107
|
+
</template>
|
|
108
|
+
</ui-stack>
|
|
109
|
+
<template $if="$textEditable">
|
|
110
|
+
<ui-form-field>
|
|
111
|
+
<label slot="label" from:for="concat($base, '-text')">Text</label>
|
|
112
|
+
<ui-input data-link-text from:id="concat($base, '-text')" size="sm" from:value="$text"></ui-input>
|
|
113
|
+
</ui-form-field>
|
|
114
|
+
</template>
|
|
115
|
+
<ui-text $if="$error != ''" size="sm" tone="danger" role="alert">{$error}</ui-text>
|
|
116
|
+
</ui-stack>
|
|
117
|
+
</form>
|
|
118
|
+
</template>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Gives each form its own field ids, reports field changes and choices, saves on Enter in a field,
|
|
2
|
+
// closes on Escape, and moves between the search field and its results with the arrow keys.
|
|
3
|
+
function connect(host) {
|
|
4
|
+
const element = host.element;
|
|
5
|
+
if (host.state.base === "ui-editor-link-form") host.state.base = `ui-editor-link-form-${Math.random().toString(36).slice(2, 11)}`;
|
|
6
|
+
const within = (event, selector) => {
|
|
7
|
+
const match = event.target.closest?.(selector);
|
|
8
|
+
return match && element.contains(match) ? match : null;
|
|
9
|
+
};
|
|
10
|
+
const onInput = (event) => {
|
|
11
|
+
if (within(event, "[data-link-destination]")) host.dispatch("destination", { value: event.target.value });
|
|
12
|
+
else if (within(event, "[data-link-text]")) host.dispatch("text", { text: event.target.value });
|
|
13
|
+
};
|
|
14
|
+
// Checkbox reports its own change with the new state; the native change it wraps carries none.
|
|
15
|
+
const onChange = (event) => {
|
|
16
|
+
if (within(event, "[data-link-new-tab]") && typeof event.detail?.checked === "boolean") host.dispatch("new-tab", { newTab: event.detail.checked });
|
|
17
|
+
};
|
|
18
|
+
const onClick = (event) => {
|
|
19
|
+
const result = within(event, "[data-link-result]");
|
|
20
|
+
if (result) host.dispatch("choose", { id: result.dataset.linkId });
|
|
21
|
+
else if (within(event, "[data-link-clear]")) {
|
|
22
|
+
host.dispatch("clear", {});
|
|
23
|
+
element.querySelector("[data-link-search]")?.focus();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const moveFocus = (event, offset) => {
|
|
27
|
+
const results = [...element.querySelectorAll("[data-link-result]")];
|
|
28
|
+
const next = results[results.indexOf(event.target) + offset];
|
|
29
|
+
if (next) next.focus();
|
|
30
|
+
else if (offset < 0) element.querySelector("[data-link-search]")?.focus();
|
|
31
|
+
else return;
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
};
|
|
34
|
+
// The form handles Enter and Escape itself, so the overlay around it does not also act on them.
|
|
35
|
+
const onKeyDown = (event) => {
|
|
36
|
+
if (event.key === "Escape") {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
event.stopPropagation();
|
|
39
|
+
host.dispatch("dismiss", {});
|
|
40
|
+
} else if (event.key === "Enter" && event.target instanceof HTMLInputElement) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
event.stopPropagation();
|
|
43
|
+
host.dispatch("save", {});
|
|
44
|
+
} else if (event.key === "ArrowDown" && (within(event, "[data-link-result]") || (within(event, "[data-link-search]") && host.props.mode.value === "target"))) {
|
|
45
|
+
moveFocus(event, 1);
|
|
46
|
+
} else if (event.key === "ArrowUp" && within(event, "[data-link-result]")) {
|
|
47
|
+
moveFocus(event, -1);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const onSubmit = (event) => {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
host.dispatch("save", {});
|
|
53
|
+
};
|
|
54
|
+
element.addEventListener("input", onInput);
|
|
55
|
+
element.addEventListener("change", onChange);
|
|
56
|
+
element.addEventListener("click", onClick);
|
|
57
|
+
element.addEventListener("keydown", onKeyDown);
|
|
58
|
+
element.addEventListener("submit", onSubmit);
|
|
59
|
+
return () => {
|
|
60
|
+
element.removeEventListener("input", onInput);
|
|
61
|
+
element.removeEventListener("change", onChange);
|
|
62
|
+
element.removeEventListener("click", onClick);
|
|
63
|
+
element.removeEventListener("keydown", onKeyDown);
|
|
64
|
+
element.removeEventListener("submit", onSubmit);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Keep DOM setup and its cleanup tied to each connection, including reconnects. */
|
|
69
|
+
export default function controller(host) {
|
|
70
|
+
host.on("connect", () => connect(host));
|
|
71
|
+
}
|