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/form.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Component as XinComponent, ElementCreator } from 'tosijs';
|
|
2
|
+
export declare class XinField extends XinComponent {
|
|
3
|
+
caption: string;
|
|
4
|
+
key: string;
|
|
5
|
+
type: '' | 'checkbox' | 'number' | 'range' | 'date' | 'text' | 'color';
|
|
6
|
+
optional: boolean;
|
|
7
|
+
pattern: string;
|
|
8
|
+
placeholder: string;
|
|
9
|
+
min: string;
|
|
10
|
+
max: string;
|
|
11
|
+
step: string;
|
|
12
|
+
fixedPrecision: number;
|
|
13
|
+
value: any;
|
|
14
|
+
content: HTMLLabelElement;
|
|
15
|
+
constructor();
|
|
16
|
+
private valueChanged;
|
|
17
|
+
handleChange: () => void;
|
|
18
|
+
initialize(form: XinForm): void;
|
|
19
|
+
connectedCallback(): void;
|
|
20
|
+
render(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare class XinForm extends XinComponent {
|
|
23
|
+
context: {
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
};
|
|
26
|
+
value: {
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
};
|
|
29
|
+
get isValid(): boolean;
|
|
30
|
+
static styleSpec: {
|
|
31
|
+
':host': {
|
|
32
|
+
display: string;
|
|
33
|
+
flexDirection: string;
|
|
34
|
+
};
|
|
35
|
+
':host::part(header), :host::part(footer)': {
|
|
36
|
+
display: string;
|
|
37
|
+
};
|
|
38
|
+
':host::part(content)': {
|
|
39
|
+
display: string;
|
|
40
|
+
flexDirection: string;
|
|
41
|
+
overflow: string;
|
|
42
|
+
height: string;
|
|
43
|
+
width: string;
|
|
44
|
+
position: string;
|
|
45
|
+
boxSizing: string;
|
|
46
|
+
};
|
|
47
|
+
':host form': {
|
|
48
|
+
display: string;
|
|
49
|
+
flex: string;
|
|
50
|
+
position: string;
|
|
51
|
+
overflow: string;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
content: (HTMLFormElement | HTMLSlotElement)[];
|
|
55
|
+
getField: (key: string) => XinField | null;
|
|
56
|
+
get fields(): any;
|
|
57
|
+
set fields(values: {
|
|
58
|
+
[key: string]: any;
|
|
59
|
+
});
|
|
60
|
+
submit: () => void;
|
|
61
|
+
handleSubmit: (event: SubmitEvent) => void;
|
|
62
|
+
submitCallback: (value: {
|
|
63
|
+
[key: string]: any;
|
|
64
|
+
}, isValid: boolean) => void;
|
|
65
|
+
connectedCallback(): void;
|
|
66
|
+
}
|
|
67
|
+
export declare const xinField: ElementCreator<XinField>;
|
|
68
|
+
export declare const xinForm: ElementCreator<XinForm>;
|
package/dist/form.js
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/*#
|
|
2
|
+
# forms
|
|
3
|
+
|
|
4
|
+
`<xin-form>` and `<xin-field>` can be used to quickly create forms complete with
|
|
5
|
+
[client-side validation](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#built-in_form_validation_examples).
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const form = preview.querySelector('xin-form')
|
|
9
|
+
preview.querySelector('.submit').addEventListener('click', form.submit)
|
|
10
|
+
```
|
|
11
|
+
```html
|
|
12
|
+
<xin-form value='{"formInitializer": "initial value from form"}'>
|
|
13
|
+
<h3 slot="header">Example Form Header</h3>
|
|
14
|
+
<xin-field caption="Required field" key="required"></xin-field>
|
|
15
|
+
<xin-field optional key="optional"><i>Optional</i> Field</xin-field>
|
|
16
|
+
<xin-field key="text" type="text" placeholder="type it in here">Tell us a long story</xin-field>
|
|
17
|
+
<xin-field caption="Zip Code" placeholder="12345 or 12345-6789" key="zipcode" pattern="\d{5}(-\d{4})?"></xin-field>
|
|
18
|
+
<xin-field caption="Date" key="date" type="date"></xin-field>
|
|
19
|
+
<xin-field caption="Number" key="number" type="number"></xin-field>
|
|
20
|
+
<xin-field caption="Range" key="range" type="range" min="0" max="10"></xin-field>
|
|
21
|
+
<xin-field key="boolean" type="checkbox">😃 <b>Agreed?!</b></xin-field>
|
|
22
|
+
<xin-field key="color" type="color" value="pink">
|
|
23
|
+
favorite color
|
|
24
|
+
</xin-field>
|
|
25
|
+
<xin-field key="select">
|
|
26
|
+
Custom Field
|
|
27
|
+
<select slot="input">
|
|
28
|
+
<option>This</option>
|
|
29
|
+
<option>That</option>
|
|
30
|
+
<option>The Other</option>
|
|
31
|
+
</select>
|
|
32
|
+
</xin-field>
|
|
33
|
+
<xin-field key="tags">
|
|
34
|
+
Tag List
|
|
35
|
+
<xin-tag-list editable slot="input" available-tags="pick me,no pick me"></xin-tag-list>
|
|
36
|
+
</xin-field>
|
|
37
|
+
<xin-field key="rating">
|
|
38
|
+
Rate this form!
|
|
39
|
+
<xin-rating slot="input"></xin-rating>
|
|
40
|
+
</xin-field>
|
|
41
|
+
<xin-field key="like">
|
|
42
|
+
Do you like it?
|
|
43
|
+
<xin-segmented
|
|
44
|
+
choices="yes=Yes:thumbsUp,no=No:thumbsDown"
|
|
45
|
+
slot="input"
|
|
46
|
+
></xin-segmented>
|
|
47
|
+
</xin-field>
|
|
48
|
+
<xin-field key="relationship">
|
|
49
|
+
Relationship Status
|
|
50
|
+
<xin-segmented
|
|
51
|
+
style="--segmented-direction: column; --segmented-align-items: stretch"
|
|
52
|
+
choices="couple=In a relationship,single=Single"
|
|
53
|
+
other="It's complicated…"
|
|
54
|
+
slot="input"
|
|
55
|
+
></xin-segmented>
|
|
56
|
+
</xin-field>
|
|
57
|
+
<xin-field key="amount" fixed-precision="2" type="number" prefix="$" suffix="(USD)">
|
|
58
|
+
What's it worth?
|
|
59
|
+
</xin-field>
|
|
60
|
+
<xin-field key="valueInitializer" value="initial value from field">
|
|
61
|
+
Initialized by field
|
|
62
|
+
</xin-field>
|
|
63
|
+
<xin-field key="formInitializer">
|
|
64
|
+
Initialized by form
|
|
65
|
+
</xin-field>
|
|
66
|
+
<button slot="footer" class="submit">Submit</button>
|
|
67
|
+
</xin-form>
|
|
68
|
+
```
|
|
69
|
+
```css
|
|
70
|
+
.preview xin-form {
|
|
71
|
+
height: 100%;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.preview ::part(header), .preview ::part(footer) {
|
|
75
|
+
background: var(--inset-bg);
|
|
76
|
+
justify-content: center;
|
|
77
|
+
padding: calc(var(--spacing) * 0.5) var(--spacing);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.preview h3, .preview h4 {
|
|
81
|
+
margin: 0;
|
|
82
|
+
padding: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.preview ::part(content) {
|
|
86
|
+
padding: var(--spacing);
|
|
87
|
+
gap: var(--spacing);
|
|
88
|
+
background: var(--background);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.preview label {
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-columns: 180px auto 100px;
|
|
94
|
+
gap: var(--spacing);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.preview label [part="caption"] {
|
|
98
|
+
text-align: right;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.preview label:has(:invalid:required)::after {
|
|
102
|
+
content: '* required'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (max-width: 500px) {
|
|
106
|
+
.preview label [part="caption"] {
|
|
107
|
+
text-align: center;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.preview label {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
position: relative;
|
|
114
|
+
align-items: stretch;
|
|
115
|
+
gap: calc(var(--spacing) * 0.5);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.preview label:has(:invalid:required)::after {
|
|
119
|
+
position: absolute;
|
|
120
|
+
top: 0;
|
|
121
|
+
right: 0;
|
|
122
|
+
content: '*'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.preview xin-field [part="field"],
|
|
126
|
+
.preview xin-field [part="input"] > * {
|
|
127
|
+
display: flex;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.preview :invalid {
|
|
133
|
+
box-shadow: inset 0 0 0 2px #F008;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## `<xin-form>`
|
|
138
|
+
|
|
139
|
+
`<xin-form>` prevents the default form behavior when a `submit` event is triggered and instead validates the
|
|
140
|
+
form contents (generating feedback if desired) and calls its `submitCallback(value: {[key: string]: any}, isValid: boolean): void`
|
|
141
|
+
method.
|
|
142
|
+
|
|
143
|
+
`<xin-form>` offers a `fields` proxy that allows values stored in the form to be updated. Any changes will trigger a `change`
|
|
144
|
+
event on the `<xin-form>` (in addition to any events fired by form fields).
|
|
145
|
+
|
|
146
|
+
`<xin-form>` instances have `value` and `isValid` properties you can access any time. Note that `isValid` is computed
|
|
147
|
+
and triggers form validation.
|
|
148
|
+
|
|
149
|
+
`<xin-form>` has `header` and `footer` `<slot>`s in addition to default `<slot>`, which is tucked inside a `<form>` element.
|
|
150
|
+
|
|
151
|
+
## `<xin-field>`
|
|
152
|
+
|
|
153
|
+
`<xin-field>` is a simple web-component with no shadowDOM that combines an `<input>` field wrapped with a `<label>`. Any
|
|
154
|
+
content of the custom-element will become the `caption` or you can simply set the `caption` attribute.
|
|
155
|
+
|
|
156
|
+
You can replace the default `<input>` field by adding an element to the slot `input` (it's a `xinSlot`) whereupon
|
|
157
|
+
the `value` of that element will be used instead of the built-in `<input>`. (The `<input>` is retained and
|
|
158
|
+
is used to drive form-validation.)
|
|
159
|
+
|
|
160
|
+
`<xin-field>` supports the following attributes:
|
|
161
|
+
|
|
162
|
+
- `caption` labels the field
|
|
163
|
+
- `key` determines the form property the field will populate
|
|
164
|
+
- `type` determines the data-type: '' | 'checkbox' | 'number' | 'range' | 'date' | 'text' | 'color'
|
|
165
|
+
- `optional` turns off the `required` attribute (fields are required by default)
|
|
166
|
+
- `pattern` is an (optional) regex pattern
|
|
167
|
+
- `placeholder` is an (optional) placeholder
|
|
168
|
+
|
|
169
|
+
The `text` type populates the `input` slot with a `<textarea>` element.
|
|
170
|
+
|
|
171
|
+
The `color` type populates the `input` slot with a `<xin-color>` element (and thus supports colors with alpha values).
|
|
172
|
+
|
|
173
|
+
<xin-css-var-editor element-selector="xin-field" target-selector=".preview"></xin-css-var-editor>
|
|
174
|
+
*/
|
|
175
|
+
import { Component as XinComponent, elements, varDefault, } from 'xinjs';
|
|
176
|
+
import { colorInput } from './color-input';
|
|
177
|
+
const { form, slot, xinSlot, label, input, span } = elements;
|
|
178
|
+
function attr(element, name, value) {
|
|
179
|
+
if (value !== '' && value !== false) {
|
|
180
|
+
element.setAttribute(name, value);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
element.removeAttribute(name);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function getInputValue(input) {
|
|
187
|
+
switch (input.type) {
|
|
188
|
+
case 'checkbox':
|
|
189
|
+
return input.checked;
|
|
190
|
+
case 'radio': {
|
|
191
|
+
const picked = input.parentElement?.querySelector(`input[type="radio"][name="${input.name}"]:checked`);
|
|
192
|
+
return picked ? picked.value : null;
|
|
193
|
+
}
|
|
194
|
+
case 'range':
|
|
195
|
+
case 'number':
|
|
196
|
+
return Number(input.value);
|
|
197
|
+
default:
|
|
198
|
+
return Array.isArray(input.value) && input.value.length === 0
|
|
199
|
+
? null
|
|
200
|
+
: input.value;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function setElementValue(input, value) {
|
|
204
|
+
if (!(input instanceof HTMLElement)) {
|
|
205
|
+
// do nothing
|
|
206
|
+
}
|
|
207
|
+
else if (input instanceof HTMLInputElement) {
|
|
208
|
+
switch (input.type) {
|
|
209
|
+
case 'checkbox':
|
|
210
|
+
input.checked = value;
|
|
211
|
+
break;
|
|
212
|
+
case 'radio':
|
|
213
|
+
input.checked = value === input.value;
|
|
214
|
+
break;
|
|
215
|
+
default:
|
|
216
|
+
input.value = String(value || '');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
if (value != null || input.value != null) {
|
|
221
|
+
;
|
|
222
|
+
input.value = String(value || '');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export class XinField extends XinComponent {
|
|
227
|
+
caption = '';
|
|
228
|
+
key = '';
|
|
229
|
+
type = '';
|
|
230
|
+
optional = false;
|
|
231
|
+
pattern = '';
|
|
232
|
+
placeholder = '';
|
|
233
|
+
min = '';
|
|
234
|
+
max = '';
|
|
235
|
+
step = '';
|
|
236
|
+
fixedPrecision = -1;
|
|
237
|
+
value = null;
|
|
238
|
+
content = label(xinSlot({ part: 'caption' }), span({ part: 'field' }, xinSlot({ part: 'input', name: 'input' }), input({ part: 'valueHolder' })));
|
|
239
|
+
constructor() {
|
|
240
|
+
super();
|
|
241
|
+
this.initAttributes('caption', 'key', 'type', 'optional', 'pattern', 'placeholder', 'min', 'max', 'step', 'fixedPrecision', 'prefix', 'suffix');
|
|
242
|
+
}
|
|
243
|
+
valueChanged = false;
|
|
244
|
+
handleChange = () => {
|
|
245
|
+
const { input, valueHolder } = this.parts;
|
|
246
|
+
const inputElement = (input.children[0] || valueHolder);
|
|
247
|
+
if (inputElement !== valueHolder) {
|
|
248
|
+
valueHolder.value = inputElement.value;
|
|
249
|
+
}
|
|
250
|
+
this.value = getInputValue(inputElement);
|
|
251
|
+
this.valueChanged = true;
|
|
252
|
+
const form = this.closest('xin-form');
|
|
253
|
+
if (form && this.key !== '') {
|
|
254
|
+
switch (this.type) {
|
|
255
|
+
case 'checkbox':
|
|
256
|
+
form.fields[this.key] = inputElement.checked;
|
|
257
|
+
break;
|
|
258
|
+
case 'number':
|
|
259
|
+
case 'range':
|
|
260
|
+
if (this.fixedPrecision > -1) {
|
|
261
|
+
inputElement.value = Number(inputElement.value).toFixed(this.fixedPrecision);
|
|
262
|
+
form.fields[this.key] = Number(inputElement.value);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
form.fields[this.key] = Number(inputElement.value);
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
default:
|
|
269
|
+
form.fields[this.key] = inputElement.value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
initialize(form) {
|
|
274
|
+
const initialValue = form.fields[this.key] !== undefined ? form.fields[this.key] : this.value;
|
|
275
|
+
if (initialValue != null && initialValue !== '') {
|
|
276
|
+
if (form.fields[this.key] == null)
|
|
277
|
+
form.fields[this.key] = initialValue;
|
|
278
|
+
this.value = initialValue;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
connectedCallback() {
|
|
282
|
+
super.connectedCallback();
|
|
283
|
+
const { input, valueHolder } = this.parts;
|
|
284
|
+
const form = this.closest(XinForm.tagName);
|
|
285
|
+
if (form instanceof XinForm) {
|
|
286
|
+
this.initialize(form);
|
|
287
|
+
}
|
|
288
|
+
valueHolder.addEventListener('change', this.handleChange);
|
|
289
|
+
input.addEventListener('change', this.handleChange, true);
|
|
290
|
+
}
|
|
291
|
+
render() {
|
|
292
|
+
if (this.valueChanged) {
|
|
293
|
+
this.valueChanged = false;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const { input, caption, valueHolder, field } = this.parts;
|
|
297
|
+
if (caption.textContent?.trim() === '') {
|
|
298
|
+
caption.append(this.caption !== '' ? this.caption : this.key);
|
|
299
|
+
}
|
|
300
|
+
if (this.type === 'text') {
|
|
301
|
+
input.textContent = '';
|
|
302
|
+
const textarea = elements.textarea({ value: this.value });
|
|
303
|
+
if (this.placeholder) {
|
|
304
|
+
textarea.setAttribute('placeholder', this.placeholder);
|
|
305
|
+
}
|
|
306
|
+
input.append(textarea);
|
|
307
|
+
}
|
|
308
|
+
else if (this.type === 'color') {
|
|
309
|
+
input.textContent = '';
|
|
310
|
+
input.append(colorInput({ value: this.value }));
|
|
311
|
+
}
|
|
312
|
+
else if (input.children.length === 0) {
|
|
313
|
+
attr(valueHolder, 'placeholder', this.placeholder);
|
|
314
|
+
attr(valueHolder, 'type', this.type);
|
|
315
|
+
attr(valueHolder, 'pattern', this.pattern);
|
|
316
|
+
attr(valueHolder, 'min', this.min);
|
|
317
|
+
attr(valueHolder, 'max', this.max);
|
|
318
|
+
attr(valueHolder, 'step', this.step);
|
|
319
|
+
}
|
|
320
|
+
setElementValue(valueHolder, this.value);
|
|
321
|
+
setElementValue(input.children[0], this.value);
|
|
322
|
+
this.prefix
|
|
323
|
+
? field.setAttribute('prefix', this.prefix)
|
|
324
|
+
: field.removeAttribute('prefix');
|
|
325
|
+
this.suffix
|
|
326
|
+
? field.setAttribute('suffix', this.suffix)
|
|
327
|
+
: field.removeAttribute('suffix');
|
|
328
|
+
valueHolder.classList.toggle('hidden', input.children.length > 0);
|
|
329
|
+
if (input.children.length > 0) {
|
|
330
|
+
valueHolder.setAttribute('tabindex', '-1');
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
valueHolder.removeAttribute('tabindex');
|
|
334
|
+
}
|
|
335
|
+
input.style.display = input.children.length === 0 ? 'none' : '';
|
|
336
|
+
attr(valueHolder, 'required', !this.optional);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
export class XinForm extends XinComponent {
|
|
340
|
+
context = {};
|
|
341
|
+
value = {};
|
|
342
|
+
get isValid() {
|
|
343
|
+
const widgets = [...this.querySelectorAll('*')].filter((widget) => widget.required !== undefined);
|
|
344
|
+
return widgets.find((widget) => !widget.reportValidity()) === undefined;
|
|
345
|
+
}
|
|
346
|
+
static styleSpec = {
|
|
347
|
+
':host': {
|
|
348
|
+
display: 'flex',
|
|
349
|
+
flexDirection: 'column',
|
|
350
|
+
},
|
|
351
|
+
':host::part(header), :host::part(footer)': {
|
|
352
|
+
display: 'flex',
|
|
353
|
+
},
|
|
354
|
+
':host::part(content)': {
|
|
355
|
+
display: 'flex',
|
|
356
|
+
flexDirection: 'column',
|
|
357
|
+
overflow: 'hidden auto',
|
|
358
|
+
height: '100%',
|
|
359
|
+
width: '100%',
|
|
360
|
+
position: 'relative',
|
|
361
|
+
boxSizing: 'border-box',
|
|
362
|
+
},
|
|
363
|
+
':host form': {
|
|
364
|
+
display: 'flex',
|
|
365
|
+
flex: '1 1 auto',
|
|
366
|
+
position: 'relative',
|
|
367
|
+
overflow: 'hidden',
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
content = [
|
|
371
|
+
slot({ part: 'header', name: 'header' }),
|
|
372
|
+
form({ part: 'form' }, slot({ part: 'content' })),
|
|
373
|
+
slot({ part: 'footer', name: 'footer' }),
|
|
374
|
+
];
|
|
375
|
+
getField = (key) => {
|
|
376
|
+
return this.querySelector(`xin-field[key="${key}"]`);
|
|
377
|
+
};
|
|
378
|
+
get fields() {
|
|
379
|
+
if (typeof this.value === 'string') {
|
|
380
|
+
try {
|
|
381
|
+
this.value = JSON.parse(this.value);
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
console.log('<xin-form> could not use its value, expects valid JSON');
|
|
385
|
+
this.value = {};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const { getField } = this;
|
|
389
|
+
const dispatch = this.dispatchEvent.bind(this);
|
|
390
|
+
return new Proxy(this.value, {
|
|
391
|
+
get(target, prop) {
|
|
392
|
+
return target[prop];
|
|
393
|
+
},
|
|
394
|
+
set(target, prop, newValue) {
|
|
395
|
+
if (target[prop] !== newValue) {
|
|
396
|
+
target[prop] = newValue;
|
|
397
|
+
const field = getField(prop);
|
|
398
|
+
if (field) {
|
|
399
|
+
field.value = newValue;
|
|
400
|
+
}
|
|
401
|
+
dispatch(new Event('change'));
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
set fields(values) {
|
|
408
|
+
const fields = [...this.querySelectorAll(XinField.tagName)];
|
|
409
|
+
for (const field of fields) {
|
|
410
|
+
field.value = values[field.key];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
submit = () => {
|
|
414
|
+
this.parts.form.dispatchEvent(new Event('submit'));
|
|
415
|
+
};
|
|
416
|
+
handleSubmit = (event) => {
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
event.stopPropagation();
|
|
419
|
+
this.submitCallback(this.value, this.isValid);
|
|
420
|
+
};
|
|
421
|
+
submitCallback = (value, isValid) => {
|
|
422
|
+
console.log('override submitCallback to handle this data', {
|
|
423
|
+
value,
|
|
424
|
+
isValid,
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
connectedCallback() {
|
|
428
|
+
super.connectedCallback();
|
|
429
|
+
const { form } = this.parts;
|
|
430
|
+
form.addEventListener('submit', this.handleSubmit);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
export const xinField = XinField.elementCreator({
|
|
434
|
+
tag: 'xin-field',
|
|
435
|
+
styleSpec: {
|
|
436
|
+
':host [part="field"]': {
|
|
437
|
+
position: 'relative',
|
|
438
|
+
display: 'flex',
|
|
439
|
+
alignItems: 'center',
|
|
440
|
+
gap: varDefault.prefixSuffixGap('8px'),
|
|
441
|
+
},
|
|
442
|
+
':host [part="field"][prefix]::before': {
|
|
443
|
+
content: 'attr(prefix)',
|
|
444
|
+
},
|
|
445
|
+
':host [part="field"][suffix]::after': {
|
|
446
|
+
content: 'attr(suffix)',
|
|
447
|
+
},
|
|
448
|
+
':host [part="field"] > *, :host [part="input"] > *': {
|
|
449
|
+
width: '100%',
|
|
450
|
+
},
|
|
451
|
+
':host textarea': {
|
|
452
|
+
resize: 'none',
|
|
453
|
+
},
|
|
454
|
+
':host input[type="checkbox"]': {
|
|
455
|
+
width: 'fit-content',
|
|
456
|
+
},
|
|
457
|
+
':host .hidden': {
|
|
458
|
+
position: 'absolute',
|
|
459
|
+
pointerEvents: 'none',
|
|
460
|
+
opacity: 0,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
export const xinForm = XinForm.elementCreator({
|
|
465
|
+
tag: 'xin-form',
|
|
466
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface XinButton {
|
|
2
|
+
index: number;
|
|
3
|
+
pressed: boolean;
|
|
4
|
+
value: number;
|
|
5
|
+
}
|
|
6
|
+
export interface XinGamepad {
|
|
7
|
+
id: string;
|
|
8
|
+
axes: number[];
|
|
9
|
+
buttons: {
|
|
10
|
+
[key: number]: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare function gamepadState(): {
|
|
14
|
+
id: string;
|
|
15
|
+
axes: readonly number[];
|
|
16
|
+
buttons: {
|
|
17
|
+
[key: number]: number;
|
|
18
|
+
};
|
|
19
|
+
}[];
|
|
20
|
+
export declare function gamepadText(): string;
|
|
21
|
+
export interface XinXRControllerComponentState {
|
|
22
|
+
pressed: boolean;
|
|
23
|
+
axes?: number[];
|
|
24
|
+
}
|
|
25
|
+
export interface XinXRControllerState {
|
|
26
|
+
[key: string]: XinXRControllerComponentState;
|
|
27
|
+
}
|
|
28
|
+
export interface XinXRControllerMap {
|
|
29
|
+
[key: string]: XinXRControllerState;
|
|
30
|
+
}
|
|
31
|
+
export declare function xrControllers(xrHelper: any): {
|
|
32
|
+
[key: string]: XinXRControllerState;
|
|
33
|
+
};
|
|
34
|
+
export declare function xrControllersText(controllers?: XinXRControllerMap): string;
|
package/dist/gamepad.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/*#
|
|
2
|
+
# gamepads
|
|
3
|
+
|
|
4
|
+
A couple of utility functions for dealing with gamepads and XRInputs.
|
|
5
|
+
|
|
6
|
+
`gamepadState()` gives you a condensed version of active gamepad state
|
|
7
|
+
|
|
8
|
+
`gamepadText()` provides the above in minimal text form for debugging
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
const { elements } = xinjs
|
|
12
|
+
const { gamepadText } = xinjsui
|
|
13
|
+
|
|
14
|
+
const pre = elements.pre()
|
|
15
|
+
preview.append(pre)
|
|
16
|
+
|
|
17
|
+
const interval = setInterval(() => {
|
|
18
|
+
if (!pre.closest('body')) {
|
|
19
|
+
clearInterval(interval)
|
|
20
|
+
} else {
|
|
21
|
+
pre.textContent = gamepadText()
|
|
22
|
+
}
|
|
23
|
+
}, 100)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## XRInput Devices
|
|
27
|
+
|
|
28
|
+
> This is experimental, the API is changing and stuff doesn't work. Hopefully it
|
|
29
|
+
> will become a lot more generally useful once Apple's VisionPro comes out.
|
|
30
|
+
|
|
31
|
+
`xrControllers(babylonjsXRHelper)` returns an `XinXRControllerMap` structure that tries to
|
|
32
|
+
conveniently render the current state of XR controls. (The babylonjs API for this is horrific!)
|
|
33
|
+
|
|
34
|
+
`xrControllerText(controllerMap)` renders the map output by the above in a compact form
|
|
35
|
+
which is useful when debugging.
|
|
36
|
+
*/
|
|
37
|
+
export function gamepadState() {
|
|
38
|
+
const gamepads = navigator
|
|
39
|
+
.getGamepads()
|
|
40
|
+
.filter((p) => p !== null);
|
|
41
|
+
return gamepads.map((p) => {
|
|
42
|
+
const { id, axes, buttons } = p;
|
|
43
|
+
return {
|
|
44
|
+
id,
|
|
45
|
+
axes,
|
|
46
|
+
buttons: buttons
|
|
47
|
+
.map((button, index) => {
|
|
48
|
+
const { pressed, value } = button;
|
|
49
|
+
return {
|
|
50
|
+
index,
|
|
51
|
+
pressed,
|
|
52
|
+
value,
|
|
53
|
+
};
|
|
54
|
+
})
|
|
55
|
+
.filter((b) => b.pressed || b.value !== 0)
|
|
56
|
+
.reduce((map, button) => {
|
|
57
|
+
map[button.index] = button.value;
|
|
58
|
+
return map;
|
|
59
|
+
}, {}),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export function gamepadText() {
|
|
64
|
+
const state = gamepadState();
|
|
65
|
+
return state.length === 0
|
|
66
|
+
? 'no active gamepads'
|
|
67
|
+
: state
|
|
68
|
+
.map(({ id, axes, buttons }) => {
|
|
69
|
+
const axesText = axes.map((a) => a.toFixed(2)).join(' ');
|
|
70
|
+
const buttonText = Object.keys(buttons)
|
|
71
|
+
.map((key) => `[${key}](${buttons[Number(key)].toFixed(2)})`)
|
|
72
|
+
.join(' ');
|
|
73
|
+
return `${id}\n${axesText}\n${buttonText}`;
|
|
74
|
+
})
|
|
75
|
+
.join('\n');
|
|
76
|
+
}
|
|
77
|
+
export function xrControllers(xrHelper) {
|
|
78
|
+
const controllers = {};
|
|
79
|
+
xrHelper.input.onControllerAddedObservable.add((controller) => {
|
|
80
|
+
controller.onMotionControllerInitObservable.add((mc) => {
|
|
81
|
+
const state = {};
|
|
82
|
+
const componentIds = mc.getComponentIds();
|
|
83
|
+
componentIds.forEach((componentId) => {
|
|
84
|
+
const component = mc.getComponent(componentId);
|
|
85
|
+
state[componentId] = { pressed: component.pressed };
|
|
86
|
+
component.onButtonStateChangedObservable.add(() => {
|
|
87
|
+
state[componentId].pressed = component.pressed;
|
|
88
|
+
});
|
|
89
|
+
// TODO does this work?! inquiring minds…
|
|
90
|
+
if (component.onAxisValueChangedObservable) {
|
|
91
|
+
state[componentId].axes = [];
|
|
92
|
+
component.onAxisValueChangedObservable.add((axes) => {
|
|
93
|
+
state[componentId].axes = axes;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
controllers[mc.handedness] = state;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return controllers;
|
|
101
|
+
}
|
|
102
|
+
export function xrControllersText(controllers) {
|
|
103
|
+
if (controllers === undefined || Object.keys(controllers).length === 0) {
|
|
104
|
+
return 'no xr inputs';
|
|
105
|
+
}
|
|
106
|
+
return Object.keys(controllers)
|
|
107
|
+
.map((controllerId) => {
|
|
108
|
+
const state = controllers[controllerId];
|
|
109
|
+
const buttonText = Object.keys(state)
|
|
110
|
+
.filter((componentId) => state[componentId].pressed)
|
|
111
|
+
.join(' ');
|
|
112
|
+
return `${controllerId}\n${buttonText}`;
|
|
113
|
+
})
|
|
114
|
+
.join('\n');
|
|
115
|
+
}
|