@pageboard/html 0.10.15 → 0.11.1-1

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/elements/card.js CHANGED
@@ -18,7 +18,7 @@ exports.cards = {
18
18
  shape: {
19
19
  title: 'Shape',
20
20
  anyOf: [{
21
- type: 'null',
21
+ const: null,
22
22
  title: 'Default',
23
23
  }, {
24
24
  const: "square",
@@ -33,9 +33,8 @@ exports.cards = {
33
33
  },
34
34
  responsive: {
35
35
  title: 'Responsive',
36
- nullable: true,
37
36
  anyOf: [{
38
- title: 'Disable',
37
+ title: 'No',
39
38
  const: null
40
39
  }, {
41
40
  title: 'Stackable',
@@ -160,14 +159,14 @@ exports.card_header = {
160
159
  contents: "inline*",
161
160
  html: '<div class="header">Header</div>'
162
161
  };
163
- exports.card_header_nolink = Object.assign({}, exports.card_header, {
162
+ exports.card_header_nolink = { ...exports.card_header,
164
163
  context: 'cardlink//',
165
164
  contents: {
166
165
  nodes: "inline*",
167
166
  marks: "nolink"
168
167
  },
169
168
  html: '<div class="header">Header</div>'
170
- });
169
+ };
171
170
 
172
171
  exports.card_meta = {
173
172
  title: 'meta',
@@ -178,14 +177,14 @@ exports.card_meta = {
178
177
  contents: "inline*",
179
178
  html: '<div class="meta">Meta</div>'
180
179
  };
181
- exports.card_meta_nolink = Object.assign({}, exports.card_meta, {
180
+ exports.card_meta_nolink = { ...exports.card_meta,
182
181
  context: 'cardlink//',
183
182
  contents: {
184
183
  nodes: "inline*",
185
184
  marks: "nolink"
186
185
  },
187
186
  html: '<div class="meta">Meta</div>'
188
- });
187
+ };
189
188
 
190
189
  exports.card_description = {
191
190
  title: 'description',
@@ -196,9 +195,9 @@ exports.card_description = {
196
195
  contents: "paragraph+",
197
196
  html: '<div class="description"></div>'
198
197
  };
199
- exports.card_description_nolink = Object.assign({}, exports.card_description, {
198
+ exports.card_description_nolink = { ...exports.card_description,
200
199
  context: 'cardlink//',
201
200
  contents: "paragraph_nolink+",
202
201
  html: '<div class="description"></div>'
203
- });
202
+ };
204
203
 
@@ -42,10 +42,10 @@ exports.fieldset_legend = {
42
42
  };
43
43
 
44
44
  exports.fieldset_list = {
45
- title: 'FieldList',
45
+ title: 'Field List',
46
46
  menu: "form",
47
47
  icon: '<i class="icons"><i class="folder outline icon"></i><i class="corner add icon"></i></i>',
48
- group: "block",
48
+ group: 'block template',
49
49
  context: 'form//',
50
50
  priority: 0,
51
51
  properties: {
@@ -54,23 +54,58 @@ exports.fieldset_list = {
54
54
  type: "integer",
55
55
  minimum: 0,
56
56
  default: 1
57
- },
58
- prefix: {
59
- title: 'Prefix',
60
- description: '',
61
- type: "string",
62
- format: 'singleline',
63
- nullable: true
64
57
  }
65
58
  },
66
59
  contents: [{
67
60
  id: 'template',
68
- nodes: 'block+'
61
+ nodes: 'block+',
62
+ expressions: true
69
63
  }],
70
- html: `<element-fieldset-list data-size="[size]" data-prefix="[prefix]">
64
+ html: `<element-fieldset-list data-size="[size]">
71
65
  <template block-content="template"></template>
72
66
  <div class="view"></div>
73
67
  </element-fieldset-list>`,
74
68
  scripts: ['../ui/fieldset-list.js'],
75
69
  stylesheets: ['../ui/fieldset-list.css']
76
70
  };
71
+
72
+
73
+ exports.fieldlist_button = {
74
+ title: 'Field List Button',
75
+ menu: "form",
76
+ icon: '<i class="icons"><i class="folder outline icon"></i><i class="corner hand pointer icon"></i></i>',
77
+ group: 'block',
78
+ context: 'fieldset_list//',
79
+ properties: {
80
+ type: {
81
+ title: 'Type',
82
+ default: 'add',
83
+ anyOf: [{
84
+ title: 'Add',
85
+ const: 'add'
86
+ }, {
87
+ title: 'Delete',
88
+ const: 'del'
89
+ }, {
90
+ title: 'Up',
91
+ const: 'up'
92
+ }, {
93
+ title: 'Down',
94
+ const: 'down'
95
+ }]
96
+ },
97
+ full: {
98
+ title: 'Full width',
99
+ type: 'boolean',
100
+ default: false
101
+ }
102
+ },
103
+ contents: {
104
+ nodes: "inline*",
105
+ marks: "nolink"
106
+ },
107
+ html: '<button type="button" class="ui [full|?:fluid:] button" value="[type]">Label</button>',
108
+ stylesheets: [
109
+ '../lib/components/button.css',
110
+ ]
111
+ };
package/elements/form.js CHANGED
@@ -92,7 +92,7 @@ exports.api_form = {
92
92
  title: 'Method',
93
93
  nullable: true,
94
94
  type: "string",
95
- pattern: "^(\\w+\\.\\w+)?$"
95
+ pattern: /^(\w+\.\w+)?$/.source
96
96
  },
97
97
  parameters: {
98
98
  title: 'Parameters',
package/elements/grid.js CHANGED
@@ -17,8 +17,10 @@ exports.grid = {
17
17
  },
18
18
  responsive: {
19
19
  title: 'Responsive',
20
- nullable: true,
21
20
  anyOf: [{
21
+ title: 'No',
22
+ const: null
23
+ }, {
22
24
  title: 'Stackable',
23
25
  const: 'stackable'
24
26
  }, {
@@ -67,9 +69,8 @@ exports.grid_row = {
67
69
  properties: {
68
70
  responsive: {
69
71
  title: 'Responsive',
70
- nullable: true,
71
72
  anyOf: [{
72
- title: 'Disable',
73
+ title: 'No',
73
74
  const: null
74
75
  }, {
75
76
  title: 'Stackable',
package/elements/image.js CHANGED
@@ -12,8 +12,9 @@ exports.image = {
12
12
  url: {
13
13
  title: 'Address',
14
14
  description: 'Local or remote URL',
15
- nullable: true,
16
15
  anyOf: [{
16
+ type: "null"
17
+ }, {
17
18
  type: "string",
18
19
  format: "uri"
19
20
  }, {
@@ -156,8 +157,9 @@ exports.inlineImage = {
156
157
  url: {
157
158
  title: 'Address',
158
159
  description: 'Local or remote URL',
159
- nullable: true,
160
160
  anyOf: [{
161
+ type: "null"
162
+ }, {
161
163
  type: "string",
162
164
  format: "uri"
163
165
  }, {
@@ -1,5 +1,5 @@
1
1
  exports.input_date_time = {
2
- title: 'DateTime',
2
+ title: 'Date Time',
3
3
  icon: '<i class="calendar outline icon"></i>',
4
4
  menu: "form",
5
5
  required: ["name"],
@@ -89,7 +89,7 @@ exports.input_date_time = {
89
89
  };
90
90
 
91
91
  exports.input_date_slot = {
92
- title: 'DateSlot',
92
+ title: 'Date Slot',
93
93
  icon: '<i class="calendar outline icon"></i>',
94
94
  menu: "form",
95
95
  required: ["nameStart", "nameEnd"],
@@ -59,16 +59,31 @@ exports.input_property = {
59
59
  let propKey;
60
60
  let required = false;
61
61
  let cases = null;
62
+ let discKey = null;
62
63
  for (let i = 0; i < list.length; i++) {
63
64
  propKey = list[i];
64
65
  required = prop.required && prop.required.indexOf(propKey) >= 0;
65
66
  if (cases) {
66
- prop = cases[propKey];
67
+ if (Array.isArray(cases)) {
68
+ prop = cases.find(obj => {
69
+ if (obj.properties && obj.properties[discKey] && obj.properties[discKey].const == propKey) {
70
+ return true;
71
+ } else {
72
+ return false;
73
+ }
74
+ });
75
+ } else {
76
+ prop = cases[propKey];
77
+ }
67
78
  name = list.slice(0, i - 1).concat(list.slice(i + 1)).join('.');
68
79
  cases = null;
80
+ discKey = null;
69
81
  } else {
70
82
  if (prop.select && prop.select.$data == `0/${propKey}`) {
71
83
  cases = prop.selectCases;
84
+ } else if (prop.discriminator && prop.discriminator.propertyName == propKey) {
85
+ cases = prop.oneOf;
86
+ discKey = propKey;
72
87
  }
73
88
  prop = (prop.items && prop.items.properties || prop.properties || {})[propKey] || null;
74
89
  }
@@ -156,8 +156,8 @@ exports.input_text = {
156
156
  nodes: 'inline*'
157
157
  },
158
158
  patterns: {
159
- tel: '^(\\(\\d+\\))? *\\d+([ .\\-]?\\d+)*$',
160
- email: '^[\\w.!#$%&\'*+\\/=?^`{|}~-]+@\\w(?:[\\w-]{0,61}\\w)?(?:\\.\\w(?:[\\w-]{0,61}\\w)?)*$'
159
+ tel: /^(\(\d+\))? *\d+([ .-]?\d+)*$/.source,
160
+ email: /^[\w.!#$%&'*+/=?^`{|}~-]+@\w(?:[\w-]{0,61}\w)?(?:\.\w(?:[\w-]{0,61}\w)?)*$/.source
161
161
  },
162
162
  html: `<div class="[width|num: wide] field [type|eq:hidden:hidden:]">
163
163
  <label block-content="label">Label</label>
@@ -233,7 +233,7 @@ exports.input_range = {
233
233
  menu: "form",
234
234
  group: "block",
235
235
  context: 'form//',
236
- properties: Object.assign({
236
+ properties: {
237
237
  multiple: {
238
238
  title: 'Multiple',
239
239
  type: 'boolean',
@@ -243,8 +243,9 @@ exports.input_range = {
243
243
  title: 'Pips',
244
244
  type: 'boolean',
245
245
  default: true
246
- }
247
- }, exports.input_number.properties),
246
+ },
247
+ ...exports.input_number.properties
248
+ },
248
249
  contents: {
249
250
  id: 'label',
250
251
  nodes: 'inline*'
@@ -106,8 +106,9 @@ exports.layout = {
106
106
  image: {
107
107
  title: 'Image',
108
108
  description: 'Local or remote URL',
109
- nullable: true,
110
109
  anyOf: [{
110
+ type: "null"
111
+ }, {
111
112
  type: "string",
112
113
  format: "uri"
113
114
  }, {
@@ -168,7 +169,7 @@ exports.layout = {
168
169
  size: {
169
170
  title: 'Size',
170
171
  anyOf: [{
171
- type: 'null',
172
+ const: null,
172
173
  title: 'Auto'
173
174
  }, {
174
175
  const: 'cover',
@@ -181,7 +182,7 @@ exports.layout = {
181
182
  position: {
182
183
  title: 'Position',
183
184
  anyOf: [{
184
- type: 'null',
185
+ const: null,
185
186
  title: 'Top Left'
186
187
  }, {
187
188
  const: 'top center',
@@ -212,7 +213,7 @@ exports.layout = {
212
213
  repeat: {
213
214
  title: 'Repeat',
214
215
  anyOf: [{
215
- type: 'null',
216
+ const: null,
216
217
  title: 'Repeat'
217
218
  }, {
218
219
  const: 'no-repeat',
@@ -234,7 +235,7 @@ exports.layout = {
234
235
  attachment: {
235
236
  title: 'Attachment',
236
237
  anyOf: [{
237
- type: 'null',
238
+ const: null,
238
239
  title: 'Local'
239
240
  }, {
240
241
  const: 'scroll',
package/elements/media.js CHANGED
@@ -8,8 +8,9 @@ exports.video = {
8
8
  url: {
9
9
  title: 'Address',
10
10
  description: 'Local or remote URL',
11
- nullable: true,
12
11
  anyOf: [{
12
+ type: "null"
13
+ }, {
13
14
  type: "string",
14
15
  format: "uri"
15
16
  }, {
@@ -88,8 +89,9 @@ exports.audio = {
88
89
  url: {
89
90
  title: 'Address',
90
91
  description: 'Local or remote URL',
91
- nullable: true,
92
92
  anyOf: [{
93
+ type: "null"
94
+ }, {
93
95
  type: "string",
94
96
  format: "uri"
95
97
  }, {
package/elements/menu.js CHANGED
@@ -9,7 +9,6 @@ exports.menu = {
9
9
  properties: {
10
10
  direction: {
11
11
  title: 'Direction',
12
- nullable: true,
13
12
  anyOf: [{
14
13
  const: null,
15
14
  title: "Horizontal"
@@ -37,7 +36,6 @@ exports.menu_group = {
37
36
  properties: {
38
37
  position: {
39
38
  title: 'Position',
40
- nullable: true,
41
39
  anyOf: [{
42
40
  const: null,
43
41
  title: "Left"
@@ -104,14 +102,14 @@ exports.menu_item_link = {
104
102
  html: '<a class="[labeled|?] item" href="[url|autolink]">Link</a>'
105
103
  };
106
104
 
107
- exports.menu_item_block = Object.assign({}, exports.menu_item_link, {
105
+ exports.menu_item_block = { ...exports.menu_item_link,
108
106
  title: 'Block',
109
107
  priority: 11,
110
108
  contents: {
111
109
  nodes: "block+",
112
110
  marks: "nolink"
113
111
  }
114
- });
112
+ };
115
113
 
116
114
  exports.menu_item_text = {
117
115
  priority: 11,
@@ -153,7 +151,6 @@ exports.menu_item_dropdown = {
153
151
  properties: {
154
152
  position: {
155
153
  title: 'Position',
156
- nullable: true,
157
154
  anyOf: [{
158
155
  const: null,
159
156
  title: "Left"
package/elements/page.js CHANGED
@@ -5,9 +5,10 @@ exports.page.stylesheets = [
5
5
  '../ui/transition.css'
6
6
  ];
7
7
 
8
- exports.page.scripts = exports.page.scripts.concat([
8
+ exports.page.scripts = [
9
+ ...exports.page.scripts,
9
10
  '../ui/transition.js'
10
- ]);
11
+ ];
11
12
 
12
13
  exports.page.properties.transition = {
13
14
  title: 'Transition',
@@ -16,9 +17,8 @@ exports.page.properties.transition = {
16
17
  properties: {
17
18
  close: {
18
19
  title: 'Close',
19
- nullable: true,
20
20
  anyOf: [{
21
- type: 'null',
21
+ const: null,
22
22
  title: 'None'
23
23
  }, {
24
24
  const: 'tr-up',
@@ -39,9 +39,8 @@ exports.page.properties.transition = {
39
39
  },
40
40
  open: {
41
41
  title: 'Open',
42
- nullable: true,
43
42
  anyOf: [{
44
- type: 'null',
43
+ const: null,
45
44
  title: 'None'
46
45
  }, {
47
46
  const: 'tr-up',
@@ -1,11 +1,11 @@
1
- exports.paragraph_nolink = Object.assign({}, exports.paragraph, {
1
+ exports.paragraph_nolink = { ...exports.paragraph,
2
2
  priority: exports.paragraph.priority - 1,
3
3
  group: null,
4
4
  contents: {
5
5
  nodes: "inline*",
6
6
  marks: "nolink"
7
7
  }
8
- });
8
+ };
9
9
 
10
10
  exports.segment = {
11
11
  title: "Segment",
@@ -128,7 +128,7 @@ exports.heading = {
128
128
  id: {
129
129
  nullable: true,
130
130
  type: 'string',
131
- pattern: '^[a-z0-9-]*$'
131
+ pattern: /^[a-z0-9-]*$/.source
132
132
  }
133
133
  },
134
134
  contents: {
@@ -159,13 +159,12 @@ exports.heading = {
159
159
  };
160
160
 
161
161
 
162
- exports.heading_nolink = Object.assign({}, exports.heading, {
162
+ exports.heading_nolink = {
163
+ ...exports.heading,
163
164
  priority: exports.heading.priority - 1,
164
165
  group: null,
165
- contents: Object.assign({}, exports.heading.contents, {
166
- marks: "nolink"
167
- })
168
- });
166
+ contents: { ...exports.heading.contents, marks: "nolink" }
167
+ };
169
168
 
170
169
  exports.divider = {
171
170
  title: "Divider",
@@ -27,7 +27,7 @@ exports.rating = {
27
27
  title: 'Color',
28
28
  anyOf: [{
29
29
  title: 'Default',
30
- type: 'null'
30
+ const: null
31
31
  }, {
32
32
  title: 'Star',
33
33
  const: "star"
@@ -1,5 +1,5 @@
1
1
  exports.sitemap = {
2
- title: "Site map",
2
+ title: "Sitemap",
3
3
  group: "block",
4
4
  icon: '<i class="sitemap icon"></i>',
5
5
  menu: 'link',
@@ -30,12 +30,13 @@ exports.sitemap = {
30
30
  title: schema.title,
31
31
  icon: schema.icon,
32
32
  standalone: true,
33
- properties: Object.assign({
33
+ properties: {
34
34
  leaf: {
35
35
  type: 'boolean',
36
36
  default: Boolean(leaf)
37
- }
38
- }, schema.properties),
37
+ },
38
+ ...schema.properties
39
+ },
39
40
  menu: "link",
40
41
  group: 'sitemap_item',
41
42
  virtual: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pageboard/html",
3
- "version": "0.10.15",
3
+ "version": "0.11.1-1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,62 +1,145 @@
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
+
1
30
  class HTMLElementFieldsetList extends VirtualHTMLElement {
31
+ #size;
32
+ #prefix;
33
+ #model;
34
+ #walk;
35
+
2
36
  fill(values, scope) {
3
- const list = this.listFromValues(Object.assign({}, values));
4
- this.resize(list.length, scope);
37
+ const list = this.#listFromValues({ ...values });
38
+ this.#resize(list.length, scope);
5
39
  }
6
40
 
7
- patch(state) {
41
+ #prepare() {
8
42
  this.ownTpl.prerender();
9
43
  if (this.isContentEditable) return;
10
- if (!this.size) this.resize(0, state.scope);
44
+ for (const node of this.ownTpl.content.querySelectorAll('[block-id]')) {
45
+ node.removeAttribute('block-id');
46
+ }
47
+ const keys = new Set();
48
+ const inputs = this.ownTpl.content.querySelectorAll('[name]');
49
+ for (const node of inputs) {
50
+ keys.add(node.name);
51
+ }
52
+ const splits = Array.from(keys).map(name => name.split('.'));
53
+ const coms = [];
54
+ let pos = 0, com = null;
55
+ while (splits.every(list => {
56
+ if (com == null) {
57
+ if (pos < list.length) {
58
+ com = list[pos];
59
+ return true;
60
+ } else {
61
+ return false;
62
+ }
63
+ } else {
64
+ return list[pos] == com;
65
+ }
66
+ })) {
67
+ coms.push(com);
68
+ com = null;
69
+ pos++;
70
+ }
71
+ if (coms.length) coms.push('');
72
+ const prefix = coms.join('.');
73
+ this.#prefix = prefix;
74
+ const model = {};
75
+ for (const key of keys) {
76
+ if (key.startsWith(prefix)) model[key.substring(prefix.length)] = null;
77
+ }
78
+ this.#model = model;
11
79
  }
12
80
 
13
- setup(state) {
14
- this.ownTpl.prerender();
81
+ patch(state) {
82
+ this.#prepare();
83
+ if (!this.#size) this.#resize(0, state.scope);
15
84
  }
16
85
 
17
- resize(size, scope) {
18
- const len = Math.max(Number(this.dataset.size) || 0, size);
19
- if (this.size == len) return;
20
- this.size = len;
86
+ setup() {
87
+ this.#prepare();
88
+ }
21
89
 
22
- const tpl = this.ownTpl.content.cloneNode(true);
23
- for (const node of tpl.querySelectorAll('[block-id]')) {
24
- node.removeAttribute('block-id');
25
- }
26
- const anc = tpl.querySelectorAll('[name]:not(button)').ancestor();
90
+ #selector(name) {
91
+ return `[block-type="fieldlist_button"][value="${name}"]`;
92
+ }
27
93
 
28
- for (let i = len - 1; i >= 1; i--) {
29
- const clone = this.updateAncestor(anc.cloneNode(true), i);
30
- clone.fuse({ $fieldset: { index: i } }, scope);
31
- anc.parentNode.insertBefore(clone, anc.nextSibling);
94
+ #resize(size, scope) {
95
+ if (this.isContentEditable) return;
96
+ const len = Math.max(Number(this.dataset.size) || 0, size);
97
+ if (this.#size === len) return;
98
+ this.#size = len;
99
+ let tpl = this.ownTpl.content.cloneNode(true);
100
+ const $fieldset = Array.from(Array(len)).map((x, i) => {
101
+ return { index: i };
102
+ });
103
+ const inputs = tpl.querySelectorAll('[name]');
104
+ const prefix = this.#prefix;
105
+ for (const node of inputs) {
106
+ if (node.name.startsWith(prefix)) {
107
+ node.name = `${prefix}[$field.index].${node.name.substring(prefix.length)}`;
108
+ }
109
+ }
110
+ const subtpl = inputs.ancestor();
111
+ if (!subtpl) {
112
+ console.warn("fieldset-list should contain input[name]", this);
113
+ return;
32
114
  }
33
- this.updateAncestor(anc, 0);
34
- anc.fuse({ $fieldset: { index: 0 } }, scope);
35
- tpl.fuse({ $fieldset: { count: len } }, scope);
115
+ subtpl.appendChild(
116
+ subtpl.ownerDocument.createTextNode('[$fieldset|repeat:*:$field|]')
117
+ );
118
+ if (len == 0) {
119
+ let node = tpl.querySelector(this.#selector('add'));
120
+ while (node != null && node != tpl && node != subtpl) {
121
+ while (node.nextSibling) node.nextSibling.remove();
122
+ while (node.previousSibling) node.previousSibling.remove();
123
+ node = node.parentNode;
124
+ }
125
+ }
126
+ tpl = tpl.fuse({ $fieldset }, scope);
127
+
36
128
  const view = this.ownView;
37
129
  view.textContent = '';
38
130
  view.appendChild(tpl);
39
- }
40
131
 
41
- updateAncestor(node, i) {
42
- const prefix = this.prefix;
43
- for (const child of node.querySelectorAll('[name]:not(button)')) {
44
- child.name = `${prefix}${i}.${child.name}`;
45
- }
46
- return node;
132
+ view.querySelectorAll(this.#selector('up')).forEach((node, i) => {
133
+ node.disabled = i == 0;
134
+ });
135
+ view.querySelectorAll(this.#selector('down')).forEach((node, i, arr) => {
136
+ node.disabled = i == arr.length - 1;
137
+ });
47
138
  }
48
139
 
49
- modelFromTemplate() {
50
- const obj = {};
51
- for (const node of this.ownTpl.content.querySelectorAll('[name]:not(button)')) {
52
- obj[node.name] = null;
53
- }
54
- return obj;
55
- }
56
-
57
- listFromValues(values) {
140
+ #listFromValues(values) {
58
141
  const list = [];
59
- const prefix = this.prefix;
142
+ const prefix = this.#prefix;
60
143
  // just unflatten the array
61
144
  for (const [key, val] of Object.entries(values)) {
62
145
  if (!key.startsWith(prefix)) continue;
@@ -71,8 +154,8 @@ class HTMLElementFieldsetList extends VirtualHTMLElement {
71
154
  return list;
72
155
  }
73
156
 
74
- listToValues(values, list) {
75
- const prefix = this.prefix;
157
+ #listToValues(values, list) {
158
+ const prefix = this.#prefix;
76
159
  for (let i = 0; i < list.length; i++) {
77
160
  const obj = list[i];
78
161
  for (const [key, val] of Object.entries(obj)) {
@@ -83,23 +166,47 @@ class HTMLElementFieldsetList extends VirtualHTMLElement {
83
166
 
84
167
  handleClick(e, state) {
85
168
  if (this.isContentEditable) return;
86
- const btn = e.target.closest('button[type="button"][name]');
169
+ const btn = e.target.closest('button');
87
170
  if (!btn) return;
88
- if (["add", "del"].includes(btn.name) == false) return;
171
+ const action = btn.value;
172
+ if (["add", "del", "up", "down"].includes(action) == false) return;
89
173
 
90
174
  const form = this.closest('form');
91
175
  const values = form.read(true);
92
- const list = this.listFromValues(values);
93
- const index = Number(btn.value);
94
- if (!Number.isInteger(index) || index < 0 || index > list.length) {
95
- throw new Error(`fieldset-list expects ${btn.outerHTML} to have a value with a valid index`);
96
- }
97
- if (btn.name == "add") {
98
- list.splice(index + 1, 0, this.modelFromTemplate());
99
- } else if (btn.name == "del") {
100
- list.splice(index, 1);
176
+ const list = this.#listFromValues(values);
177
+ const prefix = this.#prefix;
178
+ if (!this.#walk) this.#walk = new WalkIndex(this, (node) => {
179
+ if (node.name?.startsWith(prefix)) {
180
+ const index = Number(node.name.substring(prefix.length).split('.').shift());
181
+ if (Number.isInteger(index) || index >= 0 || index < list.length) {
182
+ return index;
183
+ }
184
+ }
185
+ return null;
186
+ });
187
+ let index;
188
+
189
+ switch (action) {
190
+ case "add":
191
+ list.splice((this.#walk.findBefore(btn) ?? -1) + 1, 0, this.#model);
192
+ break;
193
+ case "del":
194
+ list.splice(this.#walk.findBefore(btn) ?? 0, 1);
195
+ break;
196
+ case "up":
197
+ index = this.querySelectorAll(this.#selector('up')).indexOf(btn);
198
+ if (index > 0) {
199
+ list.splice(index - 1, 0, list.splice(index, 1).pop());
200
+ }
201
+ break;
202
+ case "down":
203
+ index = this.querySelectorAll(this.#selector('down')).indexOf(btn);
204
+ if (index < list.length - 1) {
205
+ list.splice(index + 1, 0, list.splice(index, 1).pop());
206
+ }
207
+ break;
101
208
  }
102
- this.listToValues(values, list);
209
+ this.#listToValues(values, list);
103
210
  form.fill(values, state.scope);
104
211
  }
105
212
 
@@ -111,14 +218,6 @@ class HTMLElementFieldsetList extends VirtualHTMLElement {
111
218
  get ownView() {
112
219
  return this.children.find(node => node.matches('.view'));
113
220
  }
114
- get prefix() {
115
- const prefix = this.dataset.prefix;
116
- if (prefix) return prefix + ".";
117
- else return "";
118
- }
119
221
  }
120
222
 
121
223
  VirtualHTMLElement.define('element-fieldset-list', HTMLElementFieldsetList);
122
-
123
-
124
-
package/ui/fieldset.js CHANGED
@@ -15,8 +15,8 @@ class HTMLCustomFieldSetElement extends HTMLFieldSetElement {
15
15
  this.disabled = this.hidden = val != this.options.value;
16
16
  }
17
17
 
18
- patch() {
19
- this.#update();
18
+ patch(state) {
19
+ state.finish(() => this.#update());
20
20
  }
21
21
  setup() {
22
22
  this.form?.addEventListener('change', this);
package/ui/form.js CHANGED
@@ -108,18 +108,15 @@ class HTMLCustomFormElement extends HTMLFormElement {
108
108
  return query;
109
109
  }
110
110
  fill(query, scope) {
111
- // workaround for merging arrays
112
- const tagList = "element-fieldset-list";
113
- const FieldSet = VirtualHTMLElement.define(tagList);
114
- for (const node of this.querySelectorAll(tagList)) {
115
- if (!node.fill) Object.setPrototypeOf(node, FieldSet.prototype);
111
+ // fieldset-list are not custom inputs yet
112
+ for (const node of this.querySelectorAll("element-fieldset-list")) {
116
113
  node.fill(query, scope);
117
114
  }
118
115
  const vars = [];
119
116
  for (const elem of this.elements) {
120
117
  const name = elem.name;
121
118
  if (!name) continue;
122
- if (Object.prototype.hasOwnProperty.call(query, name) && !vars.includes(name)) vars.push(name);
119
+ if (name in query && !vars.includes(name)) vars.push(name);
123
120
  const val = query[name];
124
121
  const str = ((v) => {
125
122
  if (v == null) return "";
@@ -228,7 +225,7 @@ class HTMLCustomFormElement extends HTMLFormElement {
228
225
  const loc = Page.parse(redirect);
229
226
  Object.assign(loc.query, this.read(false));
230
227
  if (loc.samePathname(state)) {
231
- loc.query = Object.assign({}, state.query, loc.query);
228
+ loc.query = { ...state.query, ...loc.query };
232
229
  }
233
230
  let status = 200;
234
231
  const p = this.ignoreInputChange
@@ -430,7 +427,7 @@ Page.setup((state) => {
430
427
  }
431
428
  });
432
429
 
433
- Page.ready((state) => {
430
+ Page.patch(state => {
434
431
  const filters = state.scope.$filters;
435
432
 
436
433
  function linearizeValues(query, obj = {}, prefix) {
@@ -461,35 +458,32 @@ Page.ready((state) => {
461
458
  if (action == "toggle") {
462
459
  action = val ? "enable" : "disable";
463
460
  }
464
- // NB: call Class methods to deal with uninstantiated custom form
465
- if (action == "enable") {
466
- HTMLCustomFormElement.prototype.enable.call(form);
467
- } else if (action == "disable") {
468
- HTMLCustomFormElement.prototype.disable.call(form);
469
- } else if (action == "fill") {
470
- if (val == null) {
471
- form.reset();
472
- } else if (typeof val == "object") {
473
- let values = val;
474
- if (val.id && val.data) {
475
- // old way
476
- values = Object.assign({}, val.data);
477
- for (const key of Object.keys(val)) {
478
- if (key != "data") values['$' + key] = val[key];
461
+
462
+ state.finish(() => {
463
+ if (action == "enable") {
464
+ form.enable();
465
+ } else if (action == "disable") {
466
+ form.disable();
467
+ } else if (action == "fill") {
468
+ if (val == null) {
469
+ form.reset();
470
+ } else if (typeof val == "object") {
471
+ let values = val;
472
+ if (val.id && val.data) {
473
+ // old way
474
+ values = { ...val.data };
475
+ for (const key of Object.keys(val)) {
476
+ if (key != "data") values['$' + key] = val[key];
477
+ }
478
+ } else {
479
+ // new way
479
480
  }
480
- } else {
481
- // new way
481
+ form.fill(linearizeValues(values), state.scope);
482
+ form.save();
482
483
  }
483
- HTMLCustomFormElement.prototype.fill.call(form, linearizeValues(values), state.scope);
484
- HTMLCustomFormElement.prototype.save.call(form);
485
484
  }
486
- } else if (action == "read") {
487
- const obj = {};
488
- for (const [key, kval] of Object.entries(val)) {
489
- if (form.querySelector(`[name="${key}"]`)) obj[key] = kval;
490
- }
491
- return obj;
492
- }
485
+ });
486
+
493
487
  return val;
494
488
  };
495
489
  });
package/ui/pagination.js CHANGED
@@ -27,9 +27,10 @@ class HTMLElementPagination extends HTMLAnchorElement {
27
27
  } else {
28
28
  this.setAttribute('href', Page.format({
29
29
  pathname: state.pathname,
30
- query: Object.assign({}, state.query, {
30
+ query: {
31
+ ...state.query,
31
32
  [name]: cur || undefined
32
- })
33
+ }
33
34
  }));
34
35
  }
35
36
  state.finish(() => {
package/ui/tab.js CHANGED
@@ -14,7 +14,7 @@ class HTMLElementTabs extends VirtualHTMLElement {
14
14
  const id = this.id;
15
15
 
16
16
  this.items.children.forEach((item, i) => {
17
- const query = Object.assign({}, state.query);
17
+ const query = { ...state.query };
18
18
  const key = `${id}.index`;
19
19
  if (i == 0) delete query[key];
20
20
  else query[key] = i;