tosijs-ui 1.5.18 → 1.5.21
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/data-table.js +15 -17
- package/dist/iife.js +41 -41
- package/dist/iife.js.map +7 -7
- package/dist/menu.d.ts +2 -1
- package/dist/menu.js +83 -14
- package/dist/select.js +1 -1
- package/dist/tag-list.js +31 -14
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/dist/menu.d.ts
CHANGED
|
@@ -27,7 +27,8 @@ export interface SubMenu {
|
|
|
27
27
|
dropAction?: (dataTransfer: DataTransfer) => void;
|
|
28
28
|
}
|
|
29
29
|
export type MenuSeparator = null;
|
|
30
|
-
export type
|
|
30
|
+
export type MenuElement = () => HTMLElement;
|
|
31
|
+
export type MenuItem = MenuAction | SubMenu | MenuSeparator | MenuElement;
|
|
31
32
|
export declare const resolveMenuItems: (provider: MenuItemsProvider) => MenuItem[];
|
|
32
33
|
export declare const filterForDrop: (items: MenuItem[], dataTypes: readonly string[], hideDisabled?: boolean) => MenuItem[];
|
|
33
34
|
export declare const filterForClick: (items: MenuItem[], hideDisabled?: boolean) => MenuItem[];
|
package/dist/menu.js
CHANGED
|
@@ -235,12 +235,14 @@ export interface PopMenuOptions {
|
|
|
235
235
|
|
|
236
236
|
## MenuItem
|
|
237
237
|
|
|
238
|
-
A `MenuItem` can be one of
|
|
238
|
+
A `MenuItem` can be one of four things:
|
|
239
239
|
|
|
240
240
|
- `null` denotes a separator
|
|
241
241
|
- `MenuAction` denotes a labeled button or `<a>` tag based on whether the `action` provided
|
|
242
242
|
is a url (string) or an event handler (function).
|
|
243
243
|
- `SubMenu` is a submenu.
|
|
244
|
+
- A `() => HTMLElement` function returns a custom element to embed inline in
|
|
245
|
+
the menu (see `MenuElement` below).
|
|
244
246
|
|
|
245
247
|
### MenuAction
|
|
246
248
|
|
|
@@ -272,6 +274,54 @@ interface SubMenu {
|
|
|
272
274
|
}
|
|
273
275
|
```
|
|
274
276
|
|
|
277
|
+
### MenuElement
|
|
278
|
+
|
|
279
|
+
For embedding a custom widget inline in a menu — e.g. a `<tosi-segmented>` for
|
|
280
|
+
quick option-picking — pass a function that returns an `HTMLElement`:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
type MenuElement = () => HTMLElement
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The returned element is added as-is and tagged with the `tosi-menu-element`
|
|
287
|
+
class, which sets `min-height` to match standard menu items so the row
|
|
288
|
+
aligns visually. The widget is responsible for its own click/focus behaviour.
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
import { popMenu, tosiSegmented } from 'tosijs-ui'
|
|
292
|
+
import { elements } from 'tosijs'
|
|
293
|
+
|
|
294
|
+
const { button } = elements
|
|
295
|
+
|
|
296
|
+
let view = 'list'
|
|
297
|
+
|
|
298
|
+
const btn = button('View options')
|
|
299
|
+
btn.addEventListener('click', () => {
|
|
300
|
+
popMenu({
|
|
301
|
+
target: btn,
|
|
302
|
+
menuItems: [
|
|
303
|
+
{ caption: 'Refresh', icon: 'refreshCcw', action() {} },
|
|
304
|
+
null,
|
|
305
|
+
() => tosiSegmented({
|
|
306
|
+
choices: 'list,grid,table',
|
|
307
|
+
value: view,
|
|
308
|
+
style: { margin: '0 1em' },
|
|
309
|
+
onChange(event) {
|
|
310
|
+
view = event.target.value
|
|
311
|
+
},
|
|
312
|
+
// stop the menu's outer onClick from closing it when the user
|
|
313
|
+
// picks a segment
|
|
314
|
+
onClick(event) { event.stopPropagation() },
|
|
315
|
+
}),
|
|
316
|
+
null,
|
|
317
|
+
{ caption: 'Settings…', icon: 'settings', action() {} },
|
|
318
|
+
]
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
preview.append(btn)
|
|
323
|
+
```
|
|
324
|
+
|
|
275
325
|
### Keyboard Shortcuts
|
|
276
326
|
|
|
277
327
|
If a menu is embodied in a `<tosi-menu>` it is supported by keyboard
|
|
@@ -736,6 +786,10 @@ export const filterForDrop = (items, dataTypes, hideDisabled = false) => {
|
|
|
736
786
|
filtered.push(item);
|
|
737
787
|
continue;
|
|
738
788
|
}
|
|
789
|
+
if (typeof item === 'function') {
|
|
790
|
+
filtered.push(item);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
739
793
|
const { acceptsDrop } = item;
|
|
740
794
|
if (!acceptsDrop) {
|
|
741
795
|
if (!hideDisabled) {
|
|
@@ -777,6 +831,10 @@ export const filterForClick = (items, hideDisabled = false) => {
|
|
|
777
831
|
filtered.push(item);
|
|
778
832
|
continue;
|
|
779
833
|
}
|
|
834
|
+
if (typeof item === 'function') {
|
|
835
|
+
filtered.push(item);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
780
838
|
const action = item.action;
|
|
781
839
|
const menuItemsProvider = item.menuItems;
|
|
782
840
|
if (action || menuItemsProvider) {
|
|
@@ -870,6 +928,9 @@ StyleSheet('xin-menu-helper', {
|
|
|
870
928
|
background: varDefault.menuSeparatorColor('#2224'),
|
|
871
929
|
margin: varDefault.menuSeparatorMargin('8px 0'),
|
|
872
930
|
},
|
|
931
|
+
'.xin-menu-element, .tosi-menu-element': {
|
|
932
|
+
minHeight: varDefault.menuItemHeight('48px'),
|
|
933
|
+
},
|
|
873
934
|
'.xin-menu-item, .tosi-menu-item': menuItemStyles,
|
|
874
935
|
'.xin-menu-item, .xin-menu-item > span, .tosi-menu-item, .tosi-menu-item > span': menuItemColorStyles,
|
|
875
936
|
'.xin-menu-with-icons .xin-menu-item, .tosi-menu-with-icons .tosi-menu-item': {
|
|
@@ -917,19 +978,18 @@ export const createMenuAction = (item, options) => {
|
|
|
917
978
|
let menuItem;
|
|
918
979
|
const props = item.properties || {};
|
|
919
980
|
if (typeof item?.action === 'string') {
|
|
920
|
-
menuItem = a({
|
|
921
|
-
class: 'xin-menu-item tosi-menu-item',
|
|
981
|
+
menuItem = a(props, {
|
|
922
982
|
role: itemRole,
|
|
923
983
|
href: item.action,
|
|
924
|
-
},
|
|
984
|
+
}, icon, options.localized ? span(localize(item.caption)) : span(item.caption), span(item.shortcut ? displayShortcut(item.shortcut) : ' '));
|
|
925
985
|
}
|
|
926
986
|
else {
|
|
927
|
-
menuItem = button({
|
|
928
|
-
class: 'xin-menu-item tosi-menu-item',
|
|
987
|
+
menuItem = button(props, {
|
|
929
988
|
role: itemRole,
|
|
930
989
|
onClick: item.action,
|
|
931
|
-
},
|
|
990
|
+
}, icon, options.localized ? span(localize(item.caption)) : span(item.caption), span(item.shortcut ? displayShortcut(item.shortcut) : ' '));
|
|
932
991
|
}
|
|
992
|
+
menuItem.classList.add('xin-menu-item', 'tosi-menu-item');
|
|
933
993
|
menuItem.classList.toggle('xin-menu-item-checked', checked !== false);
|
|
934
994
|
menuItem.classList.toggle('tosi-menu-item-checked', checked !== false);
|
|
935
995
|
if (item.tooltip) {
|
|
@@ -950,8 +1010,7 @@ export const createDropMenuItem = (item, options) => {
|
|
|
950
1010
|
icon = icons[icon]();
|
|
951
1011
|
}
|
|
952
1012
|
const props = item.properties || {};
|
|
953
|
-
const menuItem = button({
|
|
954
|
-
class: 'xin-menu-item tosi-menu-item',
|
|
1013
|
+
const menuItem = button(props, {
|
|
955
1014
|
onDragenter(event) {
|
|
956
1015
|
clearDropGraceTimer();
|
|
957
1016
|
menuItem.classList.add('xin-drop-over', 'tosi-drop-over');
|
|
@@ -978,7 +1037,8 @@ export const createDropMenuItem = (item, options) => {
|
|
|
978
1037
|
}
|
|
979
1038
|
removeLastMenu(0);
|
|
980
1039
|
},
|
|
981
|
-
},
|
|
1040
|
+
}, icon, options.localized ? span(localize(item.caption)) : span(item.caption), span(' '));
|
|
1041
|
+
menuItem.classList.add('xin-menu-item', 'tosi-menu-item');
|
|
982
1042
|
if (item.tooltip) {
|
|
983
1043
|
menuItem.dataset.tooltip = item.tooltip;
|
|
984
1044
|
}
|
|
@@ -1000,8 +1060,7 @@ export const createSubMenu = (item, options) => {
|
|
|
1000
1060
|
let disclosureTimer = null;
|
|
1001
1061
|
let disclosed = false;
|
|
1002
1062
|
const props = item.properties || {};
|
|
1003
|
-
const submenuItem = button({
|
|
1004
|
-
class: 'xin-menu-item tosi-menu-item',
|
|
1063
|
+
const submenuItem = button(props, {
|
|
1005
1064
|
disabled: !(!item.enabled || item.enabled()),
|
|
1006
1065
|
onClick(event) {
|
|
1007
1066
|
if (options._dropMode)
|
|
@@ -1093,7 +1152,8 @@ export const createSubMenu = (item, options) => {
|
|
|
1093
1152
|
}
|
|
1094
1153
|
removeLastMenu(0);
|
|
1095
1154
|
},
|
|
1096
|
-
},
|
|
1155
|
+
}, icon, options.localized ? span(localize(item.caption)) : span(item.caption), icons.chevronRight({ style: { justifySelf: 'flex-end' } }));
|
|
1156
|
+
submenuItem.classList.add('xin-menu-item', 'tosi-menu-item');
|
|
1097
1157
|
if (item.tooltip) {
|
|
1098
1158
|
submenuItem.dataset.tooltip = item.tooltip;
|
|
1099
1159
|
}
|
|
@@ -1106,6 +1166,11 @@ export const createMenuItem = (item, options) => {
|
|
|
1106
1166
|
if (item === null) {
|
|
1107
1167
|
return span({ class: 'xin-menu-separator tosi-menu-separator' });
|
|
1108
1168
|
}
|
|
1169
|
+
else if (typeof item === 'function') {
|
|
1170
|
+
const el = item();
|
|
1171
|
+
el.classList.add('xin-menu-element', 'tosi-menu-element');
|
|
1172
|
+
return el;
|
|
1173
|
+
}
|
|
1109
1174
|
else if (options._dropMode) {
|
|
1110
1175
|
const sub = item;
|
|
1111
1176
|
const hasChildren = sub.menuItems && resolveMenuItems(sub.menuItems).length > 0;
|
|
@@ -1142,7 +1207,9 @@ export const createMenuItem = (item, options) => {
|
|
|
1142
1207
|
};
|
|
1143
1208
|
export const menu = (options) => {
|
|
1144
1209
|
const { target, width, menuItems, role = 'menu' } = options;
|
|
1145
|
-
const hasIcons = menuItems.find((item) => item
|
|
1210
|
+
const hasIcons = menuItems.find((item) => item != null &&
|
|
1211
|
+
typeof item !== 'function' &&
|
|
1212
|
+
(item.icon || item.checked));
|
|
1146
1213
|
const menuDepth = options.submenuDepth || 0;
|
|
1147
1214
|
const menuDiv = div({
|
|
1148
1215
|
class: hasIcons
|
|
@@ -1296,6 +1363,8 @@ export function findShortcutAction(items, event, path = []) {
|
|
|
1296
1363
|
for (const item of items) {
|
|
1297
1364
|
if (!item)
|
|
1298
1365
|
continue;
|
|
1366
|
+
if (typeof item === 'function')
|
|
1367
|
+
continue;
|
|
1299
1368
|
const { shortcut } = item;
|
|
1300
1369
|
const { menuItems } = item;
|
|
1301
1370
|
if (shortcut) {
|
package/dist/select.js
CHANGED
package/dist/tag-list.js
CHANGED
|
@@ -26,9 +26,9 @@ as a comma-delimited string or an array of strings).
|
|
|
26
26
|
<b>Editable</b>
|
|
27
27
|
<tosi-tag-list
|
|
28
28
|
class="editable-tag-list"
|
|
29
|
-
value="belongs,also belongs,custom"
|
|
29
|
+
value="belongs,also belongs,has\, comma,custom"
|
|
30
30
|
editable
|
|
31
|
-
available-tags="belongs,also belongs,not initially chosen"
|
|
31
|
+
available-tags="belongs,also belongs,has\, comma,not initially chosen"
|
|
32
32
|
></tosi-tag-list>
|
|
33
33
|
</label>
|
|
34
34
|
<br>
|
|
@@ -77,13 +77,29 @@ test('first tag-list has correct tags', () => {
|
|
|
77
77
|
test('editable tag-list has editable attribute', () => {
|
|
78
78
|
expect(tagLists[2].editable).toBe(true)
|
|
79
79
|
})
|
|
80
|
+
test('a comma inside a tag survives the value round-trip', () => {
|
|
81
|
+
const tl = document.createElement('tosi-tag-list')
|
|
82
|
+
tl.tags = ['New York, NY', 'Boston']
|
|
83
|
+
// the literal comma is escaped in `value` so it is not a delimiter
|
|
84
|
+
expect(tl.value).toBe('New York\\, NY,Boston')
|
|
85
|
+
expect(tl.tags.length).toBe(2)
|
|
86
|
+
expect(tl.tags).toContain('New York, NY')
|
|
87
|
+
})
|
|
88
|
+
test('an escaped comma in a value string parses as one tag', () => {
|
|
89
|
+
const tl = document.createElement('tosi-tag-list')
|
|
90
|
+
tl.value = 'New York\\, NY,Boston'
|
|
91
|
+
expect(tl.tags.length).toBe(2)
|
|
92
|
+
expect(tl.tags).toContain('New York, NY')
|
|
93
|
+
})
|
|
80
94
|
```
|
|
81
95
|
|
|
82
96
|
## Properties
|
|
83
97
|
|
|
84
98
|
### `value`: string | string[]
|
|
85
99
|
|
|
86
|
-
A list of tags
|
|
100
|
+
A comma-delimited list of tags. A tag that itself contains a comma must
|
|
101
|
+
escape it as `\,` — e.g. `value="New York\, NY,Boston"` is two tags. The
|
|
102
|
+
`tags` accessor handles this escaping for you in both directions.
|
|
87
103
|
|
|
88
104
|
### `tags`: string[]
|
|
89
105
|
|
|
@@ -97,7 +113,8 @@ A read-only property giving the value as an array.
|
|
|
97
113
|
### `available-tags`: string | string[]
|
|
98
114
|
|
|
99
115
|
A list of tags that will be displayed in the popup menu by default. The popup menu
|
|
100
|
-
will always display custom tags (allowing their removal).
|
|
116
|
+
will always display custom tags (allowing their removal). As with `value`, a
|
|
117
|
+
comma inside a tag must be escaped as `\,` when set via the attribute string.
|
|
101
118
|
|
|
102
119
|
### `editable`: boolean
|
|
103
120
|
|
|
@@ -115,6 +132,12 @@ import { Component as WebComponent, elements, vars, varDefault, deprecated, } fr
|
|
|
115
132
|
import { popMenu } from './menu';
|
|
116
133
|
import { icons } from './icons';
|
|
117
134
|
const { div, input, span, button } = elements;
|
|
135
|
+
// Tags are serialised as a comma-delimited string (the form `value`). A
|
|
136
|
+
// literal comma inside a tag is escaped as `\,` so it survives the
|
|
137
|
+
// split/join round-trip — both in programmatic values and in the
|
|
138
|
+
// `value` / `available-tags` HTML attributes.
|
|
139
|
+
const splitTags = (str) => str.split(/(?<!\\),/).map((tag) => tag.trim().replace(/\\,/g, ','));
|
|
140
|
+
const joinTags = (tags) => tags.map((tag) => tag.replace(/,/g, '\\,')).join(',');
|
|
118
141
|
export class TosiTag extends WebComponent {
|
|
119
142
|
static preferredTagName = 'tosi-tag';
|
|
120
143
|
static lightStyleSpec = {
|
|
@@ -248,13 +271,10 @@ export class TosiTagList extends WebComponent {
|
|
|
248
271
|
value = '';
|
|
249
272
|
// tags parses value into array
|
|
250
273
|
get tags() {
|
|
251
|
-
return this.value
|
|
252
|
-
.split(',')
|
|
253
|
-
.map((tag) => tag.trim())
|
|
254
|
-
.filter((tag) => tag !== '');
|
|
274
|
+
return splitTags(this.value).filter((tag) => tag !== '');
|
|
255
275
|
}
|
|
256
276
|
set tags(v) {
|
|
257
|
-
this.value = v
|
|
277
|
+
this.value = joinTags(v);
|
|
258
278
|
}
|
|
259
279
|
_availableTags = [];
|
|
260
280
|
get availableTags() {
|
|
@@ -269,12 +289,9 @@ export class TosiTagList extends WebComponent {
|
|
|
269
289
|
}
|
|
270
290
|
this.queueRender();
|
|
271
291
|
}
|
|
272
|
-
// Parse available-tags string (comma-delimited)
|
|
292
|
+
// Parse available-tags string (comma-delimited; `\,` is a literal comma).
|
|
273
293
|
static parseAvailableTagsString(tagsStr) {
|
|
274
|
-
return tagsStr
|
|
275
|
-
const trimmed = tag.trim();
|
|
276
|
-
return trimmed === '' ? null : trimmed;
|
|
277
|
-
});
|
|
294
|
+
return splitTags(tagsStr).map((tag) => (tag === '' ? null : tag));
|
|
278
295
|
}
|
|
279
296
|
connectedCallback() {
|
|
280
297
|
super.connectedCallback();
|