@pageboard/html 0.10.10 → 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',
@@ -70,7 +70,7 @@ exports.input_property = {
70
70
  if (prop.select && prop.select.$data == `0/${propKey}`) {
71
71
  cases = prop.selectCases;
72
72
  }
73
- prop = (prop.properties || {})[propKey] || null;
73
+ prop = (prop.items && prop.items.properties || prop.properties || {})[propKey] || null;
74
74
  }
75
75
  if (prop == null) break;
76
76
  }
@@ -227,37 +227,13 @@ exports.input_property = {
227
227
  label: prop.title
228
228
  }
229
229
  }));
230
- } else if (propType.type == "string" && propType.format == "date") {
231
- let type = "input_date_time";
232
- if (!scope.$elements[type]) {
233
- type = 'input_text';
234
- }
235
- node.appendChild(view.render({
236
- id,
237
- type: type,
238
- data: {
239
- name: name,
240
- type: propType.format,
241
- default: propType.default,
242
- disabled: d.disabled,
243
- required: required,
244
- step: propType.step
245
- },
246
- content: {
247
- label: prop.title
248
- }
249
- }));
250
- } else if (propType.type == "string" && propType.format == "time") {
251
- let type = "input_date_time";
252
- if (!scope.$elements[type]) {
253
- type = 'input_text';
254
- }
230
+ } else if (propType.type == "string" && ["date", "time", "date-time"].includes(propType.format)) {
255
231
  node.appendChild(view.render({
256
232
  id,
257
- type: type,
233
+ type: 'input_date_time',
258
234
  data: {
259
235
  name: name,
260
- type: propType.format,
236
+ format: propType.format.replace('-', ''),
261
237
  default: propType.default,
262
238
  disabled: d.disabled,
263
239
  required: required,
@@ -294,16 +270,15 @@ exports.input_property = {
294
270
  }
295
271
  }));
296
272
  } else {
273
+ const type = (propType.format || propType.pattern) ? 'text' : 'textarea';
297
274
  node.appendChild(view.render({
298
275
  id,
299
276
  type: 'input_text',
300
277
  data: {
301
- name: name,
302
- type: (propType.pattern || propType.format) ? 'text' : 'textarea',
278
+ name, type, required,
303
279
  disabled: d.disabled,
304
280
  default: propType.default,
305
- placeholder: propType.description,
306
- required: required
281
+ placeholder: propType.description
307
282
  },
308
283
  content: {
309
284
  label: prop.title
@@ -1,46 +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
- 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
1
  exports.input_button = {
45
2
  title: 'Button',
46
3
  icon: '<i class="hand pointer icon"></i>',
@@ -502,36 +459,4 @@ exports.input_select_option = {
502
459
  ></element-select-option>`
503
460
  };
504
461
 
505
- exports.fieldset_list = {
506
- title: 'FieldList',
507
- menu: "form",
508
- icon: '<i class="icons"><i class="folder outline icon"></i><i class="corner add icon"></i></i>',
509
- group: "block",
510
- context: 'form//',
511
- priority: 0,
512
- properties: {
513
- size: {
514
- title: 'Minimum size',
515
- type: "integer",
516
- minimum: 0,
517
- default: 1
518
- },
519
- prefix: {
520
- title: 'Prefix',
521
- description: '',
522
- type: "string",
523
- format: 'singleline',
524
- nullable: true
525
- }
526
- },
527
- contents: [{
528
- id: 'template',
529
- nodes: 'block+'
530
- }],
531
- html: `<element-fieldset-list data-size="[size]" data-prefix="[prefix]">
532
- <template block-content="template"></template>
533
- <div class="view"></div>
534
- </element-fieldset-list>`,
535
- scripts: ['../ui/fieldset-list.js'],
536
- stylesheets: ['../ui/fieldset-list.css']
537
- };
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.10",
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/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,
@@ -129,10 +129,10 @@ class HTMLElementSelect extends VirtualHTMLElement {
129
129
  if (!select) return;
130
130
  const menu = this.#menu;
131
131
  menu.children.forEach(item => {
132
- const val = item.dataset.value || item.innerText != "-" && item.innerText.trim() || "";
132
+ const val = item.dataset.value;
133
133
  select.insertAdjacentHTML(
134
134
  'beforeEnd',
135
- `<option value="${val}">${item.innerHTML}</option>`
135
+ `<option value="${val == null ? '' : val}">${item.innerHTML}</option>`
136
136
  );
137
137
  });
138
138
  }
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
  }