tosijs-ui 1.0.0
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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/ab-test.d.ts +14 -0
- package/dist/ab-test.js +116 -0
- package/dist/babylon-3d.d.ts +53 -0
- package/dist/babylon-3d.js +292 -0
- package/dist/bodymovin-player.d.ts +32 -0
- package/dist/bodymovin-player.js +172 -0
- package/dist/bp-loader.d.ts +1 -0
- package/dist/bp-loader.js +26 -0
- package/dist/carousel.d.ts +113 -0
- package/dist/carousel.js +308 -0
- package/dist/code-editor.d.ts +27 -0
- package/dist/code-editor.js +102 -0
- package/dist/color-input.d.ts +41 -0
- package/dist/color-input.js +112 -0
- package/dist/data-table.d.ts +79 -0
- package/dist/data-table.js +774 -0
- package/dist/drag-and-drop.d.ts +2 -0
- package/dist/drag-and-drop.js +386 -0
- package/dist/editable-rect.d.ts +97 -0
- package/dist/editable-rect.js +450 -0
- package/dist/filter-builder.d.ts +64 -0
- package/dist/filter-builder.js +468 -0
- package/dist/float.d.ts +18 -0
- package/dist/float.js +170 -0
- package/dist/form.d.ts +68 -0
- package/dist/form.js +466 -0
- package/dist/gamepad.d.ts +34 -0
- package/dist/gamepad.js +115 -0
- package/dist/icon-data.d.ts +312 -0
- package/dist/icon-data.js +308 -0
- package/dist/icon-types.d.ts +7 -0
- package/dist/icon-types.js +1 -0
- package/dist/icons.d.ts +17 -0
- package/dist/icons.js +374 -0
- package/dist/iife.js +69 -0
- package/dist/iife.js.map +49 -0
- package/dist/index-iife.d.ts +1 -0
- package/dist/index-iife.js +4 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +47 -0
- package/dist/live-example.d.ts +63 -0
- package/dist/live-example.js +611 -0
- package/dist/localize.d.ts +46 -0
- package/dist/localize.js +381 -0
- package/dist/make-sorter.d.ts +3 -0
- package/dist/make-sorter.js +119 -0
- package/dist/make-sorter.test.d.ts +1 -0
- package/dist/make-sorter.test.js +48 -0
- package/dist/mapbox.d.ts +24 -0
- package/dist/mapbox.js +161 -0
- package/dist/markdown-viewer.d.ts +17 -0
- package/dist/markdown-viewer.js +173 -0
- package/dist/match-shortcut.d.ts +9 -0
- package/dist/match-shortcut.js +13 -0
- package/dist/match-shortcut.test.d.ts +1 -0
- package/dist/match-shortcut.test.js +194 -0
- package/dist/menu.d.ts +60 -0
- package/dist/menu.js +614 -0
- package/dist/notifications.d.ts +106 -0
- package/dist/notifications.js +308 -0
- package/dist/password-strength.d.ts +35 -0
- package/dist/password-strength.js +302 -0
- package/dist/playwright.config.d.ts +9 -0
- package/dist/playwright.config.js +73 -0
- package/dist/pop-float.d.ts +10 -0
- package/dist/pop-float.js +231 -0
- package/dist/rating.d.ts +62 -0
- package/dist/rating.js +192 -0
- package/dist/rich-text.d.ts +35 -0
- package/dist/rich-text.js +296 -0
- package/dist/segmented.d.ts +80 -0
- package/dist/segmented.js +298 -0
- package/dist/select.d.ts +43 -0
- package/dist/select.js +427 -0
- package/dist/side-nav.d.ts +36 -0
- package/dist/side-nav.js +106 -0
- package/dist/size-break.d.ts +18 -0
- package/dist/size-break.js +118 -0
- package/dist/sizer.d.ts +34 -0
- package/dist/sizer.js +92 -0
- package/dist/src/ab-test.d.ts +14 -0
- package/dist/src/babylon-3d.d.ts +53 -0
- package/dist/src/bodymovin-player.d.ts +32 -0
- package/dist/src/bp-loader.d.ts +0 -0
- package/dist/src/carousel.d.ts +113 -0
- package/dist/src/code-editor.d.ts +27 -0
- package/dist/src/color-input.d.ts +41 -0
- package/dist/src/data-table.d.ts +79 -0
- package/dist/src/drag-and-drop.d.ts +2 -0
- package/dist/src/editable-rect.d.ts +97 -0
- package/dist/src/filter-builder.d.ts +64 -0
- package/dist/src/float.d.ts +18 -0
- package/dist/src/form.d.ts +68 -0
- package/dist/src/gamepad.d.ts +34 -0
- package/dist/src/icon-data.d.ts +309 -0
- package/dist/src/icon-types.d.ts +7 -0
- package/dist/src/icons.d.ts +17 -0
- package/dist/src/index.d.ts +37 -0
- package/dist/src/live-example.d.ts +51 -0
- package/dist/src/localize.d.ts +30 -0
- package/dist/src/make-sorter.d.ts +3 -0
- package/dist/src/mapbox.d.ts +24 -0
- package/dist/src/markdown-viewer.d.ts +15 -0
- package/dist/src/match-shortcut.d.ts +9 -0
- package/dist/src/menu.d.ts +60 -0
- package/dist/src/notifications.d.ts +106 -0
- package/dist/src/password-strength.d.ts +35 -0
- package/dist/src/pop-float.d.ts +10 -0
- package/dist/src/rating.d.ts +62 -0
- package/dist/src/rich-text.d.ts +28 -0
- package/dist/src/segmented.d.ts +80 -0
- package/dist/src/select.d.ts +43 -0
- package/dist/src/side-nav.d.ts +36 -0
- package/dist/src/size-break.d.ts +18 -0
- package/dist/src/sizer.d.ts +34 -0
- package/dist/src/tab-selector.d.ts +91 -0
- package/dist/src/tag-list.d.ts +37 -0
- package/dist/src/track-drag.d.ts +5 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/via-tag.d.ts +2 -0
- package/dist/tab-selector.d.ts +91 -0
- package/dist/tab-selector.js +326 -0
- package/dist/tag-list.d.ts +37 -0
- package/dist/tag-list.js +375 -0
- package/dist/track-drag.d.ts +5 -0
- package/dist/track-drag.js +143 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/via-tag.d.ts +2 -0
- package/dist/via-tag.js +102 -0
- package/package.json +58 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/*#
|
|
2
|
+
# word (rich text editor)
|
|
3
|
+
|
|
4
|
+
`<xin-word>` is a simple and easily extensible `document.execCommand` WYSIWYG editor with some conveniences.
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<xin-word widgets="minimal">
|
|
8
|
+
<h3>Heading</h3>
|
|
9
|
+
<p>And some <b>text</b></p>
|
|
10
|
+
</xin-word>
|
|
11
|
+
```
|
|
12
|
+
```css
|
|
13
|
+
xin-word {
|
|
14
|
+
background: white;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
xin-word [part="toolbar"] {
|
|
18
|
+
background: #f8f8f8;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
xin-word [part="doc"] {
|
|
22
|
+
padding: 20px;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
By default, `<xin-word>` treats its initial contents as its document, but you can also set (and get)
|
|
27
|
+
its `value`.
|
|
28
|
+
|
|
29
|
+
## toolbar
|
|
30
|
+
|
|
31
|
+
`<xin-word>` elements have a `toolbar` slot (actually a xin-slot because it doesn't use
|
|
32
|
+
the shadowDOM).
|
|
33
|
+
|
|
34
|
+
If you set the `widgets` attribute to `default` or `minimal` you will get a toolbar
|
|
35
|
+
for free. Or you can add your own custom widgets.
|
|
36
|
+
|
|
37
|
+
## helper functions
|
|
38
|
+
|
|
39
|
+
A number of helper functions are available, including:
|
|
40
|
+
|
|
41
|
+
- `commandButton(title: string, command: string, iconClass: string)`
|
|
42
|
+
- `blockStyle(options: Array<{caption: string, tagType: string}>)`
|
|
43
|
+
- `spacer(width = '10px')`
|
|
44
|
+
- `elastic(width = '10px')`
|
|
45
|
+
|
|
46
|
+
These each create a toolbar widget. A `blockStyle`-generated `<select>` element will
|
|
47
|
+
automatically have its value changed based on the current selection.
|
|
48
|
+
|
|
49
|
+
## properties
|
|
50
|
+
|
|
51
|
+
A `<xin-word>` element also has `selectedText` and `selectedBlocks` properties, allowing
|
|
52
|
+
you to easily perform operations on text selections, and a `selectionChange` callback (which
|
|
53
|
+
simply passes through document `selectionchange` events, but also passes a reference to
|
|
54
|
+
the `<xin-word>` component).
|
|
55
|
+
*/
|
|
56
|
+
import { Component as WebComponent, elements } from 'xinjs';
|
|
57
|
+
import { icons } from './icons';
|
|
58
|
+
import { xinSelect, XinSelect } from './select';
|
|
59
|
+
const { xinSlot, div, button, span } = elements;
|
|
60
|
+
const blockStyles = [
|
|
61
|
+
{
|
|
62
|
+
caption: 'Title',
|
|
63
|
+
tagType: 'H1',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
caption: 'Heading',
|
|
67
|
+
tagType: 'H2',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
caption: 'Subheading',
|
|
71
|
+
tagType: 'H3',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
caption: 'Minor heading',
|
|
75
|
+
tagType: 'H4',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
caption: 'Body',
|
|
79
|
+
tagType: 'P',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
caption: 'Code Block',
|
|
83
|
+
tagType: 'PRE',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
export function blockStyle(options = blockStyles) {
|
|
87
|
+
return xinSelect({
|
|
88
|
+
title: 'paragraph style',
|
|
89
|
+
slot: 'toolbar',
|
|
90
|
+
class: 'block-style',
|
|
91
|
+
options: options.map(({ caption, tagType }) => ({
|
|
92
|
+
caption,
|
|
93
|
+
value: `formatBlock,${tagType}`,
|
|
94
|
+
})),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function spacer(width = '10px') {
|
|
98
|
+
return span({
|
|
99
|
+
slot: 'toolbar',
|
|
100
|
+
style: { flex: `0 0 ${width}`, content: ' ' },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function elastic(width = '10px') {
|
|
104
|
+
return span({
|
|
105
|
+
slot: 'toolbar',
|
|
106
|
+
style: { flex: `0 0 ${width}`, content: ' ' },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function commandButton(title, dataCommand, icon) {
|
|
110
|
+
return button({ slot: 'toolbar', dataCommand, title }, icon);
|
|
111
|
+
}
|
|
112
|
+
const paragraphStyleWidgets = () => [
|
|
113
|
+
commandButton('left-justify', 'justifyLeft', icons.alignLeft()),
|
|
114
|
+
commandButton('center', 'justifyCenter', icons.alignCenter()),
|
|
115
|
+
commandButton('right-justify', 'justifyRight', icons.alignRight()),
|
|
116
|
+
spacer(),
|
|
117
|
+
commandButton('bullet list', 'insertUnorderedList', icons.listBullet()),
|
|
118
|
+
commandButton('numbered list', 'insertOrderedList', icons.listNumber()),
|
|
119
|
+
spacer(),
|
|
120
|
+
commandButton('indent', 'indent', icons.blockIndent()),
|
|
121
|
+
commandButton('indent', 'outdent', icons.blockOutdent()),
|
|
122
|
+
];
|
|
123
|
+
const characterStyleWidgets = () => [
|
|
124
|
+
commandButton('bold', 'bold', icons.fontBold()),
|
|
125
|
+
commandButton('italic', 'italic', icons.fontItalic()),
|
|
126
|
+
commandButton('underline', 'underline', icons.fontUnderline()),
|
|
127
|
+
];
|
|
128
|
+
const minimalWidgets = () => [
|
|
129
|
+
blockStyle(),
|
|
130
|
+
spacer(),
|
|
131
|
+
...characterStyleWidgets(),
|
|
132
|
+
];
|
|
133
|
+
export const richTextWidgets = () => [
|
|
134
|
+
blockStyle(),
|
|
135
|
+
spacer(),
|
|
136
|
+
...paragraphStyleWidgets(),
|
|
137
|
+
spacer(),
|
|
138
|
+
...characterStyleWidgets(),
|
|
139
|
+
];
|
|
140
|
+
export class RichText extends WebComponent {
|
|
141
|
+
widgets = 'default';
|
|
142
|
+
isInitialized = false;
|
|
143
|
+
get value() {
|
|
144
|
+
return this.isInitialized
|
|
145
|
+
? this.parts.doc.innerHTML
|
|
146
|
+
: this.savedValue || this.innerHTML;
|
|
147
|
+
}
|
|
148
|
+
set value(docHtml) {
|
|
149
|
+
if (this.isInitialized) {
|
|
150
|
+
this.parts.doc.innerHTML = docHtml;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.innerHTML = docHtml;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
blockElement(elt) {
|
|
157
|
+
const { doc } = this.parts;
|
|
158
|
+
while (elt.parentElement !== null && elt.parentElement !== doc) {
|
|
159
|
+
elt = elt.parentElement;
|
|
160
|
+
}
|
|
161
|
+
return elt.parentElement === doc ? elt : undefined;
|
|
162
|
+
}
|
|
163
|
+
get selectedBlocks() {
|
|
164
|
+
const { doc } = this.parts;
|
|
165
|
+
const selObject = window.getSelection();
|
|
166
|
+
if (selObject === null) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const blocks = [];
|
|
170
|
+
for (let i = 0; i < selObject.rangeCount; i++) {
|
|
171
|
+
const range = selObject.getRangeAt(i);
|
|
172
|
+
if (!doc.contains(range.commonAncestorContainer)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
let block = this.blockElement(range.startContainer);
|
|
176
|
+
const lastBlock = this.blockElement(range.endContainer);
|
|
177
|
+
blocks.push(block);
|
|
178
|
+
while (block !== lastBlock && block !== null) {
|
|
179
|
+
block = block.nextElementSibling;
|
|
180
|
+
blocks.push(block);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return blocks;
|
|
184
|
+
}
|
|
185
|
+
get selectedText() {
|
|
186
|
+
const selObject = window.getSelection();
|
|
187
|
+
if (selObject === null) {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
return this.selectedBlocks.length ? selObject.toString() : '';
|
|
191
|
+
}
|
|
192
|
+
selectionChange = () => {
|
|
193
|
+
/* no not care */
|
|
194
|
+
};
|
|
195
|
+
handleSelectChange = (event) => {
|
|
196
|
+
// @ts-expect-error Typescript is wrong about event.target lacking closest
|
|
197
|
+
const select = event.target.closest(XinSelect.tagName);
|
|
198
|
+
if (select == null) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.doCommand(select.value);
|
|
202
|
+
};
|
|
203
|
+
handleButtonClick = (event) => {
|
|
204
|
+
// @ts-expect-error Typescript is wrong about event.target lacking closest
|
|
205
|
+
const button = event.target.closest('button');
|
|
206
|
+
if (button == null) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.doCommand(button.dataset.command);
|
|
210
|
+
};
|
|
211
|
+
content = [
|
|
212
|
+
xinSlot({
|
|
213
|
+
name: 'toolbar',
|
|
214
|
+
part: 'toolbar',
|
|
215
|
+
onClick: this.handleButtonClick,
|
|
216
|
+
onChange: this.handleSelectChange,
|
|
217
|
+
}),
|
|
218
|
+
div({
|
|
219
|
+
part: 'doc',
|
|
220
|
+
contenteditable: true,
|
|
221
|
+
style: {
|
|
222
|
+
flex: '1 1 auto',
|
|
223
|
+
outline: 'none',
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
xinSlot({
|
|
227
|
+
part: 'content',
|
|
228
|
+
}),
|
|
229
|
+
];
|
|
230
|
+
constructor() {
|
|
231
|
+
super();
|
|
232
|
+
this.initAttributes('widgets');
|
|
233
|
+
}
|
|
234
|
+
doCommand(command) {
|
|
235
|
+
if (command === undefined) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const args = command.split(',');
|
|
239
|
+
console.log('execCommand', args[0], false, ...args.slice(1));
|
|
240
|
+
document.execCommand(args[0], false, ...args.slice(1));
|
|
241
|
+
}
|
|
242
|
+
updateBlockStyle() {
|
|
243
|
+
const select = this.parts.toolbar.querySelector('.block-style');
|
|
244
|
+
if (select === null) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
let blockTags = this.selectedBlocks.map((block) => block.tagName);
|
|
248
|
+
blockTags = [...new Set(blockTags)];
|
|
249
|
+
select.value = blockTags.length === 1 ? `formatBlock,${blockTags[0]}` : '';
|
|
250
|
+
}
|
|
251
|
+
connectedCallback() {
|
|
252
|
+
super.connectedCallback();
|
|
253
|
+
const { doc, content } = this.parts;
|
|
254
|
+
if (content.innerHTML !== '' && doc.innerHTML === '') {
|
|
255
|
+
doc.innerHTML = content.innerHTML;
|
|
256
|
+
content.innerHTML = '';
|
|
257
|
+
}
|
|
258
|
+
this.isInitialized = true;
|
|
259
|
+
content.style.display = 'none';
|
|
260
|
+
document.addEventListener('selectionchange', (event) => {
|
|
261
|
+
this.updateBlockStyle();
|
|
262
|
+
this.selectionChange(event, this);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
render() {
|
|
266
|
+
const { toolbar } = this.parts;
|
|
267
|
+
super.render();
|
|
268
|
+
if (toolbar.children.length === 0) {
|
|
269
|
+
switch (this.widgets) {
|
|
270
|
+
case 'minimal':
|
|
271
|
+
toolbar.append(...minimalWidgets());
|
|
272
|
+
break;
|
|
273
|
+
case 'default':
|
|
274
|
+
toolbar.append(...richTextWidgets());
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export const richText = RichText.elementCreator({
|
|
281
|
+
tag: 'xin-word',
|
|
282
|
+
styleSpec: {
|
|
283
|
+
':host': {
|
|
284
|
+
display: 'flex',
|
|
285
|
+
flexDirection: 'column',
|
|
286
|
+
height: '100%',
|
|
287
|
+
},
|
|
288
|
+
':host [part="toolbar"]': {
|
|
289
|
+
padding: '4px',
|
|
290
|
+
display: 'flex',
|
|
291
|
+
gap: '0px',
|
|
292
|
+
flex: '0 0 auto',
|
|
293
|
+
flexWrap: 'wrap',
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Component as WebComponent, ElementCreator } from 'tosijs';
|
|
2
|
+
interface Choice {
|
|
3
|
+
icon?: string | SVGElement;
|
|
4
|
+
value: string;
|
|
5
|
+
caption: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class XinSegmented extends WebComponent {
|
|
8
|
+
choices: string | Choice[];
|
|
9
|
+
other: string;
|
|
10
|
+
multiple: boolean;
|
|
11
|
+
name: string;
|
|
12
|
+
placeholder: string;
|
|
13
|
+
localized: boolean;
|
|
14
|
+
value: null | string;
|
|
15
|
+
get values(): string[];
|
|
16
|
+
content: () => (HTMLDivElement | HTMLSlotElement)[];
|
|
17
|
+
static styleSpec: {
|
|
18
|
+
':host': {
|
|
19
|
+
display: string;
|
|
20
|
+
gap: string;
|
|
21
|
+
alignItems: string;
|
|
22
|
+
};
|
|
23
|
+
':host, :host::part(options)': {
|
|
24
|
+
flexDirection: string;
|
|
25
|
+
};
|
|
26
|
+
':host label': {
|
|
27
|
+
display: string;
|
|
28
|
+
alignItems: string;
|
|
29
|
+
gap: string;
|
|
30
|
+
gridTemplateColumns: string;
|
|
31
|
+
padding: string;
|
|
32
|
+
font: string;
|
|
33
|
+
};
|
|
34
|
+
':host label:has(:checked)': {
|
|
35
|
+
color: string;
|
|
36
|
+
background: string;
|
|
37
|
+
};
|
|
38
|
+
':host svg': {
|
|
39
|
+
height: string;
|
|
40
|
+
stroke: string;
|
|
41
|
+
};
|
|
42
|
+
':host label.no-icon': {
|
|
43
|
+
gap: number;
|
|
44
|
+
gridTemplateColumns: string;
|
|
45
|
+
};
|
|
46
|
+
':host input[type="radio"], :host input[type="checkbox"]': {
|
|
47
|
+
visibility: string;
|
|
48
|
+
};
|
|
49
|
+
':host::part(options)': {
|
|
50
|
+
display: string;
|
|
51
|
+
borderRadius: string;
|
|
52
|
+
background: string;
|
|
53
|
+
color: string;
|
|
54
|
+
overflow: string;
|
|
55
|
+
alignItems: string;
|
|
56
|
+
};
|
|
57
|
+
':host::part(custom)': {
|
|
58
|
+
padding: string;
|
|
59
|
+
color: string;
|
|
60
|
+
background: string;
|
|
61
|
+
font: string;
|
|
62
|
+
border: string;
|
|
63
|
+
outline: string;
|
|
64
|
+
};
|
|
65
|
+
':host::part(custom)::placeholder': {
|
|
66
|
+
color: string;
|
|
67
|
+
opacity: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
constructor();
|
|
71
|
+
private valueChanged;
|
|
72
|
+
handleChange: () => void;
|
|
73
|
+
handleKey: (event: KeyboardEvent) => void;
|
|
74
|
+
connectedCallback(): void;
|
|
75
|
+
private get _choices();
|
|
76
|
+
get isOtherValue(): boolean;
|
|
77
|
+
render(): void;
|
|
78
|
+
}
|
|
79
|
+
export declare const xinSegmented: ElementCreator<XinSegmented>;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/*#
|
|
2
|
+
# segmented select
|
|
3
|
+
|
|
4
|
+
This is a fairly general-purpose segmented select control.
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<blockquote>
|
|
8
|
+
Check the console to see the values being set.
|
|
9
|
+
</blockquote>
|
|
10
|
+
|
|
11
|
+
<div class="grid">
|
|
12
|
+
<xin-segmented value="yes" choices="yes, no, don't care">
|
|
13
|
+
Should we?
|
|
14
|
+
</xin-segmented>
|
|
15
|
+
|
|
16
|
+
<div>
|
|
17
|
+
<b>Localized!</b><br>
|
|
18
|
+
<xin-segmented
|
|
19
|
+
localized
|
|
20
|
+
title="do you like?"
|
|
21
|
+
choices="yes=Yes:thumbsUp, no=No:thumbsDown"
|
|
22
|
+
></xin-segmented>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<xin-segmented
|
|
26
|
+
style="--segmented-direction: column; --segmented-align-items: stretch"
|
|
27
|
+
choices="in a relationship, single" other="it's complicated…"
|
|
28
|
+
placeholder="oooh… please elaborate"
|
|
29
|
+
value="separated"
|
|
30
|
+
>
|
|
31
|
+
Relationship Status
|
|
32
|
+
</xin-segmented>
|
|
33
|
+
|
|
34
|
+
<xin-segmented
|
|
35
|
+
multiple
|
|
36
|
+
style="
|
|
37
|
+
--segmented-direction: column;
|
|
38
|
+
--segmented-align-items: start;
|
|
39
|
+
--segmented-option-grid-columns: 24px 24px 100px;
|
|
40
|
+
--segmented-input-visibility: visible;
|
|
41
|
+
"
|
|
42
|
+
choices="star=Star:star, game=Game:game, bug=Bug:bug, camera=Camera:camera"
|
|
43
|
+
value="star,bug"
|
|
44
|
+
>
|
|
45
|
+
Pick all that apply
|
|
46
|
+
</xin-segmented>
|
|
47
|
+
</div>
|
|
48
|
+
```
|
|
49
|
+
```css
|
|
50
|
+
.preview .grid {
|
|
51
|
+
--segmented-option-current-background: var(--brand-color);
|
|
52
|
+
--segmented-option-current-color: var(--brand-text-color);
|
|
53
|
+
padding: 16px;
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-columns: 1fr 1fr;
|
|
56
|
+
gap: 16px;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
```js
|
|
60
|
+
function logEvent(event) {
|
|
61
|
+
const { target } = event
|
|
62
|
+
if (target.tagName === 'XIN-SEGMENTED') {
|
|
63
|
+
console.log((target.textContent || target.title).trim(), target.value)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
preview.addEventListener('change', logEvent, true)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Properties
|
|
70
|
+
|
|
71
|
+
- `values` is an array of values (only really useful if `multiple` is set to true)
|
|
72
|
+
|
|
73
|
+
You can set `choices` programmatically to an array of `Choice` objects:
|
|
74
|
+
|
|
75
|
+
interface Choice {
|
|
76
|
+
icon?: string | SVGElement
|
|
77
|
+
value: string
|
|
78
|
+
caption: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
## Attributes
|
|
82
|
+
|
|
83
|
+
- `choices` is a string of comma-delimited options of the form `value=caption:icon`,
|
|
84
|
+
where caption and icon are optional
|
|
85
|
+
- `multiple` allows multiple selection
|
|
86
|
+
- `name` allows you to set the name of the `<input>` elements to a specific value, it will default
|
|
87
|
+
to the component's `instanceId`
|
|
88
|
+
- `other` (default '', meaning other is not allowed) is the caption for other options, allowing
|
|
89
|
+
the user to input their choice. It will be reset to '' if `multiple` is set.
|
|
90
|
+
- `placeholder` is the placeholder displayed in the `<input>` field for **other** responses
|
|
91
|
+
- `localized` automatically localizes captions
|
|
92
|
+
|
|
93
|
+
## Styling
|
|
94
|
+
|
|
95
|
+
The following CSS variables can be used to control customize the `<xin-segmented>` component.
|
|
96
|
+
|
|
97
|
+
--segmented-align-items
|
|
98
|
+
--segmented-direction
|
|
99
|
+
--segmented-option-color
|
|
100
|
+
--segmented-option-current-background
|
|
101
|
+
--segmented-option-current-color
|
|
102
|
+
--segmented-option-font
|
|
103
|
+
--segmented-option-gap
|
|
104
|
+
--segmented-option-grid-columns
|
|
105
|
+
--segmented-option-icon-color
|
|
106
|
+
--segmented-option-padding
|
|
107
|
+
--segmented-options-background
|
|
108
|
+
--segmented-options-border-radius
|
|
109
|
+
--segmented-placeholder-opacity
|
|
110
|
+
*/
|
|
111
|
+
import { Component as WebComponent, elements, varDefault, } from 'xinjs';
|
|
112
|
+
import { icons } from './icons';
|
|
113
|
+
import { xinLocalized } from './localize';
|
|
114
|
+
const { div, slot, label, span, input } = elements;
|
|
115
|
+
export class XinSegmented extends WebComponent {
|
|
116
|
+
choices = '';
|
|
117
|
+
other = '';
|
|
118
|
+
multiple = false;
|
|
119
|
+
name = '';
|
|
120
|
+
placeholder = 'Please specify…';
|
|
121
|
+
localized = false;
|
|
122
|
+
value = null;
|
|
123
|
+
get values() {
|
|
124
|
+
return (this.value || '')
|
|
125
|
+
.split(',')
|
|
126
|
+
.map((v) => v.trim())
|
|
127
|
+
.filter((v) => v !== '');
|
|
128
|
+
}
|
|
129
|
+
content = () => [
|
|
130
|
+
slot(),
|
|
131
|
+
div({ part: 'options' }, input({ part: 'custom', hidden: true })),
|
|
132
|
+
];
|
|
133
|
+
static styleSpec = {
|
|
134
|
+
':host': {
|
|
135
|
+
display: 'inline-flex',
|
|
136
|
+
gap: varDefault.segmentedOptionGap('8px'),
|
|
137
|
+
alignItems: varDefault.segmentedAlignItems('center'),
|
|
138
|
+
},
|
|
139
|
+
':host, :host::part(options)': {
|
|
140
|
+
flexDirection: varDefault.segmentedDirection('row'),
|
|
141
|
+
},
|
|
142
|
+
':host label': {
|
|
143
|
+
display: 'inline-grid',
|
|
144
|
+
alignItems: 'center',
|
|
145
|
+
gap: varDefault.segmentedOptionGap('8px'),
|
|
146
|
+
gridTemplateColumns: varDefault.segmentedOptionGridColumns('0px 24px 1fr'),
|
|
147
|
+
padding: varDefault.segmentedOptionPadding('4px 12px'),
|
|
148
|
+
font: varDefault.segmentedOptionFont('16px'),
|
|
149
|
+
},
|
|
150
|
+
':host label:has(:checked)': {
|
|
151
|
+
color: varDefault.segmentedOptionCurrentColor('#eee'),
|
|
152
|
+
background: varDefault.segmentedOptionCurrentBackground('#44a'),
|
|
153
|
+
},
|
|
154
|
+
':host svg': {
|
|
155
|
+
height: varDefault.segmentOptionIconSize('16px'),
|
|
156
|
+
stroke: varDefault.segmentedOptionIconColor('currentColor'),
|
|
157
|
+
},
|
|
158
|
+
':host label.no-icon': {
|
|
159
|
+
gap: 0,
|
|
160
|
+
gridTemplateColumns: varDefault.segmentedOptionGridColumns('0px 1fr'),
|
|
161
|
+
},
|
|
162
|
+
':host input[type="radio"], :host input[type="checkbox"]': {
|
|
163
|
+
visibility: varDefault.segmentedInputVisibility('hidden'),
|
|
164
|
+
},
|
|
165
|
+
':host::part(options)': {
|
|
166
|
+
display: 'flex',
|
|
167
|
+
borderRadius: varDefault.segmentedOptionsBorderRadius('8px'),
|
|
168
|
+
background: varDefault.segmentedOptionsBackground('#fff'),
|
|
169
|
+
color: varDefault.segmentedOptionColor('#222'),
|
|
170
|
+
overflow: 'hidden',
|
|
171
|
+
alignItems: varDefault.segmentedOptionAlignItems('stretch'),
|
|
172
|
+
},
|
|
173
|
+
':host::part(custom)': {
|
|
174
|
+
padding: varDefault.segmentedOptionPadding('4px 12px'),
|
|
175
|
+
color: varDefault.segmentedOptionCurrentColor('#eee'),
|
|
176
|
+
background: varDefault.segmentedOptionCurrentBackground('#44a'),
|
|
177
|
+
font: varDefault.segmentedOptionFont('16px'),
|
|
178
|
+
border: '0',
|
|
179
|
+
outline: 'none',
|
|
180
|
+
},
|
|
181
|
+
':host::part(custom)::placeholder': {
|
|
182
|
+
color: varDefault.segmentedOptionCurrentColor('#eee'),
|
|
183
|
+
opacity: varDefault.segmentedPlaceholderOpacity(0.75),
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
constructor() {
|
|
187
|
+
super();
|
|
188
|
+
this.initAttributes('direction', 'choices', 'other', 'multiple', 'name', 'placeholder', 'localized');
|
|
189
|
+
}
|
|
190
|
+
valueChanged = false;
|
|
191
|
+
handleChange = () => {
|
|
192
|
+
const { options, custom } = this.parts;
|
|
193
|
+
if (this.multiple) {
|
|
194
|
+
const inputs = [
|
|
195
|
+
...options.querySelectorAll('input:checked'),
|
|
196
|
+
];
|
|
197
|
+
this.value = inputs.map((input) => input.value).join(',');
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const input = options.querySelector('input:checked');
|
|
201
|
+
if (!input) {
|
|
202
|
+
this.value = null;
|
|
203
|
+
}
|
|
204
|
+
else if (input.value) {
|
|
205
|
+
custom.setAttribute('hidden', '');
|
|
206
|
+
this.value = input.value;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
custom.removeAttribute('hidden');
|
|
210
|
+
custom.focus();
|
|
211
|
+
custom.select();
|
|
212
|
+
this.value = custom.value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this.valueChanged = true;
|
|
216
|
+
};
|
|
217
|
+
handleKey = (event) => {
|
|
218
|
+
switch (event.code) {
|
|
219
|
+
case 'Space':
|
|
220
|
+
;
|
|
221
|
+
event.target.click();
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
connectedCallback() {
|
|
226
|
+
super.connectedCallback();
|
|
227
|
+
const { options } = this.parts;
|
|
228
|
+
if (this.name === '') {
|
|
229
|
+
this.name = this.instanceId;
|
|
230
|
+
}
|
|
231
|
+
options.addEventListener('change', this.handleChange);
|
|
232
|
+
options.addEventListener('keydown', this.handleKey);
|
|
233
|
+
if (this.other && this.multiple) {
|
|
234
|
+
console.warn(this, 'is set to [other] and [multiple]; [other] will be ignored');
|
|
235
|
+
this.other = '';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
get _choices() {
|
|
239
|
+
const options = Array.isArray(this.choices)
|
|
240
|
+
? this.choices
|
|
241
|
+
: this.choices
|
|
242
|
+
.split(',')
|
|
243
|
+
.filter((c) => c.trim() !== '')
|
|
244
|
+
.map((c) => {
|
|
245
|
+
const [value, remains] = c.split('=').map((v) => v.trim());
|
|
246
|
+
const [caption, iconName] = (remains || value)
|
|
247
|
+
.split(':')
|
|
248
|
+
.map((v) => v.trim());
|
|
249
|
+
const icon = iconName ? icons[iconName]() : '';
|
|
250
|
+
const choice = { value, icon, caption };
|
|
251
|
+
return choice;
|
|
252
|
+
});
|
|
253
|
+
if (this.other && !this.multiple) {
|
|
254
|
+
const [caption, icon] = this.other.split(':');
|
|
255
|
+
options.push({
|
|
256
|
+
value: '',
|
|
257
|
+
caption,
|
|
258
|
+
icon,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return options;
|
|
262
|
+
}
|
|
263
|
+
get isOtherValue() {
|
|
264
|
+
return Boolean(this.value === '' ||
|
|
265
|
+
(this.value &&
|
|
266
|
+
!this._choices.find((choice) => choice.value === this.value)));
|
|
267
|
+
}
|
|
268
|
+
render() {
|
|
269
|
+
super.render();
|
|
270
|
+
if (this.valueChanged) {
|
|
271
|
+
this.valueChanged = false;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const { options, custom } = this.parts;
|
|
275
|
+
options.textContent = '';
|
|
276
|
+
const type = this.multiple ? 'checkbox' : 'radio';
|
|
277
|
+
const { values, isOtherValue } = this;
|
|
278
|
+
options.append(...this._choices.map((choice) => {
|
|
279
|
+
return label({ tabindex: 0 }, input({
|
|
280
|
+
type,
|
|
281
|
+
name: this.name,
|
|
282
|
+
value: choice.value,
|
|
283
|
+
checked: values.includes(choice.value) ||
|
|
284
|
+
(choice.value === '' && isOtherValue),
|
|
285
|
+
tabIndex: -1,
|
|
286
|
+
}), choice.icon || { class: 'no-icon' }, this.localized ? xinLocalized(choice.caption) : span(choice.caption));
|
|
287
|
+
}));
|
|
288
|
+
if (this.other && !this.multiple) {
|
|
289
|
+
custom.hidden = !isOtherValue;
|
|
290
|
+
custom.value = isOtherValue ? this.value : '';
|
|
291
|
+
custom.placeholder = this.placeholder;
|
|
292
|
+
options.append(custom);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export const xinSegmented = XinSegmented.elementCreator({
|
|
297
|
+
tag: 'xin-segmented',
|
|
298
|
+
});
|