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
package/dist/menu.js
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/*#
|
|
2
|
+
# menu
|
|
3
|
+
|
|
4
|
+
Being able to pop a menu up anywhere is just so nice, and `xinjs-ui` allows menus
|
|
5
|
+
to be generated on-the-fly, and even supports hierarchical menus.
|
|
6
|
+
|
|
7
|
+
## popMenu and `<xin-menu>`
|
|
8
|
+
|
|
9
|
+
`popMenu({target, menuItems, …})` will spawn a menu from a target.
|
|
10
|
+
|
|
11
|
+
The `<xin-menu>` component places creates a trigger button, hosts
|
|
12
|
+
menuItems, and (because it persists in the DOM) supports keyboard
|
|
13
|
+
shortcuts.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const { popMenu, localize, xinMenu, postNotification, xinLocalized, icons } = xinjsui
|
|
17
|
+
const { elements } = xinjs
|
|
18
|
+
|
|
19
|
+
let picked = ''
|
|
20
|
+
let testingEnabled = false
|
|
21
|
+
|
|
22
|
+
const menuItems = [
|
|
23
|
+
{
|
|
24
|
+
icon: 'thumbsUp',
|
|
25
|
+
caption: 'Like',
|
|
26
|
+
shortcut: '^L',
|
|
27
|
+
action() {
|
|
28
|
+
postNotification({
|
|
29
|
+
message: 'I like it!',
|
|
30
|
+
icon: 'thumbsUp',
|
|
31
|
+
duration: 1
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
icon: 'heart',
|
|
37
|
+
caption: 'Love',
|
|
38
|
+
shortcut: '⌘⇧L',
|
|
39
|
+
action() {
|
|
40
|
+
postNotification({
|
|
41
|
+
type: 'success',
|
|
42
|
+
message: 'I LOVE it!',
|
|
43
|
+
icon: 'heart',
|
|
44
|
+
duration: 1
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
icon: 'thumbsDown',
|
|
50
|
+
caption: 'dislike',
|
|
51
|
+
shortcut: '⌘D',
|
|
52
|
+
action() {
|
|
53
|
+
postNotification({
|
|
54
|
+
type: 'error',
|
|
55
|
+
message: 'Awwwwwww…',
|
|
56
|
+
icon: 'thumbsDown',
|
|
57
|
+
duration: 1
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
null, // separator
|
|
62
|
+
{
|
|
63
|
+
caption: localize('Localized placeholder'),
|
|
64
|
+
action() {
|
|
65
|
+
alert(localize('Localized placeholder'))
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
icon: elements.span('🥹'),
|
|
70
|
+
caption: 'Also see…',
|
|
71
|
+
menuItems: [
|
|
72
|
+
{
|
|
73
|
+
icon: elements.span('😳'),
|
|
74
|
+
caption: 'And that’s not all…',
|
|
75
|
+
menuItems: [
|
|
76
|
+
{
|
|
77
|
+
icon: 'externalLink',
|
|
78
|
+
caption: 'timezones',
|
|
79
|
+
action: 'https://timezones.xinjs.net/'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
icon: 'externalLink',
|
|
83
|
+
caption: 'b8rjs',
|
|
84
|
+
action: 'https://b8rjs.com'
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
icon: 'xinjs',
|
|
90
|
+
caption: 'xinjs',
|
|
91
|
+
action: 'https://xinjs.net'
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
icon: 'xinie',
|
|
95
|
+
caption: 'xinie',
|
|
96
|
+
action: 'https://xinie.net'
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
icon: testingEnabled ? 'check' : '',
|
|
102
|
+
caption: 'Testing Enabled',
|
|
103
|
+
action() {
|
|
104
|
+
testingEnabled = !testingEnabled
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
caption: 'Testing…',
|
|
109
|
+
enabled() {
|
|
110
|
+
return testingEnabled
|
|
111
|
+
},
|
|
112
|
+
menuItems: [
|
|
113
|
+
{
|
|
114
|
+
caption: 'one',
|
|
115
|
+
checked: () => picked === 'one',
|
|
116
|
+
action () {
|
|
117
|
+
picked = 'one'
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
caption: 'two',
|
|
122
|
+
checked: () => picked === 'two',
|
|
123
|
+
action () {
|
|
124
|
+
picked = 'two'
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
caption: 'three',
|
|
129
|
+
checked: () => picked === 'three',
|
|
130
|
+
action () {
|
|
131
|
+
picked = 'three'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
preview.addEventListener('click', (event) => {
|
|
139
|
+
if (!event.target.closest('button')) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
popMenu({
|
|
143
|
+
target: event.target,
|
|
144
|
+
menuItems
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
preview.append(
|
|
149
|
+
xinMenu(
|
|
150
|
+
{
|
|
151
|
+
menuItems,
|
|
152
|
+
localized: true,
|
|
153
|
+
},
|
|
154
|
+
xinLocalized('Menu'),
|
|
155
|
+
icons.chevronDown()
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
```html
|
|
160
|
+
<button title="menu test">
|
|
161
|
+
<xin-icon icon="moreVertical"></xin-icon>
|
|
162
|
+
</button>
|
|
163
|
+
<button title="menu test from bottom-right" style="position: absolute; bottom: 0; right: 0">
|
|
164
|
+
<xin-icon icon="moreVertical"></xin-icon>
|
|
165
|
+
</button>
|
|
166
|
+
```
|
|
167
|
+
```css
|
|
168
|
+
.preview button {
|
|
169
|
+
min-width: 44px;
|
|
170
|
+
text-align: center;
|
|
171
|
+
height: 44px;
|
|
172
|
+
margin: 5px;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Overflow test
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
const { popMenu, icons, postNotification } = xinjsui
|
|
180
|
+
const { elements } = xinjs
|
|
181
|
+
|
|
182
|
+
preview.querySelector('button').addEventListener('click', (event) => {
|
|
183
|
+
popMenu({
|
|
184
|
+
target: event.target,
|
|
185
|
+
menuItems: Object.keys(icons).map(icon => ({
|
|
186
|
+
icon,
|
|
187
|
+
caption: icon,
|
|
188
|
+
action() {
|
|
189
|
+
postNotification({
|
|
190
|
+
icon: icon,
|
|
191
|
+
message: icon,
|
|
192
|
+
duration: 1
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}))
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
```html
|
|
200
|
+
<button title="big menu test" style="position: absolute; top: 0; left: 0">
|
|
201
|
+
Big Menu Test
|
|
202
|
+
</button>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## popMenu({target, width, menuItems…})
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
export interface PopMenuOptions {
|
|
209
|
+
target: HTMLElement
|
|
210
|
+
menuItems: MenuItem[]
|
|
211
|
+
width?: string | number
|
|
212
|
+
position?: FloatPosition
|
|
213
|
+
submenuDepth?: number // don't set this, it's set internally by popMenu
|
|
214
|
+
submenuOffset?: { x: number; y: number }
|
|
215
|
+
localized?: boolean
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`popMenu` will spawn a menu on a target element. A menu is just a `MenuItem[]`.
|
|
220
|
+
|
|
221
|
+
## MenuItem
|
|
222
|
+
|
|
223
|
+
A `MenuItem` can be one of three things:
|
|
224
|
+
|
|
225
|
+
- `null` denotes a separator
|
|
226
|
+
- `MenuAction` denotes a labeled button or `<a>` tag based on whether the `action` provided
|
|
227
|
+
is a url (string) or an event handler (function).
|
|
228
|
+
- `SubMenu` is a submenu.
|
|
229
|
+
|
|
230
|
+
### MenuAction
|
|
231
|
+
|
|
232
|
+
Note that popMenu does not implement shortcuts for you (yet!).
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
interface MenuAction {
|
|
236
|
+
caption: string
|
|
237
|
+
shortcut?: string
|
|
238
|
+
checked?: () => boolean
|
|
239
|
+
enabled?: () => boolean
|
|
240
|
+
action: ActionCallback | string
|
|
241
|
+
icon?: string | Element
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### SubMenu
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
interface SubMenu {
|
|
249
|
+
caption: string
|
|
250
|
+
enabled?: () => boolean
|
|
251
|
+
menuItems: MenuItem[]
|
|
252
|
+
icon?: string | Element
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Keyboard Shortcuts
|
|
257
|
+
|
|
258
|
+
If a menu is embodied in a `<xin-menu>` it is supported by keyboard
|
|
259
|
+
shortcuts. Both text and symbolic shortcut descriptions are supported,
|
|
260
|
+
e.g.
|
|
261
|
+
|
|
262
|
+
- `⌘C` or `meta-C`
|
|
263
|
+
- `⇧P` for `shift-P`
|
|
264
|
+
- `^F` or `ctrl-f`
|
|
265
|
+
- `⌥x`, `⎇x`, `alt-x` or `option-x`
|
|
266
|
+
|
|
267
|
+
## Localization
|
|
268
|
+
|
|
269
|
+
If you set `localized: true` in `PopMenuOptions` then menu captions will be be
|
|
270
|
+
passed through `localize`. You'll need to provide the appropriate localized strings,
|
|
271
|
+
of course.
|
|
272
|
+
|
|
273
|
+
> `<xin-menu>` supports the `localized` attribute but it doesn't localize
|
|
274
|
+
> its trigger button.
|
|
275
|
+
|
|
276
|
+
To see this in action, see the example below, or look at the
|
|
277
|
+
[table example](?data-table.ts). It uses a `localized` menu
|
|
278
|
+
to render column names when you show hidden columns.
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
const { elements } = xinjs
|
|
282
|
+
const { xinLocalized, localize, icons, popMenu, postNotification } = xinjsui
|
|
283
|
+
const { button } = elements
|
|
284
|
+
const makeItem = s => ({
|
|
285
|
+
caption: s,
|
|
286
|
+
action() {
|
|
287
|
+
postNotification({
|
|
288
|
+
message: localize(s),
|
|
289
|
+
duration: 1
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
const target = button(
|
|
294
|
+
{
|
|
295
|
+
onClick(event) {
|
|
296
|
+
popMenu({
|
|
297
|
+
target: event.target.closest('button'),
|
|
298
|
+
localized: true,
|
|
299
|
+
menuItems: [
|
|
300
|
+
makeItem('New'),
|
|
301
|
+
makeItem('Open...'),
|
|
302
|
+
makeItem('Save'),
|
|
303
|
+
makeItem('Close'),
|
|
304
|
+
]
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
xinLocalized(
|
|
309
|
+
{ style: { marginRight: '5px' }},
|
|
310
|
+
'menu'
|
|
311
|
+
),
|
|
312
|
+
icons.chevronDown()
|
|
313
|
+
)
|
|
314
|
+
preview.append(target)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Why another menu library?!
|
|
318
|
+
|
|
319
|
+
Support for menus is sadly lacking in HTML, and unfortunately there's a huge conceptual problem
|
|
320
|
+
with menus implemented the way React and React-influenced libraries work, i.e. you need
|
|
321
|
+
to have an instance of a menu "wrapped around" the DOM element that triggers it, whereas
|
|
322
|
+
a better approach (and one dating back at least as far as the original Mac UI) is to treat
|
|
323
|
+
a menu as a separate resource that can be instantiated on demand.
|
|
324
|
+
|
|
325
|
+
A simple example where this becomes really obvious is if you want to associate a "more options"
|
|
326
|
+
menu with every row of a large table. Either you end up having an enormous DOM (virtual or otherwise)
|
|
327
|
+
or you have to painfully swap out components on-the-fly.
|
|
328
|
+
|
|
329
|
+
And, finally, submenus are darn useful for any serious app.
|
|
330
|
+
|
|
331
|
+
For this reason, `xinjs-ui` has its own menu implementation.
|
|
332
|
+
*/
|
|
333
|
+
import { elements, varDefault, vars, StyleSheet, Component, } from 'xinjs';
|
|
334
|
+
import { popFloat } from './pop-float';
|
|
335
|
+
import { icons } from './icons';
|
|
336
|
+
import { localize } from './localize';
|
|
337
|
+
import { matchShortcut } from './match-shortcut';
|
|
338
|
+
const { div, button, span, a, xinSlot } = elements;
|
|
339
|
+
StyleSheet('xin-menu-helper', {
|
|
340
|
+
'.xin-menu': {
|
|
341
|
+
overflow: 'hidden auto',
|
|
342
|
+
maxHeight: `calc(${vars.maxHeight} - ${varDefault.menuInset('8px')})`,
|
|
343
|
+
borderRadius: vars.spacing50,
|
|
344
|
+
background: varDefault.menuBg('#fafafa'),
|
|
345
|
+
boxShadow: varDefault.menuShadow(`${vars.spacing13} ${vars.spacing50} ${vars.spacing} #0004`),
|
|
346
|
+
},
|
|
347
|
+
'.xin-menu > div': {
|
|
348
|
+
width: varDefault.menuWidth('auto'),
|
|
349
|
+
},
|
|
350
|
+
'.xin-menu-trigger': {
|
|
351
|
+
paddingLeft: 0,
|
|
352
|
+
paddingRight: 0,
|
|
353
|
+
minWidth: varDefault.touchSize('48px'),
|
|
354
|
+
},
|
|
355
|
+
'.xin-menu-separator': {
|
|
356
|
+
display: 'inline-block',
|
|
357
|
+
content: ' ',
|
|
358
|
+
height: '1px',
|
|
359
|
+
width: '100%',
|
|
360
|
+
background: varDefault.menuSeparatorColor('#2224'),
|
|
361
|
+
margin: varDefault.menuSeparatorMargin('8px 0'),
|
|
362
|
+
},
|
|
363
|
+
'.xin-menu-item': {
|
|
364
|
+
boxShadow: 'none',
|
|
365
|
+
border: 'none !important',
|
|
366
|
+
display: 'grid',
|
|
367
|
+
alignItems: 'center',
|
|
368
|
+
justifyContent: 'flex-start',
|
|
369
|
+
textDecoration: 'none',
|
|
370
|
+
gridTemplateColumns: '0px 1fr 30px',
|
|
371
|
+
width: '100%',
|
|
372
|
+
gap: 0,
|
|
373
|
+
background: 'transparent',
|
|
374
|
+
padding: varDefault.menuItemPadding('0 16px'),
|
|
375
|
+
height: varDefault.menuItemHeight('48px'),
|
|
376
|
+
lineHeight: varDefault.menuItemHeight('48px'),
|
|
377
|
+
textAlign: 'left',
|
|
378
|
+
},
|
|
379
|
+
'.xin-menu-item, .xin-menu-item > span': {
|
|
380
|
+
color: varDefault.menuItemColor('#222'),
|
|
381
|
+
},
|
|
382
|
+
'.xin-menu-with-icons .xin-menu-item': {
|
|
383
|
+
gridTemplateColumns: '30px 1fr 30px',
|
|
384
|
+
},
|
|
385
|
+
'.xin-menu-item svg': {
|
|
386
|
+
stroke: varDefault.menuItemIconColor('#222'),
|
|
387
|
+
},
|
|
388
|
+
'.xin-menu-item.xin-menu-item-checked': {
|
|
389
|
+
background: varDefault.menuItemHoverBg('#eee'),
|
|
390
|
+
},
|
|
391
|
+
'.xin-menu-item > span:nth-child(2)': {
|
|
392
|
+
whiteSpace: 'nowrap',
|
|
393
|
+
overflow: 'hidden',
|
|
394
|
+
textOverflow: 'ellipsis',
|
|
395
|
+
textAlign: 'left',
|
|
396
|
+
},
|
|
397
|
+
'.xin-menu-item:hover': {
|
|
398
|
+
// chrome rendering bug
|
|
399
|
+
boxShadow: 'none !important',
|
|
400
|
+
background: varDefault.menuItemHoverBg('#eee'),
|
|
401
|
+
},
|
|
402
|
+
'.xin-menu-item:active': {
|
|
403
|
+
// chrome rendering bug
|
|
404
|
+
boxShadow: 'none !important',
|
|
405
|
+
background: varDefault.menuItemActiveBg('#aaa'),
|
|
406
|
+
color: varDefault.menuItemActiveColor('#000'),
|
|
407
|
+
},
|
|
408
|
+
'.xin-menu-item:active svg': {
|
|
409
|
+
stroke: varDefault.menuItemIconActiveColor('#000'),
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
export const createMenuAction = (item, options) => {
|
|
413
|
+
const checked = (item.checked && item.checked() && 'check') || false;
|
|
414
|
+
let icon = item?.icon || checked || span(' ');
|
|
415
|
+
if (typeof icon === 'string') {
|
|
416
|
+
icon = icons[icon]();
|
|
417
|
+
}
|
|
418
|
+
let menuItem;
|
|
419
|
+
if (typeof item?.action === 'string') {
|
|
420
|
+
menuItem = a({
|
|
421
|
+
class: 'xin-menu-item',
|
|
422
|
+
href: item.action,
|
|
423
|
+
}, icon, options.localized ? span(localize(item.caption)) : span(item.caption), span(item.shortcut || ' '));
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
menuItem = button({
|
|
427
|
+
class: 'xin-menu-item',
|
|
428
|
+
onClick: item.action,
|
|
429
|
+
}, icon, options.localized ? span(localize(item.caption)) : span(item.caption), span(item.shortcut || ' '));
|
|
430
|
+
}
|
|
431
|
+
menuItem.classList.toggle('xin-menu-item-checked', checked !== false);
|
|
432
|
+
if (item?.enabled && !item.enabled()) {
|
|
433
|
+
menuItem.setAttribute('disabled', '');
|
|
434
|
+
}
|
|
435
|
+
return menuItem;
|
|
436
|
+
};
|
|
437
|
+
export const createSubMenu = (item, options) => {
|
|
438
|
+
const checked = (item.checked && item.checked() && 'check') || false;
|
|
439
|
+
let icon = item?.icon || checked || span(' ');
|
|
440
|
+
if (typeof icon === 'string') {
|
|
441
|
+
icon = icons[icon]();
|
|
442
|
+
}
|
|
443
|
+
const submenuItem = button({
|
|
444
|
+
class: 'xin-menu-item',
|
|
445
|
+
disabled: !(!item.enabled || item.enabled()),
|
|
446
|
+
onClick(event) {
|
|
447
|
+
popMenu(Object.assign({}, options, {
|
|
448
|
+
menuItems: item.menuItems,
|
|
449
|
+
target: submenuItem,
|
|
450
|
+
submenuDepth: (options.submenuDepth || 0) + 1,
|
|
451
|
+
position: 'side',
|
|
452
|
+
}));
|
|
453
|
+
event.stopPropagation();
|
|
454
|
+
event.preventDefault();
|
|
455
|
+
},
|
|
456
|
+
}, icon, options.localized ? span(localize(item.caption)) : span(item.caption), icons.chevronRight({ style: { justifySelf: 'flex-end' } }));
|
|
457
|
+
return submenuItem;
|
|
458
|
+
};
|
|
459
|
+
export const createMenuItem = (item, options) => {
|
|
460
|
+
if (item === null) {
|
|
461
|
+
return span({ class: 'xin-menu-separator' });
|
|
462
|
+
}
|
|
463
|
+
else if (item?.action) {
|
|
464
|
+
return createMenuAction(item, options);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
return createSubMenu(item, options);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
export const menu = (options) => {
|
|
471
|
+
const { target, width, menuItems } = options;
|
|
472
|
+
const hasIcons = menuItems.find((item) => item?.icon || item?.checked);
|
|
473
|
+
return div({
|
|
474
|
+
class: hasIcons ? 'xin-menu xin-menu-with-icons' : 'xin-menu',
|
|
475
|
+
onClick() {
|
|
476
|
+
removeLastMenu(0);
|
|
477
|
+
},
|
|
478
|
+
}, div({
|
|
479
|
+
style: {
|
|
480
|
+
minWidth: target.offsetWidth + 'px',
|
|
481
|
+
width: typeof width === 'number' ? `${width}px` : width,
|
|
482
|
+
},
|
|
483
|
+
onMousedown(event) {
|
|
484
|
+
event.preventDefault();
|
|
485
|
+
event.stopPropagation();
|
|
486
|
+
},
|
|
487
|
+
}, ...menuItems.map((item) => createMenuItem(item, options))));
|
|
488
|
+
};
|
|
489
|
+
let lastPopped;
|
|
490
|
+
const poppedMenus = [];
|
|
491
|
+
export const removeLastMenu = (depth = 0) => {
|
|
492
|
+
const toBeRemoved = poppedMenus.splice(depth);
|
|
493
|
+
for (const popped of toBeRemoved) {
|
|
494
|
+
popped.menu.remove();
|
|
495
|
+
}
|
|
496
|
+
lastPopped = toBeRemoved[0];
|
|
497
|
+
return depth > 0 ? poppedMenus[depth - 1] : undefined;
|
|
498
|
+
};
|
|
499
|
+
document.body.addEventListener('mousedown', (event) => {
|
|
500
|
+
if (event.target &&
|
|
501
|
+
!poppedMenus.find((popped) => popped.target.contains(event.target))) {
|
|
502
|
+
removeLastMenu(0);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
document.body.addEventListener('keydown', (event) => {
|
|
506
|
+
if (event.key === 'Escape') {
|
|
507
|
+
removeLastMenu(0);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
export const popMenu = (options) => {
|
|
511
|
+
options = Object.assign({ submenuDepth: 0 }, options);
|
|
512
|
+
const { target, position, submenuDepth } = options;
|
|
513
|
+
if (lastPopped && !document.body.contains(lastPopped?.menu)) {
|
|
514
|
+
lastPopped = undefined;
|
|
515
|
+
}
|
|
516
|
+
if (poppedMenus.length && !document.body.contains(poppedMenus[0].menu)) {
|
|
517
|
+
poppedMenus.splice(0);
|
|
518
|
+
}
|
|
519
|
+
if (submenuDepth === 0 && lastPopped?.target === target)
|
|
520
|
+
return;
|
|
521
|
+
const popped = removeLastMenu(submenuDepth);
|
|
522
|
+
if (lastPopped?.target === target)
|
|
523
|
+
return;
|
|
524
|
+
if (popped && popped.target === target) {
|
|
525
|
+
removeLastMenu();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (!options.menuItems?.length) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const content = menu(options);
|
|
532
|
+
const float = popFloat({
|
|
533
|
+
content,
|
|
534
|
+
target,
|
|
535
|
+
position,
|
|
536
|
+
});
|
|
537
|
+
float.remainOnScroll = 'remove';
|
|
538
|
+
poppedMenus.push({
|
|
539
|
+
target,
|
|
540
|
+
menu: float,
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
function findShortcutAction(items, event) {
|
|
544
|
+
for (const item of items) {
|
|
545
|
+
if (!item)
|
|
546
|
+
continue;
|
|
547
|
+
const { shortcut } = item;
|
|
548
|
+
const { menuItems } = item;
|
|
549
|
+
if (shortcut) {
|
|
550
|
+
if (matchShortcut(event, shortcut)) {
|
|
551
|
+
return item;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (menuItems) {
|
|
555
|
+
const foundAction = findShortcutAction(menuItems, event);
|
|
556
|
+
if (foundAction) {
|
|
557
|
+
return foundAction;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
export class XinMenu extends Component {
|
|
564
|
+
menuItems = [];
|
|
565
|
+
menuWidth = 'auto';
|
|
566
|
+
localized = false;
|
|
567
|
+
showMenu = (event) => {
|
|
568
|
+
if (event.type === 'click' || event.code === 'Space') {
|
|
569
|
+
popMenu({
|
|
570
|
+
target: this.parts.trigger,
|
|
571
|
+
width: this.menuWidth,
|
|
572
|
+
localized: this.localized,
|
|
573
|
+
menuItems: this.menuItems,
|
|
574
|
+
});
|
|
575
|
+
event.stopPropagation();
|
|
576
|
+
event.preventDefault();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
content = () => button({ tabindex: 0, part: 'trigger', onClick: this.showMenu }, xinSlot());
|
|
580
|
+
handleShortcut = async (event) => {
|
|
581
|
+
const menuAction = findShortcutAction(this.menuItems, event);
|
|
582
|
+
if (menuAction) {
|
|
583
|
+
if (menuAction.action instanceof Function) {
|
|
584
|
+
menuAction.action();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
constructor() {
|
|
589
|
+
super();
|
|
590
|
+
this.initAttributes('menuWidth', 'localized', 'icon');
|
|
591
|
+
this.addEventListener('keydown', this.showMenu);
|
|
592
|
+
}
|
|
593
|
+
connectedCallback() {
|
|
594
|
+
super.connectedCallback();
|
|
595
|
+
document.addEventListener('keydown', this.handleShortcut, true);
|
|
596
|
+
}
|
|
597
|
+
disconnectedCallback() {
|
|
598
|
+
super.disconnectedCallback();
|
|
599
|
+
document.removeEventListener('keydown', this.handleShortcut);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
export const xinMenu = XinMenu.elementCreator({
|
|
603
|
+
tag: 'xin-menu',
|
|
604
|
+
styleSpec: {
|
|
605
|
+
':host': {
|
|
606
|
+
display: 'inline-block',
|
|
607
|
+
},
|
|
608
|
+
':host button > xin-slot': {
|
|
609
|
+
display: 'flex',
|
|
610
|
+
alignItems: 'center',
|
|
611
|
+
gap: varDefault.xinMenuTriggerGap('10px'),
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Component, ElementCreator } from 'tosijs';
|
|
2
|
+
interface NotificationSpec {
|
|
3
|
+
message: string;
|
|
4
|
+
type?: 'success' | 'info' | 'log' | 'warn' | 'error' | 'progress';
|
|
5
|
+
icon?: SVGElement | string;
|
|
6
|
+
duration?: number;
|
|
7
|
+
progress?: () => number;
|
|
8
|
+
close?: () => void;
|
|
9
|
+
}
|
|
10
|
+
type callback = () => void;
|
|
11
|
+
export declare class XinNotification extends Component {
|
|
12
|
+
private static singleton?;
|
|
13
|
+
static styleSpec: {
|
|
14
|
+
':host': {
|
|
15
|
+
_notificationSpacing: number;
|
|
16
|
+
_notificationWidth: number;
|
|
17
|
+
_notificationPadding: string;
|
|
18
|
+
_notificationBg: string;
|
|
19
|
+
_notificationAccentColor: string;
|
|
20
|
+
_notificationTextColor: string;
|
|
21
|
+
_notificationIconSize: string;
|
|
22
|
+
_notificationButtonSize: number;
|
|
23
|
+
_notificationBorderWidth: string;
|
|
24
|
+
_notificationBorderRadius: string;
|
|
25
|
+
position: string;
|
|
26
|
+
left: number;
|
|
27
|
+
right: number;
|
|
28
|
+
bottom: number;
|
|
29
|
+
paddingBottom: string;
|
|
30
|
+
width: string;
|
|
31
|
+
display: string;
|
|
32
|
+
flexDirection: string;
|
|
33
|
+
margin: string;
|
|
34
|
+
gap: string;
|
|
35
|
+
maxHeight: string;
|
|
36
|
+
overflow: string;
|
|
37
|
+
boxShadow: string;
|
|
38
|
+
};
|
|
39
|
+
':host *': {
|
|
40
|
+
color: string;
|
|
41
|
+
};
|
|
42
|
+
':host .note': {
|
|
43
|
+
display: string;
|
|
44
|
+
background: string;
|
|
45
|
+
padding: string;
|
|
46
|
+
gridTemplateColumns: string;
|
|
47
|
+
gap: string;
|
|
48
|
+
alignItems: string;
|
|
49
|
+
borderRadius: string;
|
|
50
|
+
boxShadow: string;
|
|
51
|
+
borderColor: string;
|
|
52
|
+
borderWidth: string;
|
|
53
|
+
borderStyle: string;
|
|
54
|
+
transition: string;
|
|
55
|
+
transitionProperty: string;
|
|
56
|
+
zIndex: number;
|
|
57
|
+
};
|
|
58
|
+
':host .note .icon': {
|
|
59
|
+
stroke: string;
|
|
60
|
+
};
|
|
61
|
+
':host .note button': {
|
|
62
|
+
display: string;
|
|
63
|
+
lineHeight: string;
|
|
64
|
+
padding: number;
|
|
65
|
+
margin: number;
|
|
66
|
+
height: string;
|
|
67
|
+
width: string;
|
|
68
|
+
background: string;
|
|
69
|
+
alignItems: string;
|
|
70
|
+
justifyContent: string;
|
|
71
|
+
boxShadow: string;
|
|
72
|
+
border: string;
|
|
73
|
+
position: string;
|
|
74
|
+
};
|
|
75
|
+
':host .note button:hover svg': {
|
|
76
|
+
stroke: string;
|
|
77
|
+
};
|
|
78
|
+
':host .note button:active svg': {
|
|
79
|
+
borderRadius: number;
|
|
80
|
+
stroke: string;
|
|
81
|
+
background: string;
|
|
82
|
+
padding: string;
|
|
83
|
+
};
|
|
84
|
+
':host .note svg': {
|
|
85
|
+
height: string;
|
|
86
|
+
width: string;
|
|
87
|
+
pointerEvents: string;
|
|
88
|
+
};
|
|
89
|
+
':host .message': {
|
|
90
|
+
display: string;
|
|
91
|
+
flexDirection: string;
|
|
92
|
+
alignItems: string;
|
|
93
|
+
gap: string;
|
|
94
|
+
};
|
|
95
|
+
':host .note.closing': {
|
|
96
|
+
opacity: number;
|
|
97
|
+
zIndex: number;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
static removeNote(note: HTMLElement): void;
|
|
101
|
+
static post(spec: NotificationSpec | string): callback;
|
|
102
|
+
content: null;
|
|
103
|
+
}
|
|
104
|
+
export declare const xinNotification: ElementCreator<XinNotification>;
|
|
105
|
+
export declare function postNotification(spec: NotificationSpec | string): callback;
|
|
106
|
+
export {};
|