@pageboard/html 0.10.7 → 0.10.12

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.
@@ -0,0 +1,76 @@
1
+ exports.fieldset = {
2
+ title: 'Fieldset',
3
+ icon: '<i class="folder outline icon"></i>',
4
+ menu: 'form',
5
+ group: 'block',
6
+ context: 'form//',
7
+ properties: {
8
+ name: {
9
+ title: 'Show if input named',
10
+ type: 'string',
11
+ format: 'singleline',
12
+ nullable: true,
13
+ $helper: {
14
+ name: 'element-property',
15
+ existing: true
16
+ }
17
+ },
18
+ value: {
19
+ title: 'matches this value',
20
+ type: 'string',
21
+ format: 'singleline',
22
+ $filter: {
23
+ name: 'element-value',
24
+ using: 'name'
25
+ }
26
+ },
27
+ plain: {
28
+ title: 'Without borders',
29
+ type: 'boolean',
30
+ default: false
31
+ }
32
+ },
33
+ contents: "fieldset_legend block+",
34
+ html: '<fieldset class="[plain|?]" data-name="[name]" data-value="[value]" is="element-fieldset"></fieldset>',
35
+ scripts: ["../ui/fieldset.js"]
36
+ };
37
+
38
+ exports.fieldset_legend = {
39
+ inplace: true,
40
+ contents: "inline*",
41
+ html: '<legend>Title</legend>'
42
+ };
43
+
44
+ exports.fieldset_list = {
45
+ title: 'FieldList',
46
+ menu: "form",
47
+ icon: '<i class="icons"><i class="folder outline icon"></i><i class="corner add icon"></i></i>',
48
+ group: "block",
49
+ context: 'form//',
50
+ priority: 0,
51
+ properties: {
52
+ size: {
53
+ title: 'Minimum size',
54
+ type: "integer",
55
+ minimum: 0,
56
+ default: 1
57
+ },
58
+ prefix: {
59
+ title: 'Prefix',
60
+ description: '',
61
+ type: "string",
62
+ format: 'singleline',
63
+ nullable: true
64
+ }
65
+ },
66
+ contents: [{
67
+ id: 'template',
68
+ nodes: 'block+'
69
+ }],
70
+ html: `<element-fieldset-list data-size="[size]" data-prefix="[prefix]">
71
+ <template block-content="template"></template>
72
+ <div class="view"></div>
73
+ </element-fieldset-list>`,
74
+ scripts: ['../ui/fieldset-list.js'],
75
+ stylesheets: ['../ui/fieldset-list.css']
76
+ };
package/elements/form.js CHANGED
@@ -65,6 +65,7 @@ exports.api_form = {
65
65
  icon: '<i class="write icon"></i>',
66
66
  group: 'block form',
67
67
  menu: "form",
68
+ standalone: true,
68
69
  required: ["action"],
69
70
  $lock: {
70
71
  'data.action.parameters': 'webmaster'
@@ -0,0 +1,150 @@
1
+ exports.input_date_time = {
2
+ title: 'DateTime',
3
+ icon: '<i class="calendar outline icon"></i>',
4
+ menu: "form",
5
+ required: ["name"],
6
+ group: "block",
7
+ context: 'form//',
8
+ properties: {
9
+ name: {
10
+ title: "Name",
11
+ description: "The form object key",
12
+ type: "string",
13
+ format: "singleline"
14
+ },
15
+ value: {
16
+ title: "Default value",
17
+ nullable: true,
18
+ type: "string",
19
+ format: "singleline"
20
+ },
21
+ placeholder: {
22
+ title: "Placeholder",
23
+ nullable: true,
24
+ type: "string",
25
+ format: "singleline"
26
+ },
27
+ required: {
28
+ title: 'Required',
29
+ type: 'boolean',
30
+ default: false
31
+ },
32
+ disabled: {
33
+ title: 'Disabled',
34
+ type: 'boolean',
35
+ default: false
36
+ },
37
+ format: {
38
+ title: 'Format',
39
+ default: "datetime",
40
+ anyOf: [{
41
+ const: "datetime",
42
+ title: "Date-Time"
43
+ }, {
44
+ const: "date",
45
+ title: "Date"
46
+ }, {
47
+ const: "time",
48
+ title: "Time"
49
+ }]
50
+ },
51
+ step: {
52
+ title: 'Step',
53
+ description: 'rounding/increment in seconds',
54
+ type: 'integer',
55
+ nullable: true,
56
+ anyOf: [{
57
+ const: 60 * 5,
58
+ title: '5 minutes'
59
+ }, {
60
+ const: 60 * 15,
61
+ title: '15 minutes'
62
+ }, {
63
+ const: 60 * 30,
64
+ title: '30 minutes'
65
+ }, {
66
+ const: 60 * 60,
67
+ title: '1 hour'
68
+ }, {
69
+ const: 86400,
70
+ title: '1 day'
71
+ }]
72
+ }
73
+ },
74
+ contents: {
75
+ id: 'label',
76
+ nodes: 'inline*'
77
+ },
78
+ html: `<div class="field">
79
+ <label block-content="label">Label</label>
80
+ <input is="element-input-date"
81
+ name="[name]" disabled="[disabled]" placeholder="[placeholder]"
82
+ required="[required]" value="[value]" step="[step|magnet:]"
83
+ type="[format|eq:datetime:datetime-local]"
84
+ />
85
+ </div>`,
86
+ scripts: [
87
+ '../ui/input-date.js'
88
+ ]
89
+ };
90
+
91
+ exports.input_date_slot = {
92
+ title: 'DateSlot',
93
+ icon: '<i class="calendar outline icon"></i>',
94
+ menu: "form",
95
+ required: ["nameStart", "nameEnd"],
96
+ group: "block",
97
+ context: 'form//',
98
+ properties: {
99
+ nameStart: {
100
+ title: "Name for start date",
101
+ description: "The form object key",
102
+ type: "string",
103
+ format: "singleline"
104
+ },
105
+ nameEnd: {
106
+ title: "Name for end date",
107
+ description: "The form object key",
108
+ type: "string",
109
+ format: "singleline"
110
+ },
111
+ valueStart: {
112
+ title: 'Start time',
113
+ nullable: true,
114
+ type: "string",
115
+ format: "singleline"
116
+ },
117
+ valueEnd: {
118
+ title: 'End time',
119
+ nullable: true,
120
+ type: "string",
121
+ format: "singleline"
122
+ },
123
+ required: {
124
+ title: 'Required',
125
+ type: 'boolean',
126
+ default: false
127
+ },
128
+ disabled: {
129
+ title: 'Disabled',
130
+ type: 'boolean',
131
+ default: false
132
+ },
133
+ step: exports.input_date_time.properties.step,
134
+ format: exports.input_date_time.properties.format
135
+ },
136
+ contents: {
137
+ id: 'label',
138
+ nodes: 'inline*'
139
+ },
140
+ html: `<div class="field">
141
+ <label block-content="label">Label</label>
142
+ <element-input-date-slot type="[format|eq:datetime:datetime-local]" step="[step|magnet:]">
143
+ <input is="element-input-date" name="[nameStart]" value="[valueStart]" />
144
+ <input is="element-input-date" name="[nameEnd]" value="[valueEnd]" />
145
+ </element-input-date-slot>
146
+ </div>`,
147
+ scripts: [
148
+ '../ui/input-date-slot.js'
149
+ ]
150
+ };
@@ -28,11 +28,6 @@ exports.input_file = {
28
28
  type: 'boolean',
29
29
  default: false
30
30
  },
31
- now: {
32
- title: 'Upload on change',
33
- type: 'boolean',
34
- default: false
35
- },
36
31
  limits: {
37
32
  title: 'Limits',
38
33
  type: 'object',
@@ -51,13 +46,6 @@ exports.input_file = {
51
46
  type: 'string'
52
47
  },
53
48
  default: ['*/*']
54
- },
55
- files: {
56
- title: 'Files',
57
- description: 'Max number of files',
58
- type: 'integer',
59
- minimum: 1,
60
- default: 1
61
49
  }
62
50
  }
63
51
  }
