@kubex/zinc 1.1.78 → 1.1.80
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 +1149 -130
- package/dist/vscode.html-custom-data.json +175 -1
- package/dist/web-types.json +396 -2
- package/dist/zn.d.ts +384 -0
- package/dist/zn.min.js +350 -295
- package/docs/pages/components/inline-edit.md +21 -0
- package/docs/pages/components/rating.md +2 -1
- package/docs/pages/components/slash-item.md +126 -0
- package/docs/pages/components/slash-menu.md +168 -0
- package/docs/pages/components/textarea.md +148 -0
- package/docs/pages/components/translations.md +34 -0
- package/package.json +1 -1
- package/src/components/inline-edit/inline-edit.component.ts +31 -0
- package/src/components/inline-edit/inline-edit.test.ts +83 -1
- package/src/components/rating/rating.component.ts +10 -1
- package/src/components/rating/rating.scss +6 -9
- package/src/components/slash-item/index.ts +12 -0
- package/src/components/slash-item/slash-item.component.ts +76 -0
- package/src/components/slash-item/slash-item.scss +5 -0
- package/src/components/slash-menu/index.ts +14 -0
- package/src/components/slash-menu/slash-menu-controller.ts +361 -0
- package/src/components/slash-menu/slash-menu-items.ts +122 -0
- package/src/components/slash-menu/slash-menu.component.ts +305 -0
- package/src/components/slash-menu/slash-menu.scss +120 -0
- package/src/components/slash-menu/slash-menu.test.ts +154 -0
- package/src/components/textarea/textarea.component.ts +143 -0
- package/src/components/textarea/textarea.test.ts +310 -2
- package/src/components/translations/translations.component.ts +31 -0
- package/src/components/translations/translations.test.ts +89 -1
- package/src/events/events.ts +2 -0
- package/src/events/zn-slash-insert.ts +9 -0
- package/src/events/zn-slash-select.ts +9 -0
- package/src/utilities/caret-position.ts +118 -0
- package/src/zinc.ts +3 -0
|
@@ -117,6 +117,27 @@ Use `input-type="textarea"` for multi-line text content.
|
|
|
117
117
|
</div>
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
#### Slash Menu Quick Insertions
|
|
121
|
+
|
|
122
|
+
{% raw %}
|
|
123
|
+
|
|
124
|
+
`slash-items`, `slash-preset`, `slash-trigger` and `slash-heading` are forwarded to the inner
|
|
125
|
+
[`zn-textarea`](/components/textarea#slash-menu-quick-insertions), so typing `/` offers replacement strings at the
|
|
126
|
+
caret. The menu claims `Enter` and `Escape` while it is open, so choosing an item neither submits nor cancels the edit.
|
|
127
|
+
|
|
128
|
+
```html:preview
|
|
129
|
+
<zn-inline-edit
|
|
130
|
+
name="terms"
|
|
131
|
+
input-type="textarea"
|
|
132
|
+
textarea-rows="4"
|
|
133
|
+
value="This agreement is between you and "
|
|
134
|
+
slash-heading="Replacement strings"
|
|
135
|
+
slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}}">
|
|
136
|
+
</zn-inline-edit>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
{% endraw %}
|
|
140
|
+
|
|
120
141
|
### Number Input Type
|
|
121
142
|
|
|
122
143
|
Use `input-type="number"` for numeric values.
|
|
@@ -328,7 +328,8 @@ Available CSS custom properties:
|
|
|
328
328
|
- `--symbol-color` - Color of unfilled symbols
|
|
329
329
|
- `--symbol-color-active` - Color of filled/active symbols
|
|
330
330
|
- `--symbol-size` - Size of the symbols
|
|
331
|
-
- `--symbol-spacing` -
|
|
331
|
+
- `--symbol-spacing` - Gap between symbols. The rating adds no padding of its own, so the first symbol sits flush with
|
|
332
|
+
the component's edge and lines up with other form controls.
|
|
332
333
|
- `--preview-color` - Color of the preview value (see [Preview](#preview))
|
|
333
334
|
- `--preview-size` - Font size of the preview value
|
|
334
335
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
meta:
|
|
3
|
+
title: Slash Item
|
|
4
|
+
description: Slash items declare the quick insertions offered by a slash menu.
|
|
5
|
+
layout: component
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{% raw %}
|
|
9
|
+
|
|
10
|
+
`<zn-slash-item>` renders nothing on its own. It describes one entry for the component it sits in — today that is
|
|
11
|
+
[`zn-textarea`](/components/textarea#slash-menu-quick-insertions), which picks up items placed anywhere inside it,
|
|
12
|
+
including inside a slotted [`zn-slash-menu`](/components/slash-menu).
|
|
13
|
+
|
|
14
|
+
```html:preview
|
|
15
|
+
<zn-textarea label="Terms and conditions" rows="5" help-text="Type / to insert">
|
|
16
|
+
<zn-slash-item
|
|
17
|
+
icon="tag@lu"
|
|
18
|
+
label="Brand name"
|
|
19
|
+
description="The merchant's trading name"
|
|
20
|
+
value="{{BRAND_NAME}}"></zn-slash-item>
|
|
21
|
+
<zn-slash-item
|
|
22
|
+
icon="mail@lu"
|
|
23
|
+
label="Support email"
|
|
24
|
+
value="{{SUPPORT_EMAIL}}"></zn-slash-item>
|
|
25
|
+
</zn-textarea>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
### Inserting Long Or Multi-Line Text
|
|
31
|
+
|
|
32
|
+
Leave `value` off and the element's text content is inserted instead, which keeps whole clauses readable in markup.
|
|
33
|
+
|
|
34
|
+
```html:preview
|
|
35
|
+
<zn-textarea label="Policy" rows="6" help-text="Type / to insert">
|
|
36
|
+
<zn-slash-item icon="scale@lu" label="Governing law">This agreement is governed by the laws of {{JURISDICTION}}, and the parties submit to the exclusive jurisdiction of its courts.</zn-slash-item>
|
|
37
|
+
<zn-slash-item icon="undo-2@lu" label="Refund window">Refunds are available within {{REFUND_DAYS}} days of purchase.</zn-slash-item>
|
|
38
|
+
</zn-textarea>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Searching, Grouping And Ordering
|
|
42
|
+
|
|
43
|
+
`keywords` adds terms an item can be found by, `group` collects items under a heading, and `order` overrides the
|
|
44
|
+
position an item takes in the list.
|
|
45
|
+
|
|
46
|
+
```html:preview
|
|
47
|
+
<zn-textarea label="Grouped insertions" rows="5" help-text="Type / then try 'company'">
|
|
48
|
+
<zn-slash-item
|
|
49
|
+
group="Merchant"
|
|
50
|
+
icon="tag@lu"
|
|
51
|
+
label="Brand name"
|
|
52
|
+
keywords="company, trading"
|
|
53
|
+
value="{{BRAND_NAME}}"></zn-slash-item>
|
|
54
|
+
<zn-slash-item
|
|
55
|
+
group="Merchant"
|
|
56
|
+
icon="building@lu"
|
|
57
|
+
label="Legal entity"
|
|
58
|
+
keywords="company, registered"
|
|
59
|
+
value="{{LEGAL_ENTITY}}"></zn-slash-item>
|
|
60
|
+
<zn-slash-item
|
|
61
|
+
group="Customer"
|
|
62
|
+
icon="user@lu"
|
|
63
|
+
label="Customer name"
|
|
64
|
+
value="{{CUSTOMER_NAME}}"></zn-slash-item>
|
|
65
|
+
</zn-textarea>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Placing The Caret Inside An Insertion
|
|
69
|
+
|
|
70
|
+
`caret-offset` sets where the caret lands after insertion, as an offset into the inserted text. Here it lands between
|
|
71
|
+
the tags, ready for the conditional's body.
|
|
72
|
+
|
|
73
|
+
```html:preview
|
|
74
|
+
<zn-textarea label="Conditional block" rows="5" help-text="Type / to insert">
|
|
75
|
+
<zn-slash-item
|
|
76
|
+
icon="git-branch@lu"
|
|
77
|
+
label="If trial customer"
|
|
78
|
+
caret-offset="12"
|
|
79
|
+
value="{{IF_TRIAL}}{{END_IF}}"></zn-slash-item>
|
|
80
|
+
</zn-textarea>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Disabled Items
|
|
84
|
+
|
|
85
|
+
A disabled item is listed but cannot be chosen — useful for showing a replacement string that is not available in the
|
|
86
|
+
current context.
|
|
87
|
+
|
|
88
|
+
```html:preview
|
|
89
|
+
<zn-textarea label="Available insertions" rows="5" help-text="Type / to insert">
|
|
90
|
+
<zn-slash-item label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item>
|
|
91
|
+
<zn-slash-item
|
|
92
|
+
label="Invoice number"
|
|
93
|
+
description="Only available on invoice templates"
|
|
94
|
+
value="{{INVOICE_NUMBER}}"
|
|
95
|
+
disabled></zn-slash-item>
|
|
96
|
+
</zn-textarea>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Handling An Item Yourself
|
|
100
|
+
|
|
101
|
+
An item with an `action` and no `value` inserts nothing. Listen for `zn-slash-select`, call `preventDefault()`, and do
|
|
102
|
+
whatever the action means in your application. The typed trigger and query are removed either way — they are a command,
|
|
103
|
+
not content.
|
|
104
|
+
|
|
105
|
+
```html:preview
|
|
106
|
+
<zn-textarea id="action-textarea" label="Notes" rows="5" help-text="Type / to insert">
|
|
107
|
+
<zn-slash-item label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item>
|
|
108
|
+
<zn-slash-item icon="clock@lu" label="Timestamp" action="timestamp"></zn-slash-item>
|
|
109
|
+
</zn-textarea>
|
|
110
|
+
|
|
111
|
+
<script type="module">
|
|
112
|
+
const textarea = document.getElementById('action-textarea');
|
|
113
|
+
|
|
114
|
+
await customElements.whenDefined('zn-textarea');
|
|
115
|
+
|
|
116
|
+
textarea.addEventListener('zn-slash-select', (event) => {
|
|
117
|
+
if (event.detail.item.action !== 'timestamp') return;
|
|
118
|
+
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
textarea.setRangeText(new Date().toISOString());
|
|
121
|
+
textarea.focus();
|
|
122
|
+
});
|
|
123
|
+
</script>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
{% endraw %}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
meta:
|
|
3
|
+
title: Slash Menu
|
|
4
|
+
description: A keyboard-driven list of quick insertions, anchored to the caret of the field that opened it.
|
|
5
|
+
layout: component
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{% raw %}
|
|
9
|
+
|
|
10
|
+
`<zn-slash-menu>` is the panel behind a slash menu. Most of the time you don't use it directly — a component drives it
|
|
11
|
+
for you, as [`zn-textarea`](/components/textarea#slash-menu-quick-insertions) does with its
|
|
12
|
+
[`zn-slash-item`](/components/slash-item) entries:
|
|
13
|
+
|
|
14
|
+
```html:preview
|
|
15
|
+
<zn-textarea label="Terms and conditions" rows="5" help-text="Type / to insert a replacement string"
|
|
16
|
+
slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}"></zn-textarea>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Slot one into a textarea when you want the panel's own settings — `heading`, `max-items`, `placement`, `empty-text`, or
|
|
20
|
+
its width — declared in markup. The textarea then drives your menu instead of building its own:
|
|
21
|
+
|
|
22
|
+
```html:preview
|
|
23
|
+
<zn-textarea label="Terms and conditions" rows="6" help-text="Type / to insert">
|
|
24
|
+
<zn-slash-menu slot="slash-menu" heading="Replacement strings" max-items="6" style="--slash-menu-width: 360px">
|
|
25
|
+
<zn-slash-item icon="tag@lu" label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item>
|
|
26
|
+
<zn-slash-item icon="building@lu" label="Legal entity" value="{{LEGAL_ENTITY}}"></zn-slash-item>
|
|
27
|
+
<zn-slash-item icon="scale@lu" label="Jurisdiction" value="{{JURISDICTION}}"></zn-slash-item>
|
|
28
|
+
</zn-slash-menu>
|
|
29
|
+
</zn-textarea>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Reach for the component on its own when you are adding a slash menu to a control the library doesn't cover. It renders
|
|
33
|
+
and positions the list; the `SlashMenuController` watches a text field, tracks the query and inserts the result.
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
### Driving It Directly
|
|
38
|
+
|
|
39
|
+
Set `items`, position the panel with `anchor` (an element or a
|
|
40
|
+
[virtual element](https://floating-ui.com/docs/virtual-elements)), and call `show()`. The menu emits
|
|
41
|
+
`zn-slash-item-select` when an item is chosen. That event does not cross shadow boundaries, so a component that hosts
|
|
42
|
+
the menu in its shadow root re-emits it as its own public event.
|
|
43
|
+
|
|
44
|
+
```html:preview
|
|
45
|
+
<zn-button id="menu-anchor">Open the menu</zn-button>
|
|
46
|
+
<zn-slash-menu id="standalone-menu"></zn-slash-menu>
|
|
47
|
+
<div id="standalone-log" style="margin-top: 1rem; font-family: monospace; font-size: 0.875rem;"></div>
|
|
48
|
+
|
|
49
|
+
<script type="module">
|
|
50
|
+
const anchor = document.getElementById('menu-anchor');
|
|
51
|
+
const menu = document.getElementById('standalone-menu');
|
|
52
|
+
const log = document.getElementById('standalone-log');
|
|
53
|
+
|
|
54
|
+
await customElements.whenDefined('zn-slash-menu');
|
|
55
|
+
|
|
56
|
+
menu.items = [
|
|
57
|
+
{label: 'Brand name', value: '{{BRAND_NAME}}', icon: 'tag@lu', group: 'Merchant'},
|
|
58
|
+
{label: 'Legal entity', value: '{{LEGAL_ENTITY}}', icon: 'building@lu', group: 'Merchant'},
|
|
59
|
+
{label: 'Jurisdiction', value: '{{JURISDICTION}}', icon: 'scale@lu', group: 'Policy'}
|
|
60
|
+
];
|
|
61
|
+
menu.anchor = anchor;
|
|
62
|
+
|
|
63
|
+
anchor.addEventListener('click', () => menu.open ? menu.hide() : menu.show());
|
|
64
|
+
|
|
65
|
+
menu.addEventListener('zn-slash-item-select', (event) => {
|
|
66
|
+
log.textContent = `selected ${event.detail.item.label} → ${event.detail.item.value}`;
|
|
67
|
+
menu.hide();
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Keyboard Navigation
|
|
73
|
+
|
|
74
|
+
The menu doesn't listen for keys itself — whatever owns the field decides which keys belong to the menu, then calls
|
|
75
|
+
`moveActive()`, `selectActive()` and `hide()`. Disabled items are skipped, and moving past either end wraps around.
|
|
76
|
+
|
|
77
|
+
```html:preview
|
|
78
|
+
<zn-button id="nav-toggle">Toggle</zn-button>
|
|
79
|
+
<zn-button id="nav-up">↑</zn-button>
|
|
80
|
+
<zn-button id="nav-down">↓</zn-button>
|
|
81
|
+
<zn-button id="nav-select">Enter</zn-button>
|
|
82
|
+
<zn-slash-menu id="nav-menu"></zn-slash-menu>
|
|
83
|
+
<div id="nav-log" style="margin-top: 1rem; font-family: monospace; font-size: 0.875rem;"></div>
|
|
84
|
+
|
|
85
|
+
<script type="module">
|
|
86
|
+
const menu = document.getElementById('nav-menu');
|
|
87
|
+
const toggle = document.getElementById('nav-toggle');
|
|
88
|
+
const log = document.getElementById('nav-log');
|
|
89
|
+
|
|
90
|
+
await customElements.whenDefined('zn-slash-menu');
|
|
91
|
+
|
|
92
|
+
menu.items = [
|
|
93
|
+
{label: 'Brand name', value: '{{BRAND_NAME}}'},
|
|
94
|
+
{label: 'Not available here', value: '{{INVOICE_NUMBER}}', disabled: true},
|
|
95
|
+
{label: 'Jurisdiction', value: '{{JURISDICTION}}'}
|
|
96
|
+
];
|
|
97
|
+
menu.anchor = toggle;
|
|
98
|
+
|
|
99
|
+
toggle.addEventListener('click', () => menu.open ? menu.hide() : menu.show());
|
|
100
|
+
document.getElementById('nav-up').addEventListener('click', () => menu.moveActive(-1));
|
|
101
|
+
document.getElementById('nav-down').addEventListener('click', () => menu.moveActive(1));
|
|
102
|
+
document.getElementById('nav-select').addEventListener('click', () => menu.selectActive());
|
|
103
|
+
|
|
104
|
+
menu.addEventListener('zn-slash-item-select', (event) => {
|
|
105
|
+
log.textContent = `selected ${event.detail.item.label}`;
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Truncating Long Lists
|
|
111
|
+
|
|
112
|
+
`max-items` caps how many items are rendered; the rest are reported in a footer rather than silently dropped. The panel
|
|
113
|
+
scrolls when its content exceeds `--slash-menu-max-height`.
|
|
114
|
+
|
|
115
|
+
```html:preview
|
|
116
|
+
<zn-button id="capped-toggle">Show 3 of 9</zn-button>
|
|
117
|
+
<zn-slash-menu id="capped-menu" max-items="3"></zn-slash-menu>
|
|
118
|
+
|
|
119
|
+
<script type="module">
|
|
120
|
+
const menu = document.getElementById('capped-menu');
|
|
121
|
+
const toggle = document.getElementById('capped-toggle');
|
|
122
|
+
|
|
123
|
+
await customElements.whenDefined('zn-slash-menu');
|
|
124
|
+
|
|
125
|
+
menu.items = Array.from({length: 9}, (_, i) => ({label: `Replacement ${i + 1}`, value: `{{TOKEN_${i + 1}}}`}));
|
|
126
|
+
menu.anchor = toggle;
|
|
127
|
+
|
|
128
|
+
toggle.addEventListener('click', () => menu.open ? menu.hide() : menu.show());
|
|
129
|
+
</script>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Attaching It To Your Own Field
|
|
133
|
+
|
|
134
|
+
`SlashMenuController` is the reusable half. Give it the field, the menu, and the items; it handles trigger detection,
|
|
135
|
+
filtering, keyboard handling and insertion.
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
import {SlashMenuController} from '@kubex/zinc';
|
|
139
|
+
|
|
140
|
+
class MyEditor extends ZincElement {
|
|
141
|
+
private slash = new SlashMenuController(this, {
|
|
142
|
+
menu: () => this.shadowRoot.querySelector('zn-slash-menu'),
|
|
143
|
+
items: () => [{label: 'Brand name', value: '{{BRAND_NAME}}', icon: 'tag@lu'}],
|
|
144
|
+
trigger: () => '/',
|
|
145
|
+
onSelect: (item, query) => !this.emit('my-select', {detail: {item, query}}).defaultPrevented,
|
|
146
|
+
onInsert: (item, value) => this.emit('my-insert', {detail: {item, value}})
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
firstUpdated() {
|
|
150
|
+
this.slash.attach(this.shadowRoot.querySelector('textarea'));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Items can be shared between fields by registering them once as a preset:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import {registerSlashMenuPreset} from '@kubex/zinc';
|
|
159
|
+
|
|
160
|
+
registerSlashMenuPreset('legal', [
|
|
161
|
+
{label: 'Brand name', value: '{{BRAND_NAME}}', icon: 'tag@lu'},
|
|
162
|
+
{label: 'Jurisdiction', value: '{{JURISDICTION}}', icon: 'scale@lu'}
|
|
163
|
+
]);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Any component that reads presets — `zn-textarea` via `slash-preset="legal"` — then offers the same list.
|
|
167
|
+
|
|
168
|
+
{% endraw %}
|
|
@@ -408,6 +408,154 @@ Use methods to programmatically control the textarea.
|
|
|
408
408
|
</script>
|
|
409
409
|
```
|
|
410
410
|
|
|
411
|
+
### Slash Menu Quick Insertions
|
|
412
|
+
|
|
413
|
+
{% raw %}
|
|
414
|
+
|
|
415
|
+
Give a textarea a list of quick insertions and typing `/` opens a menu at the caret, in the same way the
|
|
416
|
+
[editor](/components/editor)'s context menu works. It is built for replacement strings — merge fields such as
|
|
417
|
+
`{{BRAND_NAME}}` in legal copy — but any text can be inserted.
|
|
418
|
+
|
|
419
|
+
The list can be declared as an attribute, which accepts the shorthand `Label={{TOKEN}}` for each entry:
|
|
420
|
+
|
|
421
|
+
```html:preview
|
|
422
|
+
<zn-textarea
|
|
423
|
+
label="Terms and conditions"
|
|
424
|
+
rows="6"
|
|
425
|
+
help-text="Type / to insert a replacement string"
|
|
426
|
+
slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}}, Support email={{SUPPORT_EMAIL}}">
|
|
427
|
+
This agreement is between you and {{LEGAL_ENTITY}}, trading as
|
|
428
|
+
</zn-textarea>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Type `/` at the start of a word, keep typing to filter, then choose an item with `↑`/`↓` and `Enter` (or `Tab`), or by
|
|
432
|
+
clicking it. `Escape` closes the menu without inserting and leaves the field focused. The trigger and the query that
|
|
433
|
+
follows it are replaced by the item's value.
|
|
434
|
+
|
|
435
|
+
#### Declaring Items With `zn-slash-item`
|
|
436
|
+
|
|
437
|
+
For richer entries — icons, descriptions, groups, or multi-line values — put [`zn-slash-item`](/components/slash-item)
|
|
438
|
+
elements inside the textarea. No `slot` attribute is needed: items are picked up wherever they sit. An item's text
|
|
439
|
+
content is inserted when it has no `value` attribute, which keeps long clauses readable in markup.
|
|
440
|
+
|
|
441
|
+
```html:preview
|
|
442
|
+
<zn-textarea label="Privacy policy" rows="8" help-text="Type / to insert">
|
|
443
|
+
<zn-slash-item
|
|
444
|
+
group="Merchant"
|
|
445
|
+
icon="tag@lu"
|
|
446
|
+
label="Brand name"
|
|
447
|
+
description="The merchant's trading name"
|
|
448
|
+
keywords="company, trading"
|
|
449
|
+
value="{{BRAND_NAME}}"></zn-slash-item>
|
|
450
|
+
<zn-slash-item
|
|
451
|
+
group="Merchant"
|
|
452
|
+
icon="mail@lu"
|
|
453
|
+
label="Support email"
|
|
454
|
+
value="{{SUPPORT_EMAIL}}"></zn-slash-item>
|
|
455
|
+
<zn-slash-item
|
|
456
|
+
group="Clauses"
|
|
457
|
+
icon="scale@lu"
|
|
458
|
+
label="Governing law"
|
|
459
|
+
description="Full clause, inserted inline">This agreement is governed by the laws of {{JURISDICTION}}, and the parties submit to the exclusive jurisdiction of its courts.</zn-slash-item>
|
|
460
|
+
</zn-textarea>
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### Slotting The Menu Itself
|
|
464
|
+
|
|
465
|
+
Wrap the items in a [`zn-slash-menu`](/components/slash-menu) on the `slash-menu` slot to keep the whole configuration
|
|
466
|
+
in one place and set the panel's own attributes — `heading`, `max-items`, `placement`, `empty-text` — in markup. The
|
|
467
|
+
textarea uses that menu instead of building its own, so anything you style on it applies.
|
|
468
|
+
|
|
469
|
+
```html:preview
|
|
470
|
+
<zn-textarea label="Terms and conditions" rows="7" help-text="Type / to insert">
|
|
471
|
+
<zn-slash-menu slot="slash-menu" heading="Replacement strings" max-items="6" style="--slash-menu-width: 360px">
|
|
472
|
+
<zn-slash-item icon="tag@lu" label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item>
|
|
473
|
+
<zn-slash-item icon="building@lu" label="Legal entity" value="{{LEGAL_ENTITY}}"></zn-slash-item>
|
|
474
|
+
<zn-slash-item icon="scale@lu" label="Jurisdiction" value="{{JURISDICTION}}"></zn-slash-item>
|
|
475
|
+
<zn-slash-item icon="mail@lu" label="Support email" value="{{SUPPORT_EMAIL}}"></zn-slash-item>
|
|
476
|
+
</zn-slash-menu>
|
|
477
|
+
</zn-textarea>
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
#### Reusable Presets
|
|
481
|
+
|
|
482
|
+
Register a list once and reference it by name from any textarea with `slash-preset`. This keeps a single definition of
|
|
483
|
+
the merge fields an application allows.
|
|
484
|
+
|
|
485
|
+
```html:preview
|
|
486
|
+
<zn-textarea
|
|
487
|
+
label="Refund policy"
|
|
488
|
+
rows="5"
|
|
489
|
+
slash-preset="legal"
|
|
490
|
+
help-text="Type / to insert a replacement string"></zn-textarea>
|
|
491
|
+
|
|
492
|
+
<script type="module">
|
|
493
|
+
import {registerSlashMenuPreset} from '/dist/zn.min.js';
|
|
494
|
+
|
|
495
|
+
registerSlashMenuPreset('legal', [
|
|
496
|
+
{label: 'Brand name', value: '{{BRAND_NAME}}', icon: 'tag@lu', group: 'Merchant'},
|
|
497
|
+
{label: 'Legal entity', value: '{{LEGAL_ENTITY}}', icon: 'building@lu', group: 'Merchant'},
|
|
498
|
+
{label: 'Refund window', value: '{{REFUND_DAYS}} days', icon: 'calendar@lu', group: 'Policy'},
|
|
499
|
+
{label: 'Jurisdiction', value: '{{JURISDICTION}}', icon: 'scale@lu', group: 'Policy'}
|
|
500
|
+
]);
|
|
501
|
+
</script>
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
#### Changing the Trigger
|
|
505
|
+
|
|
506
|
+
Set `slash-trigger` to any characters. Using `{{` lets someone who already knows the token they want type it directly
|
|
507
|
+
and get the list as they go.
|
|
508
|
+
|
|
509
|
+
```html:preview
|
|
510
|
+
<zn-textarea
|
|
511
|
+
label="Email footer"
|
|
512
|
+
rows="4"
|
|
513
|
+
slash-trigger="{{"
|
|
514
|
+
help-text="Type {{ to insert a replacement string"
|
|
515
|
+
slash-items="Brand name={{BRAND_NAME}}, Unsubscribe link={{UNSUBSCRIBE_URL}}"></zn-textarea>
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
#### Custom And Remote Items
|
|
519
|
+
|
|
520
|
+
`slashItemsProvider` resolves extra items each time the menu opens, so a list can come from an API. `zn-slash-select` is
|
|
521
|
+
cancelable: call `preventDefault()` to handle an item yourself instead of inserting its value, which is how items that
|
|
522
|
+
open a dialog or run a command are built. The trigger and query the user typed are removed either way.
|
|
523
|
+
|
|
524
|
+
```html:preview
|
|
525
|
+
<zn-textarea id="provider-textarea" label="Notes" rows="5" help-text="Type / to insert"></zn-textarea>
|
|
526
|
+
<div id="provider-log" style="margin-top: 1rem; font-family: monospace; font-size: 0.875rem;"></div>
|
|
527
|
+
|
|
528
|
+
<script type="module">
|
|
529
|
+
const textarea = document.getElementById('provider-textarea');
|
|
530
|
+
const log = document.getElementById('provider-log');
|
|
531
|
+
|
|
532
|
+
await customElements.whenDefined('zn-textarea');
|
|
533
|
+
|
|
534
|
+
textarea.slashItemsProvider = async (query) => {
|
|
535
|
+
// In a real application this would be a fetch
|
|
536
|
+
return [
|
|
537
|
+
{label: 'Today', value: new Date().toLocaleDateString(), icon: 'calendar@lu'},
|
|
538
|
+
{label: 'Clear the field', icon: 'trash-2@lu', action: 'clear', description: 'Handled by the page'}
|
|
539
|
+
];
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
textarea.addEventListener('zn-slash-select', (event) => {
|
|
543
|
+
log.textContent = `zn-slash-select: ${event.detail.item.label}`;
|
|
544
|
+
|
|
545
|
+
if (event.detail.item.action === 'clear') {
|
|
546
|
+
event.preventDefault();
|
|
547
|
+
textarea.value = '';
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
textarea.addEventListener('zn-slash-insert', (event) => {
|
|
552
|
+
log.textContent = `zn-slash-insert: ${event.detail.value}`;
|
|
553
|
+
});
|
|
554
|
+
</script>
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
{% endraw %}
|
|
558
|
+
|
|
411
559
|
### Selection and Range Methods
|
|
412
560
|
|
|
413
561
|
Use `setSelectionRange()` and `setRangeText()` methods to work with text selections.
|
|
@@ -323,6 +323,36 @@ A complete example showing product content management with translations.
|
|
|
323
323
|
</div>
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
+
### Slash Menu Quick Insertions
|
|
327
|
+
|
|
328
|
+
{% raw %}
|
|
329
|
+
|
|
330
|
+
Copy that carries replacement strings — `{{BRAND_NAME}}` and the like — needs the same tokens in every language. Set
|
|
331
|
+
`slash-items` alongside `input-type="textarea"` and typing `/` offers them at the caret, in whichever language is
|
|
332
|
+
being edited. The attribute takes the same shorthand and JSON that
|
|
333
|
+
[`zn-textarea`](/components/textarea#slash-menu-quick-insertions) accepts, and `slash-preset`, `slash-trigger` and
|
|
334
|
+
`slash-heading` are forwarded too.
|
|
335
|
+
|
|
336
|
+
```html:preview
|
|
337
|
+
<zn-translations
|
|
338
|
+
label="Confirmation message"
|
|
339
|
+
input-type="textarea"
|
|
340
|
+
textarea-rows="4"
|
|
341
|
+
languages='{"en": "EN", "fr": "FR", "de": "DE"}'
|
|
342
|
+
values='{"en": "Look for /"}'
|
|
343
|
+
slash-heading="Replacement strings"
|
|
344
|
+
slash-items='[
|
|
345
|
+
{"label": "Brand name", "value": "{{BRAND_NAME}}", "description": "The product / company name", "icon": "sell"},
|
|
346
|
+
{"label": "Customer email", "value": "{{CUSTOMER_EMAIL}}", "description": "The customer's email address", "icon": "mail"},
|
|
347
|
+
{"label": "Renewal price", "value": "{{RENEWAL_PRICE}}", "description": "The renewal price amount", "icon": "payments"}
|
|
348
|
+
]'></zn-translations>
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The menu claims `↑`, `↓`, `Enter`, `Tab` and `Escape` while it is open, so choosing an item never submits the form the
|
|
352
|
+
way `Enter` otherwise would.
|
|
353
|
+
|
|
354
|
+
{% endraw %}
|
|
355
|
+
|
|
326
356
|
## Properties
|
|
327
357
|
|
|
328
358
|
| Property | Type | Default | Description |
|
|
@@ -335,6 +365,10 @@ A complete example showing product content management with translations.
|
|
|
335
365
|
| `flush` | `boolean` | `false` | Removes padding for compact layout |
|
|
336
366
|
| `languages` | `Record<string, string>` | `{en: "EN"}` | Object mapping language codes to display names |
|
|
337
367
|
| `values` | `Record<string, string>` | `{}` | Object mapping language codes to translation text |
|
|
368
|
+
| `slash-items` | `SlashMenuItem[]` | `[]` | Quick insertions offered by the slash menu on a `textarea` input |
|
|
369
|
+
| `slash-preset` | `string` | `''` | Registered item sets to offer, comma separated |
|
|
370
|
+
| `slash-trigger` | `string` | `'/'` | The characters that open the slash menu |
|
|
371
|
+
| `slash-heading` | `string` | `'Insert'` | Heading shown above the slash menu's items |
|
|
338
372
|
|
|
339
373
|
## Events
|
|
340
374
|
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import { defaultValue } from "../../internal/default-value";
|
|
|
5
5
|
import { FormControlController } from "../../internal/form";
|
|
6
6
|
import { HasSlotController } from "../../internal/slot";
|
|
7
7
|
import { ifDefined } from "lit/directives/if-defined.js";
|
|
8
|
+
import { parseSlashItems, type SlashMenuItem } from "../slash-menu/slash-menu-items";
|
|
8
9
|
import { property, query, state } from 'lit/decorators.js';
|
|
9
10
|
import { watch } from "../../internal/watch";
|
|
10
11
|
import ZincElement from '../../internal/zinc-element';
|
|
@@ -97,6 +98,31 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
97
98
|
@property({ attribute: "input-type" }) inputType: 'select' | 'text' | 'data-select' | 'number' | 'textarea' = 'text';
|
|
98
99
|
@property({ attribute: "textarea-rows", type: Number }) textareaRows: 1;
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Quick insertions offered by the slash menu of a `textarea` input. Accepts a JSON array of items, or the
|
|
103
|
+
* shorthand `Brand name={{BRAND_NAME}}, Support email={{SUPPORT_EMAIL}}`. Forwarded to the inner `zn-textarea`.
|
|
104
|
+
*/
|
|
105
|
+
@property({
|
|
106
|
+
attribute: 'slash-items',
|
|
107
|
+
converter: {
|
|
108
|
+
fromAttribute: (value: string) => parseSlashItems(value),
|
|
109
|
+
toAttribute: (value: SlashMenuItem[]) => JSON.stringify(value)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
slashItems: SlashMenuItem[] = [];
|
|
113
|
+
|
|
114
|
+
/** Names of item sets registered with `registerSlashMenuPreset`, comma separated. */
|
|
115
|
+
@property({ attribute: 'slash-preset' }) slashPreset = '';
|
|
116
|
+
|
|
117
|
+
/** The characters that open the slash menu. */
|
|
118
|
+
@property({ attribute: 'slash-trigger' }) slashTrigger = '/';
|
|
119
|
+
|
|
120
|
+
/** The heading shown above the slash menu's items. */
|
|
121
|
+
@property({ attribute: 'slash-heading' }) slashHeading = 'Insert';
|
|
122
|
+
|
|
123
|
+
/** Resolves additional slash menu items each time the menu opens. JavaScript only. */
|
|
124
|
+
@property({ attribute: false }) slashItemsProvider?: (query: string) => SlashMenuItem[] | Promise<SlashMenuItem[]>;
|
|
125
|
+
|
|
100
126
|
@property({ type: Object }) options: { [key: string]: string } = {};
|
|
101
127
|
|
|
102
128
|
@property({ attribute: 'provider' }) selectProvider: string;
|
|
@@ -434,6 +460,11 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
434
460
|
pattern=${ifDefined(this.pattern)}
|
|
435
461
|
autocomplete=${ifDefined(this.autocomplete)}
|
|
436
462
|
dir="${this.dir}"
|
|
463
|
+
slash-trigger="${this.slashTrigger}"
|
|
464
|
+
slash-heading="${this.slashHeading}"
|
|
465
|
+
slash-preset="${this.slashPreset}"
|
|
466
|
+
.slashItems="${this.slashItems}"
|
|
467
|
+
.slashItemsProvider="${this.slashItemsProvider}"
|
|
437
468
|
@zn-input="${this.handleInput}"
|
|
438
469
|
@zn-blur="${this.handleBlur}">
|
|
439
470
|
</zn-textarea>`;
|