@pageboard/html 0.14.9 → 0.14.10

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.
@@ -1,72 +1,44 @@
1
- class WalkIndex {
2
- #walk;
3
- #find;
4
- #index;
5
- constructor(root, fn) {
6
- this.#find = fn;
7
- this.#walk = root.ownerDocument.createTreeWalker(
8
- root,
9
- NodeFilter.SHOW_ELEMENT,
10
- this
11
- );
12
- }
13
- acceptNode(node) {
14
- const index = this.#find(node);
15
- if (index != null) {
16
- this.#index = index;
17
- return NodeFilter.FILTER_ACCEPT;
18
- } else {
19
- return NodeFilter.FILTER_SKIP;
20
- }
21
- }
22
- findBefore(node) {
23
- this.#index = null;
24
- this.#walk.currentNode = node;
25
- this.#walk.previousNode();
26
- return this.#index;
27
- }
28
- }
29
-
30
1
  class HTMLElementFieldsetList extends Page.Element {
31
- #size;
32
- #initialSize;
2
+ #list;
3
+ #defaultList;
33
4
  #prefix;
34
5
  #model;
35
- #walk;
36
6
 
37
- fill(values, scope) {
38
- if (scope.$write || this.prefix == null) return;
39
- // unflatten array-values
40
- const vars = [];
7
+ fill(values) {
8
+ if (this.isContentEditable || this.prefix == null) return;
41
9
  for (const [key, val] of Object.entries(values)) {
42
- if (!this.#prefixed(key)) continue;
43
- vars.push(key);
44
- if (Array.isArray(val)) {
10
+ const parts = this.#prefixed(key);
11
+ if (!parts) continue;
12
+ if (parts.length == 1 && Number.isInteger(Number(parts[0])) && Array.isArray(val)) {
13
+ console.warn("fielset-list should receive flat lists", key, val);
14
+ } else if (parts.length == 0 && Array.isArray(val)) {
45
15
  for (let i = 0; i < val.length; i++) {
46
16
  values[key + '.' + i] = val[i];
47
17
  }
48
18
  delete values[key];
49
19
  }
50
20
  }
51
- const list = this.#listFromValues({ ...values });
52
- if (this.#initialSize == null) this.#initialSize = list.length;
53
- this.#resize(list.length, scope);
54
- return vars;
21
+ this.#list = this.#listFromValues({ ...values });
22
+ if (this.#defaultList == null) this.save();
23
+ this.#resize();
55
24
  }
56
25
 
57
26
  reset() {
58
- this.#resize(this.#initialSize, {}); // missing scope
27
+ this.#list = this.#defaultList.slice();
28
+ this.#refresh();
59
29
  }
60
30
 
61
31
  save() {
62
- this.#initialSize = this.#size;
32
+ this.#defaultList = this.#list.slice();
63
33
  }
64
34
 
65
35
  #modelize(tpl) {
66
36
  const keys = new Set();
67
37
  const inputs = tpl.querySelectorAll('[name]');
68
38
  for (const node of inputs) {
69
- keys.add(node.name);
39
+ // because custom elements are not started in templates, do not use .name
40
+ const name = node.getAttribute('name');
41
+ keys.add(name);
70
42
  }
71
43
  const splits = Array.from(keys).map(name => this.#parts(name));
72
44
  const prefix = [];
@@ -90,27 +62,24 @@ class HTMLElementFieldsetList extends Page.Element {
90
62
  this.#prefix = prefix;
91
63
  const model = {};
92
64
  for (const key of keys) {
93
- if (this.#prefixed(key)) {
94
- model[this.#parts(key).slice(prefix.length).join('.')] = null;
65
+ const keyParts = this.#prefixed(key);
66
+ if (keyParts) {
67
+ model[keyParts.join('.')] = undefined;
95
68
  }
96
69
  }
97
70
  this.#model = model;
98
71
  }
99
72
 
100
- #prepare() {
73
+ prepare(editable) {
101
74
  const tpl = this.ownTpl;
102
- tpl.prerender();
103
- this.#modelize(tpl.content);
104
- for (const node of tpl.content.querySelectorAll('[block-id]')) {
105
- node.removeAttribute('block-id');
106
- }
107
- }
108
-
109
- patch({ scope }) {
110
- if (scope.$write) {
111
- this.#modelize(this.ownTpl);
112
- } else if (!this.#size) {
113
- this.#resize(0, scope);
75
+ if (editable) {
76
+ this.#modelize(tpl);
77
+ } else {
78
+ tpl.prerender();
79
+ this.#modelize(tpl.content);
80
+ for (const node of tpl.content.querySelectorAll('[block-id]')) {
81
+ node.removeAttribute('block-id');
82
+ }
114
83
  }
115
84
  }
116
85
 
@@ -123,82 +92,97 @@ class HTMLElementFieldsetList extends Page.Element {
123
92
  for (let i = 0; i < p.length; i++) {
124
93
  if (parts[i] != p[i]) return false;
125
94
  }
126
- return true;
95
+ return parts.slice(p.length);
127
96
  }
128
97
 
129
98
  #incrementkey(index, name) {
130
- if (!this.#prefixed(name)) return null;
99
+ const nameParts = this.#prefixed(name);
100
+ if (!nameParts) return null;
131
101
  const parts = this.#prefix.slice();
132
102
  parts.push(index);
133
- parts.push(...this.#parts(name).slice(this.#prefix.length));
103
+ parts.push(...nameParts);
134
104
  return parts.join('.');
135
105
  }
136
106
 
137
- #resize(size, scope) {
138
- if (scope.$write) return;
139
- const len = Math.max(Number(this.dataset.size) || 0, size);
140
- if (this.#size === len) return;
141
- this.#size = len;
107
+ #resize() {
142
108
  let tpl = this.ownTpl.content.cloneNode(true);
143
- const fieldlist = Array.from(Array(len)).map((x, i) => {
144
- return { index: i };
145
- });
109
+
146
110
  const inputs = tpl.querySelectorAll('[name]');
147
111
  for (const node of inputs) {
148
- const name = this.#incrementkey('[fielditem.index]', node.name);
149
- if (name != null) {
112
+ const name = node.getAttribute('name');
113
+ const iname = this.#incrementkey('[field.$i]', name);
114
+ if (iname != null) {
150
115
  const id = node.id;
151
- if (id?.startsWith(`for-${node.name}`)) {
152
- node.id = id.replace(node.name, name);
116
+ if (id?.startsWith(`for-${name}`)) {
117
+ node.id = id.replace(name, iname);
153
118
  }
154
119
  if (node.nextElementSibling?.htmlFor == id) {
155
120
  node.nextElementSibling.htmlFor = node.id;
156
121
  }
157
- node.name = name;
122
+ node.setAttribute('name', iname);
158
123
  }
159
124
  }
125
+
160
126
  const conditionalFieldsets = tpl.querySelectorAll('[is="element-fieldset"]');
161
127
  for (const node of conditionalFieldsets) {
162
- const name = this.#incrementkey('[fielditem.index]', node.dataset.name);
128
+ const name = this.#incrementkey('[field.$i]', node.dataset.name);
163
129
  if (name != null) {
164
130
  node.dataset.name = name;
165
131
  }
166
132
  }
167
133
 
168
134
  const subtpl = inputs.map(node => node.closest('.fields') ?? node).ancestor();
169
- if (!subtpl) {
170
- console.warn("fieldset-list should contain input[name]", this);
171
- return;
172
- }
173
135
  subtpl.appendChild(
174
- subtpl.ownerDocument.createTextNode('[fieldlist|at:*|repeat:fielditem|]')
136
+ subtpl.ownerDocument.createTextNode(
137
+ `[$fields|at:${this.dataset.at || '*'}|repeat:field|const:]`
138
+ )
175
139
  );
176
- if (len == 0) {
177
- let node = tpl.querySelector(this.#selector('add'));
178
- while (node != null && node != tpl && node != subtpl) {
179
- while (node.nextSibling) node.nextSibling.remove();
180
- while (node.previousSibling) node.previousSibling.remove();
181
- node = node.parentNode;
182
- }
183
- {
184
- const hidden = tpl.ownerDocument.createElement('input');
185
- hidden.type = "hidden";
186
- hidden.name = this.#prefix.join('.');
187
- tpl.appendChild(hidden);
188
- }
140
+ const min = Number(this.dataset.min) || 0;
141
+ const max = Number(this.dataset.max) || Infinity;
142
+ let list = this.#list;
143
+ const placeholder = list.length == 0 && min == 0;
144
+ if (list.length == 0) {
145
+ list = [{...this.#model, $i: min == 0 ? -1 : 0}];
189
146
  }
190
- tpl = tpl.fuse({ fieldlist }, scope);
147
+ tpl = tpl.fuse({
148
+ $fields: list,
149
+ $pathname: Page.pathname,
150
+ $query: Page.query
151
+ }, {
152
+ $hooks: {
153
+ before: {
154
+ get(ctx, val, args) {
155
+ const path = args[0];
156
+ if (path[0] == "field") {
157
+ args[0] = [path[0], path.slice(1).join('.')];
158
+ }
159
+ }
160
+ }
161
+ }
162
+ });
191
163
 
192
164
  const view = this.ownView;
193
165
  view.textContent = '';
194
166
  view.appendChild(tpl);
195
167
 
168
+ if (placeholder) {
169
+ for (const node of view.querySelectorAll(`[name^="${this.#prefixStr}"]`)) {
170
+ node.disabled = true;
171
+ }
172
+ }
173
+
196
174
  view.querySelectorAll(this.#selector('up')).forEach((node, i) => {
197
175
  node.disabled = i == 0;
198
176
  });
199
177
  view.querySelectorAll(this.#selector('down')).forEach((node, i, arr) => {
200
178
  node.disabled = i == arr.length - 1;
201
179
  });
180
+ view.querySelectorAll(this.#selector('del')).forEach((node) => {
181
+ node.disabled = this.#list.length <= min;
182
+ });
183
+ view.querySelectorAll(this.#selector('add')).forEach((node) => {
184
+ node.disabled = this.#list.length >= max;
185
+ });
202
186
  }
203
187
 
204
188
  #parts(key) {
@@ -208,14 +192,15 @@ class HTMLElementFieldsetList extends Page.Element {
208
192
  #listFromValues(values) {
209
193
  const list = [];
210
194
  for (const [key, val] of Object.entries(values)) {
211
- if (!this.#prefixed(key)) continue;
212
- const parts = this.#parts(key).slice(this.#prefix.length);
195
+ const parts = this.#prefixed(key);
196
+ if (!parts) continue;
213
197
  const index = Number(parts.shift());
214
198
  if (!Number.isInteger(index)) continue;
215
199
  delete values[key];
216
200
  let obj = list[index];
217
201
  if (!obj) list[index] = obj = {};
218
202
  obj[parts.join('.')] = val;
203
+ obj.$i = index;
219
204
  }
220
205
  return list;
221
206
  }
@@ -224,6 +209,7 @@ class HTMLElementFieldsetList extends Page.Element {
224
209
  for (let i = 0; i < list.length; i++) {
225
210
  const obj = list[i] ?? {};
226
211
  for (const [key, val] of Object.entries(obj)) {
212
+ if (key == "$i") continue;
227
213
  const parts = this.#prefix.slice();
228
214
  parts.push(i);
229
215
  parts.push(...this.#parts(key));
@@ -232,33 +218,32 @@ class HTMLElementFieldsetList extends Page.Element {
232
218
  }
233
219
  }
234
220
 
221
+ #findIndex(btn) {
222
+ let node = btn;
223
+ const sel = `[name^="${this.#prefixStr}"]`;
224
+ while ((node = node.parentNode)) {
225
+ const input = Array.from(node.querySelectorAll(sel)).pop();
226
+ if (!input) continue;
227
+ const { index } = this.#parseName(input.name);
228
+ if (index >= 0 && index < this.#list.length) return index;
229
+ }
230
+ }
231
+
235
232
  handleClick(e, state) {
236
233
  if (state.scope.$write) return;
237
234
  const btn = e.target.closest('button');
238
235
  if (!btn) return;
239
236
  const action = btn.value;
240
237
  if (["add", "del", "up", "down"].includes(action) == false) return;
241
-
242
- const form = this.closest('form');
243
- const values = form.read(true);
244
- const list = this.#listFromValues(values);
245
-
246
- if (!this.#walk) this.#walk = new WalkIndex(this, node => {
247
- const { index } = this.#parseName(node.name);
248
- if (index >= 0 && index < list.length) return index;
249
- else return null;
250
- });
238
+ const list = this.#listFromValues(this.closest('form').read(true));
251
239
  let index;
252
240
 
253
- const fileInputs = this.querySelectorAll('[name][type="file"]')
254
- .map(n => n.cloneNode(true));
255
-
256
241
  switch (action) {
257
242
  case "add":
258
- list.splice((this.#walk.findBefore(btn) ?? -1) + 1, 0, this.#model);
243
+ list.splice((this.#findIndex(btn) ?? 0), 0, { ...this.#model });
259
244
  break;
260
245
  case "del":
261
- list.splice(this.#walk.findBefore(btn) ?? 0, 1);
246
+ list.splice(this.#findIndex(btn) ?? 0, 1);
262
247
  break;
263
248
  case "up":
264
249
  index = this.querySelectorAll(this.#selector('up')).indexOf(btn);
@@ -273,8 +258,22 @@ class HTMLElementFieldsetList extends Page.Element {
273
258
  }
274
259
  break;
275
260
  }
276
- this.#listToValues(values, list);
277
- form.fill(values, state.scope);
261
+ this.#list = list;
262
+ this.#refresh();
263
+ state.dispatch(this, 'change');
264
+ }
265
+
266
+ #refresh() {
267
+ const form = this.closest('form');
268
+ const values = form.read(true);
269
+ for (const key of Object.keys(values)) {
270
+ if (this.#prefixed(key)) delete values[key];
271
+ }
272
+ const fileInputs = this.querySelectorAll('[name][type="file"]')
273
+ .map(n => n.cloneNode(true));
274
+ this.#listToValues(values, this.#list);
275
+ form.fill(values);
276
+
278
277
  const liveFileInputs = this.querySelectorAll('[name][type="file"]');
279
278
  for (const node of fileInputs) {
280
279
  const { value } = node;
@@ -286,14 +285,13 @@ class HTMLElementFieldsetList extends Page.Element {
286
285
  live.replaceWith(node);
287
286
  }
288
287
  }
289
- state.dispatch(this, 'change');
290
288
  }
291
289
 
292
290
  #parseName(name) {
293
- if (!this.#prefixed(name)) {
291
+ const parts = this.#prefixed(name);
292
+ if (!parts) {
294
293
  return { index: -1 };
295
294
  }
296
- const parts = this.#parts(name).slice(this.#prefix.length);
297
295
  const index = Number(parts.shift());
298
296
  if (!Number.isInteger(index)) return { index: -1 };
299
297
  return { index, sub: parts.join('.') };
@@ -308,11 +306,11 @@ class HTMLElementFieldsetList extends Page.Element {
308
306
  return this.children.find(node => node.matches('.view'));
309
307
  }
310
308
  get prefix() {
311
- if (this.#prefix == null) {
312
- this.#prepare();
313
- }
314
309
  return this.#prefix;
315
310
  }
311
+ get #prefixStr() {
312
+ return this.#prefix.length ? this.#prefix.join('.') + '.' : '';
313
+ }
316
314
  }
317
315
 
318
316
  Page.define('element-fieldset-list', HTMLElementFieldsetList);
package/ui/fieldset.js CHANGED
@@ -4,8 +4,8 @@ class HTMLElementFieldSet extends Page.create(HTMLFieldSetElement) {
4
4
  dataValue: null
5
5
  };
6
6
 
7
- fill(query, scope) {
8
- if (scope.$write || !this.options?.name || !this.form) return;
7
+ fill(query) {
8
+ if (this.isContentEditable || !this.options?.name || !this.form) return;
9
9
  if (!query) query = this.form.read(true);
10
10
  const val = query[this.options.name];
11
11
  const disabled = this.disabled = this.hidden = val != this.options.value;
@@ -16,12 +16,12 @@ class HTMLElementFieldSet extends Page.create(HTMLFieldSetElement) {
16
16
 
17
17
  patch(state) {
18
18
  // before/after form#fill
19
- this.fill(null, state.scope);
20
- state.finish(() => this.fill(null, state.scope));
19
+ this.fill(null);
20
+ state.finish(() => this.fill(null));
21
21
  }
22
22
  handleAllChange(e, state) {
23
23
  if (this.form?.contains(e.target)) {
24
- this.fill(null, state.scope);
24
+ this.fill(null);
25
25
  }
26
26
  }
27
27
  }
package/ui/form.css CHANGED
@@ -15,8 +15,11 @@ label[for] {
15
15
  user-select: none;
16
16
  }
17
17
 
18
- element-select {
19
- z-index:1; /* or else dropdown menu goes under what's next */
18
+ element-select.active {
19
+ z-index:1;
20
+ }
21
+ element-select.active.ui.dropdown .menu {
22
+ display:block;
20
23
  }
21
24
 
22
25
  .inline.fields element-select {
package/ui/form.js CHANGED
@@ -122,7 +122,7 @@ class HTMLElementForm extends Page.create(HTMLFormElement) {
122
122
  }
123
123
  this.restore(state.scope);
124
124
  } else {
125
- for (const name of this.fill(state.query, state.scope)) {
125
+ for (const name of this.fill(state.query)) {
126
126
  state.vars[name] = true;
127
127
  }
128
128
  }
@@ -224,11 +224,11 @@ class HTMLElementForm extends Page.create(HTMLFormElement) {
224
224
  }
225
225
  return query;
226
226
  }
227
- fill(query, scope) {
227
+ fill(query) {
228
228
  // fieldset-list are not custom inputs yet
229
229
  const vars = [];
230
230
  for (const node of this.querySelectorAll("element-fieldset-list")) {
231
- if (node.fill) vars.push(...node.fill(query, scope));
231
+ node?.fill(query);
232
232
  }
233
233
 
234
234
  for (const elem of this.elements) {
@@ -242,8 +242,11 @@ class HTMLElementForm extends Page.create(HTMLFormElement) {
242
242
  elem.fill?.(val);
243
243
  }
244
244
  }
245
+ for (const node of this.querySelectorAll("element-select")) {
246
+ node.fill?.(query);
247
+ }
245
248
  for (const node of this.querySelectorAll('fieldset[is="element-fieldset"]')) {
246
- node.fill?.(query, scope);
249
+ node.fill?.(query);
247
250
  }
248
251
  return vars;
249
252
  }
@@ -253,17 +256,17 @@ class HTMLElementForm extends Page.create(HTMLFormElement) {
253
256
  node.save();
254
257
  }
255
258
  for (const node of this.elements) {
256
- if (node.save) node.save();
259
+ node.save?.();
257
260
  }
258
261
  }
259
262
  reset() {
260
263
  this.classList.remove('unsaved');
264
+ for (const node of this.elements) {
265
+ node.reset?.();
266
+ }
261
267
  for (const node of this.querySelectorAll("element-fieldset-list")) {
262
268
  node.reset();
263
269
  }
264
- for (const node of this.elements) {
265
- if (node.reset) node.reset();
266
- }
267
270
  }
268
271
  backup() {
269
272
  if (!this.action) return;
@@ -289,6 +292,7 @@ class HTMLElementForm extends Page.create(HTMLFormElement) {
289
292
  window.sessionStorage.removeItem(this.action);
290
293
  }
291
294
  handleReset(e, state) {
295
+ e.preventDefault();
292
296
  this.reset();
293
297
  }
294
298
  handleSubmit(e, state) {
@@ -422,18 +426,22 @@ HTMLFormElement.prototype.disable = function () {
422
426
  }
423
427
  };
424
428
 
425
-
426
-
427
-
428
429
  HTMLSelectElement.prototype.fill = function (val) {
429
430
  if (!Array.isArray(val)) val = [val];
430
431
  for (let i = 0; i < this.options.length; i++) {
431
432
  const opt = this.options[i];
432
- opt.selected = val.indexOf(opt.value) > -1;
433
+ opt.selected = val.includes(opt.value);
433
434
  }
434
435
  };
435
436
  HTMLSelectElement.prototype.reset = function () {
436
- for (let i = 0; i < this.options.length; i++) this.options[i].selected = false;
437
+ for (let i = 0; i < this.options.length; i++) {
438
+ this.options[i].selected = this.options[i].defaultSelected;
439
+ }
440
+ };
441
+ HTMLSelectElement.prototype.save = function () {
442
+ for (let i = 0; i < this.options.length; i++) {
443
+ this.options[i].defaultSelected = this.options[i].selected;
444
+ }
437
445
  };
438
446
 
439
447
  HTMLButtonElement.prototype.fill = HTMLInputElement.prototype.fill = function (val) {
@@ -465,7 +473,7 @@ HTMLButtonElement.prototype.fill = HTMLInputElement.prototype.fill = function (v
465
473
 
466
474
  HTMLInputElement.prototype.reset = function () {
467
475
  if (this.type == "radio" || this.type == "checkbox") {
468
- this.fill(this.defaultChecked);
476
+ this.fill(this.defaultChecked ? this.value : '');
469
477
  } else {
470
478
  this.fill(this.defaultValue);
471
479
  }
package/ui/image.js CHANGED
@@ -72,22 +72,6 @@ const HTMLElementImageConstructor = Superclass => class extends Superclass {
72
72
  return img;
73
73
  }
74
74
 
75
- fix(img) {
76
- if (!window.objectFitImages.supportsObjectFit) {
77
- let style = "";
78
- if (this.fit) {
79
- style += `object-fit: ${this.fit};`;
80
- }
81
- if (this.position) {
82
- const pos = this.position.replace(/(h|v)center/g, 'center');
83
- style += `object-position: ${pos};`;
84
- }
85
- if (style.length) {
86
- img.style.fontFamily = `'${style}'`;
87
- window.objectFitImages(img);
88
- }
89
- }
90
- }
91
75
  patch(state) {
92
76
  this.classList.remove('loading');
93
77
  if (this.currentSrc != this.options.src) {
@@ -171,7 +155,6 @@ const HTMLElementImageConstructor = Superclass => class extends Superclass {
171
155
  captureLoad() {
172
156
  this.#defer.resolve();
173
157
  this.classList.remove('loading');
174
- this.fix(this.image);
175
158
  }
176
159
  captureError() {
177
160
  this.#defer.reject();
package/ui/input-file.css CHANGED
@@ -29,12 +29,7 @@
29
29
  .form .field.error > input[type="file"][value]::after {
30
30
  color:inherit;
31
31
  }
32
- .form .field > input[type="file"]::after,
33
- .form .field > input[type="file"]:not([value])::after {
34
- content:attr(placeholder);
35
- color: rgba(0 0 0 / 50%);
36
- float: left;
37
- }
32
+
38
33
  input[type="file"]::file-selector-button {
39
34
  display:block;
40
35
  position:absolute;
package/ui/select.css CHANGED
@@ -5,3 +5,18 @@ element-select.ui.multiple.dropdown > .label {
5
5
  vertical-align: baseline;
6
6
  line-height: inherit;
7
7
  }
8
+
9
+ [contenteditable] .ui.dropdown[block-focused] > .menu {
10
+ overflow: visible;
11
+ width: auto;
12
+ height: auto;
13
+ top: 100% !important;
14
+ opacity: 1;
15
+ }
16
+
17
+ [contenteditable] .field[block-focused] > .ui.dropdown {
18
+ z-index:1;
19
+ }
20
+ [contenteditable] .field[block-focused] .dropdown > .menu {
21
+ display: block !important;
22
+ }