@@ -68,16 +56,9 @@ exports.input_file = {
68
56
  },
69
57
  html: `<div class="field">
70
58
  <label block-content="label">Label</label>
71
- <element-input-file class="ui action input" data-now="[now]">
72
- <input type="text" name="[name]" placeholder="[placeholder]" />
73
- <input type="file" id="[$id]" required="[required]"
74
- disabled="[disabled]" multiple="[limits.files|gt:1|battr]" />
75
- <label for="[$id]" class="ui icon button">
76
- <i class="upload icon"></i>
77
- <i class="delete icon"></i>
78
- </label>
79
- <div class="mini floating ui basic label"></div>
80
- </element-input-file>
59
+ <div class="ui basic label"></div>
60
+ <input is="element-input-file" type="file" id="[$id]" required="[required]"
61
+ disabled="[disabled]" accept="[limits.types|join:,]" name="[name]" placeholder="[placeholder]" />
81
62
  </div>`,
82
63
  stylesheets: [
83
64
  '../lib/components/input.css',
@@ -54,6 +54,7 @@ exports.input_property = {
54
54
  // /.api/form wraps it into block.data
55
55
  list.shift();
56
56
  name = list.join('.');
57
+ const id = scope.$id;
57
58
  let prop = el;
58
59
  let propKey;
59
60
  let required = false;
@@ -63,12 +64,13 @@ exports.input_property = {
63
64
  required = prop.required && prop.required.indexOf(propKey) >= 0;
64
65
  if (cases) {
65
66
  prop = cases[propKey];
67
+ name = list.slice(0, i - 1).concat(list.slice(i + 1)).join('.');
66
68
  cases = null;
67
69
  } else {
68
70
  if (prop.select && prop.select.$data == `0/${propKey}`) {
69
71
  cases = prop.selectCases;
70
72
  }
71
- prop = (prop.properties || {})[propKey] || null;
73
+ prop = (prop.items && prop.items.properties || prop.properties || {})[propKey] || null;
72
74
  }
73
75
  if (prop == null) break;
74
76
  }
@@ -77,8 +79,14 @@ exports.input_property = {
77
79
  }
78
80
  node.textContent = "";
79
81
  if (prop.nullable) required = false;
80
- let listOf = prop.anyOf || prop.oneOf;
81
- let propType;
82
+ let propType = prop;
83
+ let multiple = d.multiple;
84
+ if (prop.type == "array" && prop.items && Array.isArray(prop.items) == false) {
85
+ propType = prop.items;
86
+ multiple = true;
87
+ }
88
+
89
+ let listOf = propType.anyOf || propType.oneOf;
82
90
  if (listOf) {
83
91
  const listOfNo = listOf.filter((item) => item.type != "null");
84
92
  if (listOfNo.length != listOf.length) {
@@ -87,7 +95,7 @@ exports.input_property = {
87
95
  if (listOfNo.length == 1 && listOfNo[0].const === undefined) {
88
96
  propType = listOfNo[0];
89
97
  listOf = null;
90
- } else if (d.multiple) {
98
+ } else if (multiple) {
91
99
  listOf = listOfNo;
92
100
  }
93
101
  } else if (Array.isArray(prop.type)) {
@@ -106,7 +114,6 @@ exports.input_property = {
106
114
  listOf = null; // cannot deal with this for now
107
115
  }
108
116
  }
109
- if (!propType) propType = prop;
110
117
 
111
118
  if (listOf) {
112
119
  if (listOf.length <= d.radios) {
@@ -126,7 +133,8 @@ exports.input_property = {
126
133
  content = content.lastElementChild;
127
134
  for (const item of listOf) {
128
135
  content.appendChild(view.render({
129
- type: d.multiple ? 'input_checkbox' : 'input_radio',
136
+ type: multiple ? 'input_checkbox' : 'input_radio',
137
+ id,
130
138
  data: {
131
139
  name: name,
132
140
  value: item.type == "null" ? null : item.const,
@@ -151,10 +159,11 @@ exports.input_property = {
151
159
  }));
152
160
  }
153
161
  node.appendChild(view.render({
162
+ id,
154
163
  type: 'input_select',
155
164
  data: {
156
165
  name: name,
157
- multiple: d.multiple,
166
+ multiple: multiple,
158
167
  placeholder: prop.description,
159
168
  disabled: d.disabled,
160
169
  required: required
@@ -170,15 +179,16 @@ exports.input_property = {
170
179
  if (propType.minimum != null && propType.maximum != null) {
171
180
  if (propType.maximum - propType.minimum <= d.range) {
172
181
  return node.appendChild(view.render({
182
+ id,
173
183
  type: 'input_range',
174
184
  data: {
175
185
  name: name,
176
186
  min: propType.minimum,
177
187
  max: propType.maximum,
178
- value: d.multiple ? `${propType.minimum}⩽${propType.maximum}` : propType.default,
188
+ value: multiple ? `${propType.minimum}⩽${propType.maximum}` : propType.default,
179
189
  disabled: d.disabled,
180
190
  required: required,
181
- multiple: d.multiple,
191
+ multiple: multiple,
182
192
  step: step
183
193
  },
184
194
  content: {
@@ -188,6 +198,7 @@ exports.input_property = {
188
198
  }
189
199
  }
190
200
  node.appendChild(view.render({
201
+ id,
191
202
  type: 'input_number',
192
203
  data: {
193
204
  name: name,
@@ -204,6 +215,7 @@ exports.input_property = {
204
215
  }));
205
216
  } else if (propType.type == "boolean") {
206
217
  node.appendChild(view.render({
218
+ id,
207
219
  type: 'input_checkbox',
208
220
  data: {
209
221
  name: name,
@@ -215,35 +227,13 @@ exports.input_property = {
215
227
  label: prop.title
216
228
  }
217
229
  }));
218
- } else if (propType.type == "string" && propType.format == "date") {
219
- let type = "input_date_time";
220
- if (!scope.$elements[type]) {
221
- type = 'input_text';
222
- }
230
+ } else if (propType.type == "string" && ["date", "time", "date-time"].includes(propType.format)) {
223
231
  node.appendChild(view.render({
224
- type: type,
232
+ id,
233
+ type: 'input_date_time',
225
234
  data: {
226
235
  name: name,
227
- type: propType.format,
228
- default: propType.default,
229
- disabled: d.disabled,
230
- required: required,
231
- step: propType.step
232
- },
233
- content: {
234
- label: prop.title
235
- }
236
- }));
237
- } else if (propType.type == "string" && propType.format == "time") {
238
- let type = "input_date_time";
239
- if (!scope.$elements[type]) {
240
- type = 'input_text';
241
- }
242
- node.appendChild(view.render({
243
- type: type,
244
- data: {
245
- name: name,
246
- type: propType.format,
236
+ format: propType.format.replace('-', ''),
247
237
  default: propType.default,
248
238
  disabled: d.disabled,
249
239
  required: required,
@@ -255,7 +245,7 @@ exports.input_property = {
255
245
  }));
256
246
  } else if (propType.$helper && propType.$helper.name == "href") {
257
247
  const limits = {
258
- files: d.multiple ? null : 1
248
+ files: multiple ? null : 1
259
249
  };
260
250
  const filter = propType.$helper.filter;
261
251
  if (filter && filter.type) {
@@ -266,6 +256,7 @@ exports.input_property = {
266
256
  });
267
257
  }
268
258
  node.appendChild(view.render({
259
+ id,
269
260
  type: 'input_file',
270
261
  data: {
271
262
  name: name,
@@ -279,15 +270,15 @@ exports.input_property = {
279
270
  }
280
271
  }));
281
272
  } else {
273
+ const type = (propType.format || propType.pattern) ? 'text' : 'textarea';
282
274
  node.appendChild(view.render({
275
+ id,
283
276
  type: 'input_text',
284
277
  data: {
285
- name: name,
286
- type: propType.format ? 'text' : 'textarea',
278
+ name, type, required,
287
279
  disabled: d.disabled,
288
280
  default: propType.default,
289
- placeholder: propType.description,
290
- required: required
281
+ placeholder: propType.description
291
282
  },
292
283
  content: {
293
284
  label: prop.title
@@ -1,26 +1,3 @@
1
- exports.fieldset = {
2
- title: 'Fieldset',
3
- icon: '<i class="folder outline icon"></i>',
4
- menu: 'form',
5
- group: 'block',
6
- context: 'form//',
7
- properties: {
8
- plain: {
9
- title: 'Without borders',
10
- type: 'boolean',
11
- default: false
12
- }
13
- },
14
- contents: "fieldset_legend block+",
15
- html: '<fieldset class="[plain|?]"></fieldset>'
16
- };
17
-
18
- exports.fieldset_legend = {
19
- inplace: true,
20
- contents: "inline*",
21
- html: '<legend>Title</legend>'
22
- };
23
-
24
1
  exports.input_button = {
25
2
  title: 'Button',
26
3
  icon: '<i class="hand pointer icon"></i>',
@@ -204,8 +181,8 @@ exports.input_text = {
204
181
  };
205
182
 
206
183
  exports.input_number = {
207
- title: 'Number input',
208
- icon: '<i class="text cursor icon">n</i>',
184
+ title: 'Number',
185
+ icon: '<i class="text cursor icon"></i>',
209
186
  menu: "form",
210
187
  required: ["name"],
211
188
  group: "block",
@@ -339,8 +316,8 @@ exports.input_checkbox = {
339
316
  <div class="ui [toggle|?] checkbox">
340
317
  <input type="checkbox" required="[required]" disabled="[disabled]"
341
318
  name="[name]" value="[value]" checked="[checked]"
342
- id="for-[name][value|or:|pre:-]" />
343
- <label block-content="label" for="for-[name][value|or:|pre:-]">Label</label>
319
+ id="for-[name][value|or:|pre:-]-[$id|slice:0:6]" />
320
+ <label block-content="label" for="for-[name][value|or:|pre:-]-[$id|slice:0:6]">Label</label>
344
321
  </div>
345
322
  </div>`,
346
323
  stylesheets: [
@@ -385,8 +362,8 @@ exports.input_radio = {
385
362
  <div class="ui radio checkbox">
386
363
  <input type="radio" disabled="[disabled]"
387
364
  name="[name]" value="[value|or:]" checked="[checked]"
388
- id="for-[name][value|or:|pre:-]" />
389
- <label block-content="label" for="for-[name][value|or:|pre:-]">Label</label>
365
+ id="for-[name][value|or:|pre:-]-[$id|slice:0:6]" />
366
+ <label block-content="label" for="for-[name][value|or:|pre:-]-[$id|slice:0:6]">Label</label>
390
367
  </div>
391
368
  </div>`,
392
369
  stylesheets: [
@@ -482,36 +459,4 @@ exports.input_select_option = {
482
459
  ></element-select-option>`
483
460
  };
484
461
 
485
- exports.fieldset_list = {
486
- title: 'FieldList',
487
- menu: "form",
488
- icon: '<i class="icons"><i class="folder outline icon"></i><i class="corner add icon"></i></i>',
489
- group: "block",
490
- context: 'form//',
491
- priority: 0,
492
- properties: {
493
- size: {
494
- title: 'Minimum size',
495
- type: "integer",
496
- minimum: 0,
497
- default: 1
498
- },
499
- prefix: {
500
- title: 'Prefix',
501
- description: '',
502
- type: "string",
503
- format: 'singleline',
504
- nullable: true
505
- }
506
- },
507
- contents: [{
508
- id: 'template',
509
- nodes: 'block+'
510
- }],
511
- html: `<element-fieldset-list data-size="[size]" data-prefix="[prefix]">
512
- <template block-content="template"></template>
513
- <div class="view"></div>
514
- </element-fieldset-list>`,
515
- scripts: ['../ui/fieldset-list.js'],
516
- stylesheets: ['../ui/fieldset-list.css']
517
- };
462
+
@@ -235,4 +235,4 @@ this.window.objectFitImages = (function () {
235
235
 
236
236
  return ofi_commonJs;
237
237
 
238
- }());
238
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pageboard/html",
3
- "version": "0.10.7",
3
+ "version": "0.10.12",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,5 +1,4 @@
1
1
  class HTMLElementFieldsetList extends VirtualHTMLElement {
2
- #size
3
2
  fill(values, scope) {
4
3
  const list = this.listFromValues(Object.assign({}, values));
5
4
  this.resize(list.length, scope);
@@ -8,7 +7,7 @@ class HTMLElementFieldsetList extends VirtualHTMLElement {
8
7
  patch(state) {
9
8
  this.ownTpl.prerender();
10
9
  if (this.isContentEditable) return;
11
- if (!this.#size) this.resize(0, state.scope);
10
+ if (!this.size) this.resize(0, state.scope);
12
11
  }
13
12
 
14
13
  setup(state) {
@@ -17,8 +16,8 @@ class HTMLElementFieldsetList extends VirtualHTMLElement {
17
16
 
18
17
  resize(size, scope) {
19
18
  const len = Math.max(Number(this.dataset.size) || 0, size);
20
- if (this.#size == len) return;
21
- this.#size = len;
19
+ if (this.size == len) return;
20
+ this.size = len;
22
21
 
23
22
  const tpl = this.ownTpl.content.cloneNode(true);
24
23
  for (const node of tpl.querySelectorAll('[block-id]')) {
package/ui/fieldset.js ADDED
@@ -0,0 +1,36 @@
1
+ class HTMLCustomFieldSetElement extends HTMLFieldSetElement {
2
+ static defaults = {
3
+ dataName: null,
4
+ dataValue: null
5
+ };
6
+ constructor() {
7
+ super();
8
+ this.init?.();
9
+ }
10
+
11
+ #update() {
12
+ if (this.isContentEditable || !this.options.name) return;
13
+ const name = this.options.name.split(".").slice(1).join('.');
14
+ const val = this.form?.read(true)?.[name];
15
+ this.disabled = this.hidden = val != this.options.value;
16
+ }
17
+
18
+ patch() {
19
+ this.#update();
20
+ }
21
+ setup() {
22
+ this.form?.addEventListener('change', this);
23
+ }
24
+ close() {
25
+ this.form?.removeEventListener('change', this);
26
+ }
27
+ handleEvent(e) {
28
+ if (e.type == "change") {
29
+ this.#update();
30
+ }
31
+ }
32
+ }
33
+
34
+ Page.ready(() => {
35
+ VirtualHTMLElement.define(`element-fieldset`, HTMLCustomFieldSetElement, 'fieldset');
36
+ });
package/ui/form.js CHANGED
@@ -14,7 +14,7 @@ class HTMLCustomFormElement extends HTMLFormElement {
14
14
  if (name && name == this.name) {
15
15
  state.vars.submit = true;
16
16
  }
17
- const vars = state.templatesQuery(this);
17
+ const vars = state.templatesQuery(this) || {};
18
18
  for (const [key, val] of Object.entries(vars)) {
19
19
  this.setAttribute('data-' + key, val);
20
20
  }
@@ -80,6 +80,9 @@ class HTMLCustomFormElement extends HTMLFormElement {
80
80
  if (defVal == "") defVal = null;
81
81
  if (!withDefaults && query[node.name] == defVal) {
82
82
  query[node.name] = undefined;
83
+ } else {
84
+ // not yet using form-associated custom input
85
+ query[node.name] = node.value;
83
86
  }
84
87
  }
85
88
  if (query[node.name] === undefined && withDefaults) {
@@ -181,6 +184,9 @@ class HTMLCustomFormElement extends HTMLFormElement {
181
184
  if (!this.action) return;
182
185
  window.sessionStorage.removeItem(this.action);
183
186
  }
187
+ handleReset(e, state) {
188
+ this.reset();
189
+ }
184
190
  handleSubmit(e, state) {
185
191
  if (e.type == "submit") e.preventDefault();
186
192
  if (this.isContentEditable) return;
@@ -234,9 +240,9 @@ class HTMLCustomFormElement extends HTMLFormElement {
234
240
 
235
241
  const data = { $query };
236
242
  return Promise.all(Array.from(form.elements).filter((node) => {
237
- return node.type == "file";
243
+ return Boolean(node.presubmit);
238
244
  }).map((input) => {
239
- return input.closest('element-input-file').upload();
245
+ return input.presubmit();
240
246
  })).then(() => {
241
247
  data.$query = state.query;
242
248
  data.$request = form.read(true);
@@ -438,6 +444,7 @@ Page.ready((state) => {
438
444
  if (action == "toggle") {
439
445
  action = val ? "enable" : "disable";
440
446
  }
447
+ // NB: call Class methods to deal with uninstantiated custom form
441
448
  if (action == "enable") {
442
449
  HTMLCustomFormElement.prototype.enable.call(form);
443
450
  } else if (action == "disable") {
@@ -0,0 +1,76 @@
1
+ class HTMLElementInputDateSlot extends VirtualHTMLElement {
2
+ handleChange(e, state) {
3
+ this.update(e.target);
4
+ }
5
+
6
+ get type() {
7
+ const type = this.getAttribute('type');
8
+ const step = this.step;
9
+ if (step) {
10
+ if (step >= 86400) return "date";
11
+ else if (type == "date") return "datetime-local";
12
+ }
13
+ return type;
14
+ }
15
+ set type(f) {
16
+ this.setAttribute('type', f);
17
+ }
18
+ get step() {
19
+ const step = parseInt(this.getAttribute('step'));
20
+ if (Number.isNaN(step)) return null;
21
+ else return step;
22
+ }
23
+ set step(val) {
24
+ if (!val) this.removeAttribute('step');
25
+ else this.setAttribute('step', val);
26
+ }
27
+
28
+ update(input) {
29
+ const [startEl, endEl] = this.#inputs();
30
+ const isStart = input == startEl;
31
+
32
+ let start = startEl.valueAsDate;
33
+ let end = endEl.valueAsDate;
34
+ if (!start && !end) return;
35
+ if (!start) start = new Date(end);
36
+ else if (!end) end = new Date(start);
37
+ let startPart, endPart;
38
+ let equal = true;
39
+ for (const Part of ['FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds']) {
40
+ startPart = start[`get${Part}`]();
41
+ endPart = end[`get${Part}`]();
42
+ if (startPart > endPart && equal) {
43
+ if (isStart) {
44
+ end[`set${Part}`](startPart);
45
+ } else {
46
+ start[`set${Part}`](endPart);
47
+ }
48
+ } else if (startPart != endPart) {
49
+ equal = false;
50
+ }
51
+ }
52
+ endEl.valueAsDate = end;
53
+ startEl.valueAsDate = start;
54
+ }
55
+ #inputs() {
56
+ return Array.from(this.querySelectorAll('input'));
57
+ }
58
+
59
+ patch(state) {
60
+ const [ start, end ] = this.#inputs();
61
+ const type = this.type;
62
+ let step = this.step;
63
+ if (step) {
64
+ if (type == "date") step = Math.round(step / 86400);
65
+ start.setAttribute('step', step);
66
+ end.setAttribute('step', step);
67
+ } else {
68
+ start.removeAttribute('step');
69
+ end.removeAttribute('step');
70
+ }
71
+ start.type = end.type = type;
72
+ }
73
+ }
74
+
75
+ VirtualHTMLElement.define('element-input-date-slot', HTMLElementInputDateSlot);
76
+
@@ -0,0 +1,69 @@
1
+ class HTMLElementInputDate extends HTMLInputElement {
2
+ constructor() {
3
+ super();
4
+ if (this.init) this.init();
5
+ }
6
+
7
+ setAttribute(name, value) {
8
+ if (name == "value") {
9
+ this.value = value;
10
+ value = super.value;
11
+ }
12
+ super.setAttribute(name, value);
13
+ }
14
+
15
+ get valueAsDate() {
16
+ let str = super.value;
17
+ if (!str) return null;
18
+ if (this.type == "time") {
19
+ str = `1970-01-01T${str}`;
20
+ }
21
+ const d = new Date(str + 'Z');
22
+ const t = d.getTime();
23
+ if (Number.isNaN(t)) return null;
24
+ const tz = d.getTimezoneOffset();
25
+ d.setTime(t + tz * 60 * 1000);
26
+ return d;
27
+ }
28
+ set valueAsDate(d) {
29
+ let t = d.getTime();
30
+ if (!d || Number.isNaN(t)) {
31
+ super.value = "";
32
+ return;
33
+ }
34
+ d = new Date(t);
35
+ const step = this.step * 1000;
36
+ if (step) {
37
+ t = Math.round(t / step) * step;
38
+ d.setTime(t);
39
+ }
40
+ const tz = d.getTimezoneOffset();
41
+ d.setTime(t - tz * 60 * 1000);
42
+ const str = d.toISOString().replace(/Z$/, '');
43
+ if (this.type == "time") {
44
+ super.value = str.split('T')[1];
45
+ } else if (this.type == "date") {
46
+ super.value = str.split('T')[0];
47
+ } else {
48
+ super.value = str;
49
+ }
50
+ }
51
+
52
+ get value() {
53
+ return this.valueAsDate?.toISOString();
54
+ }
55
+ set value(str) {
56
+ this.valueAsDate = new Date(str);
57
+ }
58
+
59
+ get type() {
60
+ return super.type;
61
+ }
62
+ set type(t) {
63
+ const str = super.value;
64
+ super.type = t;
65
+ this.value = str;
66
+ }
67
+ }
68
+
69
+ VirtualHTMLElement.define('element-input-date', HTMLElementInputDate, 'input');
package/ui/input-file.css CHANGED
@@ -1,47 +1,58 @@
1
- element-input-file > input[type="text"] + input[type="file"] {
2
- position:absolute !important;
3
- left:0 !important;
4
- top:0 !important;
5
- width:100% !important;
6
- height:100% !important;
7
- opacity:0 !important;
8
- padding:0 !important;
9
- margin:0 !important;
10
- }
11
- element-input-file {
12
- display:block;
1
+ [block-type="input_file"] {
13
2
  position:relative;
14
3
  }
15
- element-input-file > .ui.icon.button {
16
- z-index: 1;
17
- }
18
- element-input-file i.icon.upload::before {
19
- content:'⬆';
20
- }
21
- element-input-file > input[type="file"] + .ui.icon.button {
4
+ [block-type="input_file"] > .ui.label {
22
5
  display:none;
6
+ position: absolute;
7
+ right:-0.25em;
8
+ top:0.25em;
23
9
  }
24
- element-input-file > input[type="text"] + input[type="file"] + .ui.icon.button {
10
+ .loading[block-type="input_file"] > .ui.label {
25
11
  display:block;
26
12
  }
27
- element-input-file .button > .upload {
28
- pointer-events:none;
29
- }
30
- element-input-file input[type="text"]:not([value]) ~ .button > .delete,
31
- element-input-file input[type="text"][value=""] ~ .button > .delete,
32
- element-input-file input[type="text"][value]:not([value=""]) ~ .button > .upload,
33
- .loading.field element-input-file .button > .upload,
34
- .success.field element-input-file .button > .upload {
35
- display:none;
36
- }
37
- element-input-file input[type="text"][value]:not([value=""]) ~ .button > .delete,
38
- .loading.field element-input-file .button > .delete,
39
- .success.field element-input-file .button > .delete {
40
- display:inline-block;
41
- }
42
- element-input-file > .ui.label {
43
- display:none;
44
- }
45
- .loading.field element-input-file > .ui.label {
13
+ .form .field > input[type="file"],
14
+ .form .field > input[type="file"]:focus,
15
+ .form .field > input[type="file"]:hover {
16
+ position:relative;
17
+ color: rgba(0 0 0 / 0%);
18
+ }
19
+ .form .field > input[type="file"][filename]::after {
20
+ content:attr(filename);
21
+ color: black;
22
+ text-align: left;
23
+ position: absolute;
24
+ left: 0;
25
+ right: 0;
26
+ padding-left: inherit;
27
+ padding-right: inherit;
28
+ }
29
+ .form .field.error > input[type="file"][filename]::after {
30
+ color:inherit;
31
+ }
32
+ .form .field > input[type="file"]::after,
33
+ .form .field > input[type="file"]:not([filename])::after {
34
+ content:attr(placeholder);
35
+ color: rgba(0 0 0 / 50%);
36
+ float: left;
37
+ }
38
+ input[type="file"]::file-selector-button {
46
39
  display:block;
40
+ position:absolute;
41
+ top:0;
42
+ right:0;
43
+ margin:0;
44
+ border-width: 0;
45
+ font-size: 0;
46
+ width:2rem;
47
+ height:100%;
48
+ background-image: url('data:image/svg+xml,<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M28 1.6C29.1.5 30.6-.1 32.2 0c1.5-.095 3 .48 4.1 1.6l26 26a5.6 5.6 0 1 1-7.8 7.9l-17-17v40a5.37 5.37 0 0 1-5.4 5.5h-.048c-3.1-.048-5.5-2.5-5.5-5.5v-40l-17 17a5.6 5.6 0 1 1-7.8-7.9z"/></svg>');
49
+ background-size: 1rem;
50
+ background-repeat: no-repeat;
51
+ background-position: center;
52
+ background-color:inherit;
53
+ z-index: 1;
54
+ }
55
+
56
+ input[type="file"][filename]::file-selector-button {
57
+ background-image: url('data:image/svg+xml,<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><path d="m40.9 31.9 21-21a6.4 6.4 0 1 0-9-9l-21 21-21-21a6.4 6.4 0 1 0-9 9l21 21-21 21a6.4 6.4 0 0 0 0 9C3 63 4.6 63.8 6.2 63.8s3.3-.7 4.5-2l21-21 21 21c1.2 1.3 2.8 2 4.5 2s3.2-.7 4.5-2a6.4 6.4 0 0 0 0-9z"/></svg>');
47
58
  }
package/ui/input-file.js CHANGED
@@ -1,73 +1,88 @@
1
- class HTMLElementInputFile extends VirtualHTMLElement {
2
- #xhr
3
- #promise
1
+ class HTMLElementInputFile extends HTMLInputElement {
2
+ #xhr;
3
+ #promise;
4
+ #value;
5
+
6
+ constructor() {
7
+ super();
8
+ if (this.init) this.init();
9
+ this.save();
10
+ }
11
+
12
+ get value() {
13
+ return this.getAttribute('value');
14
+ }
15
+ save() {
16
+ this.#value = this.value;
17
+ }
18
+ reset() {
19
+ if (this.#value != null) this.setAttribute('value', this.#value);
20
+ else this.removeAttribute('value');
21
+ this.value = this.#value;
22
+ }
23
+ set value(str) {
24
+ if (str != null) {
25
+ this.setAttribute('filename', str.split(/\/|\\/).pop());
26
+ } else {
27
+ this.removeAttribute('filename');
28
+ super.value = "";
29
+ }
30
+ }
4
31
  captureClick(e, state) {
5
- const input = this.querySelector('input[type="text"]');
6
- if (!input) return;
7
- if (input.value) {
32
+ if (super.value) {
8
33
  e.preventDefault();
9
34
  if (this.#xhr) {
10
35
  this.#xhr.abort();
11
36
  this.#xhr = null;
12
37
  }
13
- input.value = '';
14
- const file = this.querySelector('input[type="file"]');
15
- file.reset();
16
- this.closest('.field').classList.remove('filled', 'loading', 'error', 'success');
38
+ this.value = null;
39
+ this.closest('.field').classList.remove('loading', 'error', 'success');
17
40
  } else {
18
41
  // ok
19
42
  }
20
43
  }
21
44
 
22
45
  handleChange(e, state) {
23
- const input = this.querySelector('input[type="text"]');
24
- if (!input) return;
25
- if (e.target.type == "file" && e.target.value) {
26
- input.value = (e.target.value || "").split(/\/|\\/).pop();
46
+ if (super.value) {
47
+ this.value = super.value;
48
+ } else {
49
+ this.value = null;
27
50
  }
28
- if (this.dataset.now != null) this.upload();
29
51
  }
30
52
 
31
- upload() {
53
+ presubmit() {
32
54
  if (this.#promise) return this.#promise;
33
- const file = this.querySelector('input[type="file"]');
34
- const input = this.querySelector('input[type="text"]');
35
- if (!input || !file) throw new Error("Unitialized input-file");
36
- if (!file.files.length) return Promise.resolve();
37
-
55
+ if (!this.files.length) return Promise.resolve();
38
56
  const field = this.closest('.field');
39
57
  field.classList.remove('success', 'error');
40
- const label = this.querySelector('.label');
58
+ const label = field.querySelector('.label');
41
59
  function track(num) {
42
60
  label.innerText = num;
43
61
  }
44
62
  track(0);
45
63
  field.classList.add('loading');
46
64
  const p = new Promise((resolve, reject) => {
47
- const me = this;
48
- function fail(err) {
65
+ const fail = (err) => {
49
66
  field.classList.add('error');
50
67
  field.classList.remove('loading');
51
- me.#xhr = null;
68
+ this.#xhr = null;
52
69
  reject(err);
53
- me.#promise = null;
54
- }
55
- function pass(obj) {
70
+ this.#promise = null;
71
+ };
72
+ const pass = (obj) => {
56
73
  if (!obj.items || obj.items.length == 0) return fail(new Error("File rejected"));
57
74
  const val = obj.items[0];
58
- input.value = val;
75
+ this.setAttribute('value', val);
59
76
  field.classList.add('success');
60
77
  field.classList.remove('loading');
61
- me.#xhr = null;
78
+ this.#xhr = null;
62
79
  resolve();
63
- me.#promise = null;
64
- }
65
- if (file.files.length == 0) return resolve(); // or reject ?
80
+ this.#promise = null;
81
+ };
82
+ if (this.files.length == 0) return resolve(); // or reject ?
66
83
 
67
84
  const fd = new FormData();
68
- for (let i = 0; i < file.files.length; i++) {
69
- fd.append("files", file.files[i]);
70
- }
85
+ fd.append("files", this.files[0]);
71
86
 
72
87
  const xhr = new XMLHttpRequest();
73
88
 
@@ -96,7 +111,7 @@ class HTMLElementInputFile extends VirtualHTMLElement {
96
111
  fail(err);
97
112
  });
98
113
  try {
99
- xhr.open("POST", `/.api/upload/${file.id}`, true);
114
+ xhr.open("POST", `/.api/upload/${this.id}`, true);
100
115
  xhr.setRequestHeader('Accept', "application/json; q=1.0");
101
116
  xhr.send(fd);
102
117
  this.#xhr = xhr;
@@ -109,6 +124,5 @@ class HTMLElementInputFile extends VirtualHTMLElement {
109
124
  }
110
125
  }
111
126
 
112
- Page.setup(() => {
113
- VirtualHTMLElement.define('element-input-file', HTMLElementInputFile);
114
- });
127
+ VirtualHTMLElement.define('element-input-file', HTMLElementInputFile, 'input');
128
+
package/ui/pagination.js CHANGED
@@ -1,10 +1,10 @@
1
1
  class HTMLElementPagination extends HTMLAnchorElement {
2
- #observer
3
- #queue
4
- #reached
5
- #size
6
- #visible
7
- #continue
2
+ #observer;
3
+ #queue;
4
+ #reached;
5
+ #size;
6
+ #visible;
7
+ #continue;
8
8
 
9
9
  constructor() {
10
10
  super();
package/ui/select.js CHANGED
@@ -1,5 +1,5 @@
1
1
  class HTMLElementSelect extends VirtualHTMLElement {
2
- #observer
2
+ #observer;
3
3
 
4
4
  static defaults = {
5
5
  placeholder: null,
@@ -100,11 +100,15 @@ class HTMLElementSelect extends VirtualHTMLElement {
100
100
  }
101
101
  #setPlaceholder(str) {
102
102
  const text = this.#text;
103
- text.textContent = str || this.options.placeholder;
104
- text.classList.add('default');
103
+ if (!str) str = this.options.placeholder;
105
104
 
106
105
  const defaultOption = this.#select.querySelector('option[value=""]');
107
- if (defaultOption) defaultOption.innerHTML = str || "-";
106
+ if (defaultOption) {
107
+ if (!str) str = defaultOption.innerHTML;
108
+ else defaultOption.innerHTML = str;
109
+ }
110
+ text.textContent = str;
111
+ text.classList.add('default');
108
112
  }
109
113
 
110
114
  #menuOption(val) {
@@ -124,11 +128,13 @@ class HTMLElementSelect extends VirtualHTMLElement {
124
128
  const select = this.#select;
125
129
  if (!select) return;
126
130
  const menu = this.#menu;
127
- select.innerHTML = '<option selected value="">-</option>';
128
- menu.children.forEach(item => select.insertAdjacentHTML(
129
- 'beforeEnd',
130
- `<option value="${item.dataset.value || item.innerText.trim()}">${item.innerHTML}</option>`
131
- ));
131
+ menu.children.forEach(item => {
132
+ const val = item.dataset.value;
133
+ select.insertAdjacentHTML(
134
+ 'beforeEnd',
135
+ `<option value="${val == null ? '' : val}">${item.innerHTML}</option>`
136
+ );
137
+ });
132
138
  }
133
139
  setup(state) {
134
140
  this.#observer = new MutationObserver((mutations) => this.#fillSelect());
@@ -154,6 +160,12 @@ class HTMLElementSelect extends VirtualHTMLElement {
154
160
  for (const node of this.querySelectorAll('.ui.label')) node.remove();
155
161
  }
156
162
  select.name = this.options.name;
163
+ if (!select.required) {
164
+ const menu = this.#menu;
165
+ if (!menu.querySelector('element-select-option[data-value=""]')) {
166
+ menu.insertAdjacentHTML('afterBegin', `<element-select-option data-value="" block-type="input_select_option" class="item">-</element-select-option>`);
167
+ }
168
+ }
157
169
  this.#fillSelect();
158
170
  }
159
171
 
package/ui/sticky.js CHANGED
@@ -3,7 +3,7 @@ class HTMLElementStickyNav extends HTMLElement {
3
3
  super();
4
4
  if (this.init) this.init();
5
5
  }
6
- #lastScroll
6
+ #lastScroll;
7
7
  #currentScroll() {
8
8
  return document.documentElement.scrollTop;
9
9
  }