@neeloong/form 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @neeloong/form v0.23.0
2
+ * @neeloong/form v0.25.0
3
3
  * (c) 2024-2026 Fierflame
4
4
  * @license Apache-2.0
5
5
  */
@@ -36,7 +36,7 @@ function createBooleanStates(self, defState, fn, parent) {
36
36
 
37
37
  }
38
38
 
39
- /** @import { Schema } from '../types.mjs' */
39
+ /** @import { Schema } from '../Schema.types.mjs' */
40
40
  /** @param {*} v */
41
41
  const string = v => typeof v === 'string' && v || null;
42
42
  /** @param {*} v */
@@ -133,7 +133,7 @@ function createRef(store) {
133
133
  return r;
134
134
  }
135
135
 
136
- /** @import { Schema } from '../types.mjs' */
136
+ /** @import { Schema } from '../Schema.types.mjs' */
137
137
  /** @import ArrayStore from './ArrayStore.mjs' */
138
138
 
139
139
 
@@ -185,7 +185,7 @@ function setStore$1(type, Class) {
185
185
  }
186
186
 
187
187
  /** @import Store from './Store.mjs' */
188
- /** @import { AsyncValidator, Validator } from '../types.mjs' */
188
+ /** @import { Schema } from '../Schema.types.mjs' */
189
189
  /**
190
190
  *
191
191
  * @param {*} v
@@ -200,7 +200,7 @@ function toResult(v) {
200
200
  /**
201
201
  *
202
202
  * @param {Store} store
203
- * @param {...Validator | undefined | null | (Validator | undefined | null)[]} validators
203
+ * @param {...Schema.Validator | undefined | null | (Schema.Validator | undefined | null)[]} validators
204
204
  * @returns
205
205
  */
206
206
  function createValidator(store, ...validators) {
@@ -223,7 +223,7 @@ function createValidator(store, ...validators) {
223
223
  /**
224
224
  *
225
225
  * @param {Store} store
226
- * @param {...AsyncValidator | undefined | null | (AsyncValidator | undefined | null)[]} validators
226
+ * @param {...Schema.AsyncValidator | undefined | null | (Schema.AsyncValidator | undefined | null)[]} validators
227
227
  * @returns {[exec: () => Promise<string[]>, state: Signal.Computed<string[]>, stop: () => void]}
228
228
  */
229
229
  function createAsyncValidator(store, ...validators) {
@@ -238,7 +238,7 @@ function createAsyncValidator(store, ...validators) {
238
238
  const st = new Signal.State(/** @type {string[]} */([]));
239
239
  /**
240
240
  *
241
- * @param {AsyncValidator} validator
241
+ * @param {Schema.AsyncValidator} validator
242
242
  * @param {AbortSignal} signal
243
243
  */
244
244
  async function run(validator, signal) {
@@ -292,7 +292,8 @@ function makeDefault(store, def) {
292
292
  }
293
293
 
294
294
  /** @import { Ref } from './ref.mjs' */
295
- /** @import { AsyncValidator, Schema, Validator } from '../types.mjs' */
295
+ /** @import { Schema } from '../Schema.types.mjs' */
296
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
296
297
 
297
298
  /**
298
299
  * 管理单个表单字段的状态和行为
@@ -389,8 +390,8 @@ class Store {
389
390
  * @param {number} [options.maxLength]
390
391
  * @param {RegExp} [options.pattern]
391
392
  * @param {(Schema.Value.Group | Schema.Value | string | number)[]} [options.values] 可选值
392
- * @param {Validator | Validator[] | null} [options.validator]
393
- * @param {{[k in keyof Schema.Events]?: AsyncValidator | AsyncValidator[] | null}} [options.validators]
393
+ * @param {Schema.Validator | Schema.Validator[] | null} [options.validator]
394
+ * @param {{[k in keyof Schema.Events]?: Schema.AsyncValidator | Schema.AsyncValidator[] | null}} [options.validators]
394
395
  *
395
396
  * @param {Ref?} [options.ref]
396
397
  *
@@ -422,6 +423,7 @@ class Store {
422
423
  this.#type = schema.type;
423
424
  this.#meta = schema.meta;
424
425
  this.#component = schema.component;
426
+ this.#layout = schema.layout || {};
425
427
 
426
428
  const selfNewState = new Signal.State(Boolean(isNew));
427
429
  this.#selfNew = selfNewState;
@@ -512,6 +514,9 @@ class Store {
512
514
  this.listen(k, f);
513
515
  }
514
516
  }
517
+ /** @type {StoreLayout.Field<any>} */
518
+ #layout
519
+ get layout() { return this.#layout; }
515
520
  #createDefault;
516
521
  /** @param {any} [value] @returns {any} */
517
522
  createDefault(value) { return this.#createDefault(value); }
@@ -899,7 +904,7 @@ class Store {
899
904
  /**
900
905
  * 异步校验
901
906
  * @overload
902
- * @param {true} [path]
907
+ * @param {true} [self]
903
908
  * @returns {Promise<string[] | null>}
904
909
  */
905
910
  /**
@@ -922,6 +927,7 @@ class Store {
922
927
  });
923
928
  }
924
929
  const selfPath = Array.isArray(path) ? path : [];
930
+ if (this.#hidden.get()) { return Promise.resolve([]); }
925
931
  const list = [this.validate(true).then(errors => {
926
932
  if (!errors?.length) { return []; }
927
933
  return [{ path: [...selfPath], store: /** @type {Store} */(this), errors }];
@@ -933,7 +939,7 @@ class Store {
933
939
  }
934
940
  }
935
941
 
936
- /** @import { Schema } from '../types.mjs' */
942
+ /** @import { Schema } from '../Schema.types.mjs' */
937
943
 
938
944
  /**
939
945
  * @template {Record<string, any>} [T=Record<string, any>]
@@ -997,7 +1003,7 @@ class ObjectStore extends Store {
997
1003
  // @ts-ignore
998
1004
  setObjectStore(ObjectStore);
999
1005
 
1000
- /** @import { Schema } from '../types.mjs' */
1006
+ /** @import { Schema } from '../Schema.types.mjs' */
1001
1007
 
1002
1008
 
1003
1009
 
@@ -1051,7 +1057,7 @@ class ArrayStore extends Store {
1051
1057
  }
1052
1058
 
1053
1059
  };
1054
- super(schema, {
1060
+ super({ ...schema, immutable: false }, {
1055
1061
  index, new: isNew, parent,
1056
1062
  size: new Signal.Computed(() => childrenState.get().length),
1057
1063
  setValue(v) {
@@ -1088,7 +1094,10 @@ class ArrayStore extends Store {
1088
1094
  },
1089
1095
  };
1090
1096
  this.#create = (index, isNew) => {
1091
- const child = create(schema, { ...childCommonOptions, index, new: isNew });
1097
+ const child = create(
1098
+ { ...schema, creatable: true },
1099
+ { ...childCommonOptions, index, new: isNew },
1100
+ );
1092
1101
  child.index = index;
1093
1102
  return child;
1094
1103
  };
@@ -3486,8 +3495,7 @@ class EventEmitter {
3486
3495
  }
3487
3496
  }
3488
3497
 
3489
- /** @import { Component, Relatedness } from '../types.mjs' */
3490
- /** @import Store from '../Store/index.mjs' */
3498
+ /** @import { Component } from '../types.mjs' */
3491
3499
  /** @import Environment from './Environment/index.mjs' */
3492
3500
  /** @import * as Layout from '../Layout/index.mjs' */
3493
3501
 
@@ -3657,7 +3665,8 @@ function bindFilters(env, fn, filterFns) {
3657
3665
  }
3658
3666
  }
3659
3667
 
3660
- /** @import { Component, Relatedness } from '../types.mjs' */
3668
+ /** @import { Component } from '../types.mjs' */
3669
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
3661
3670
  /** @import { ComponentHandler } from './types.mjs' */
3662
3671
  /** @import Store from '../Store/index.mjs' */
3663
3672
  /** @import Environment from './Environment/index.mjs' */
@@ -3668,7 +3677,7 @@ function bindFilters(env, fn, filterFns) {
3668
3677
  * @param {Component | string} component
3669
3678
  * @param {Environment} env
3670
3679
  * @param {Store?} store
3671
- * @param {((store: Store, el: Element | Relatedness) => () => void)?} [relate]
3680
+ * @param {((store: Store, el: Element | StoreLayout.Relatedness) => () => void)?} [relate]
3672
3681
  * @returns
3673
3682
  */
3674
3683
  function createContext(component, env, store, relate) {
@@ -4546,7 +4555,7 @@ function divergent(layout, parent, next, env, renderItem) {
4546
4555
  * @param {Record<string, [Layout.Template, Environment]>} templates
4547
4556
  * @param {string[]} componentPath
4548
4557
  * @param {Record<string, Enhancement>} enhancements
4549
- * @param {((store: Store, el: Element | Relatedness) => () => void)?} [relate]
4558
+ * @param {((store: Store, el: Element | StoreLayout.Relatedness) => () => void)?} [relate]
4550
4559
  * @param {Component.Getter?} [getComponent]
4551
4560
  */
4552
4561
  function renderNode(layout, parent, next, env, templates, componentPath, enhancements, relate, getComponent) {
@@ -4628,7 +4637,7 @@ function createTemplates(env, parentTemplates, newTemplates) {
4628
4637
  * @param {Record<string, [Layout.Template, Environment]>} parentTemplates
4629
4638
  * @param {string[]} componentPath
4630
4639
  * @param {Record<string, Enhancement>} enhancements
4631
- * @param {((store: Store, el: Element | Relatedness) => () => void)?} [relate]
4640
+ * @param {((store: Store, el: Element | StoreLayout.Relatedness) => () => void)?} [relate]
4632
4641
  * @param {Component.Getter?} [getComponent]
4633
4642
  * @returns {() => void}
4634
4643
  */
@@ -4693,7 +4702,7 @@ function renderChild(layout, parent, next, parentEnv, parentTemplates, component
4693
4702
  * @param {Record<string, [Layout.Template, Environment]>} templates
4694
4703
  * @param {string[]} componentPath
4695
4704
  * @param {Record<string, Enhancement>} enhancements
4696
- * @param {((store: Store, el: Element | Relatedness) => () => void)?} [relate]
4705
+ * @param {((store: Store, el: Element | StoreLayout.Relatedness) => () => void)?} [relate]
4697
4706
  * @param {Component.Getter?} [getComponent]
4698
4707
  * @returns {() => void}
4699
4708
  */
@@ -4716,7 +4725,7 @@ function renderChildren(layouts, parent, next, env, templates, componentPath, en
4716
4725
  * @param {object} [options] 选项
4717
4726
  * @param {Record<string, Store | {get?(): any; set?(v: any): void; exec?(...p: any[]): any; calc?(...p: any[]): any }>} [options.global] 全局数据
4718
4727
  * @param {Component.Getter?} [options.component] 自定义组件
4719
- * @param {(store: Store, el: Element | Relatedness) => () => void} [options.relate] 关联函数
4728
+ * @param {(store: Store, el: Element | StoreLayout.Relatedness) => () => void} [options.relate] 关联函数
4720
4729
  * @param {Record<string, Enhancement>} [options.enhancements] 增强信息
4721
4730
  * @returns {() => void}
4722
4731
  */
@@ -4729,7 +4738,7 @@ function render(store, layouts, parent, { component, global, relate, enhancement
4729
4738
  }
4730
4739
 
4731
4740
  /** @import { Store } from '../Store/index.mjs' */
4732
- /** @import { StoreLayout } from '../types.mjs' */
4741
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
4733
4742
 
4734
4743
  /**
4735
4744
  *
@@ -4755,7 +4764,7 @@ function createFieldFilter(field) {
4755
4764
  * @param {Store} store
4756
4765
  * @param {Node} node
4757
4766
  * @param {StoreLayout.Options?} options
4758
- * @param {StoreLayout<T>?} [layout]
4767
+ * @param {StoreLayout<T>} layout
4759
4768
  * @param {Node} [anchor]
4760
4769
  * @param {(child?: Store<any, any> | undefined) => void} [dragenter]
4761
4770
  * @returns {void}
@@ -4769,8 +4778,10 @@ function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragent
4769
4778
  const field = node.getAttribute('name') || '';
4770
4779
  const mode = node.getAttribute('mode') || '';
4771
4780
  const fieldStore = field ? store.child(field) : store;
4772
- const fieldLayout = field ? layout?.fields?.find(createFieldFilter(field)) || null : null;
4773
4781
  if (!fieldStore) { return; }
4782
+ const fieldLayout = field
4783
+ ? layout?.fields?.find(createFieldFilter(field)) || fieldStore.layout
4784
+ : { ...layout, html: '' };
4774
4785
  switch (mode) {
4775
4786
  case 'grid': {
4776
4787
  const el = Form(store, fieldRenderer, fieldLayout, options);
@@ -4778,7 +4789,7 @@ function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragent
4778
4789
  return;
4779
4790
  }
4780
4791
  }
4781
- const res = fieldRenderer(fieldStore, layout?.renderer, options);
4792
+ const res = fieldRenderer(fieldStore, layout.renderer, options);
4782
4793
  if (res) {
4783
4794
  node.replaceWith(res);
4784
4795
  return;
@@ -4811,7 +4822,9 @@ function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragent
4811
4822
  return;
4812
4823
  }
4813
4824
  node.removeAttribute('nl-form-field');
4814
- const fieldLayout = field ? layout?.fields?.find(createFieldFilter(field)) : layout;
4825
+ const fieldLayout = field
4826
+ ? layout.fields?.find(createFieldFilter(field)) || fieldStore.layout
4827
+ : { ...layout, html: '' };
4815
4828
  if (!array) {
4816
4829
  renderHtml(fieldStore, fieldRenderer, node, options, fieldLayout, anchor);
4817
4830
  return;
@@ -4976,26 +4989,32 @@ function getHtmlContent(html) {
4976
4989
  * @template T
4977
4990
  * @param {Store<any, any>} store
4978
4991
  * @param {StoreLayout.Renderer<T>} fieldRenderer
4979
- * @param {StoreLayout.Field<T>?} layout
4992
+ * @param {StoreLayout<T>} layout
4980
4993
  * @param {StoreLayout.Options?} options
4981
4994
  * @returns {ParentNode?}
4982
4995
  */
4983
4996
  function FormFieldInline(store, fieldRenderer, layout, options) {
4984
4997
  if (options?.signal?.aborted) { return null; }
4985
- return fieldRenderer(store, layout?.renderer, options);
4998
+ const html = layout.html;
4999
+ if (html) {
5000
+ const content = getHtmlContent(html);
5001
+ renderHtml(store, fieldRenderer, content, options, layout);
5002
+ return content;
5003
+ }
5004
+ return fieldRenderer(store, layout.renderer, options);
4986
5005
  }
4987
5006
 
4988
5007
  /** @import { Store } from '../Store/index.mjs' */
4989
- /** @import { StoreLayout } from '../types.mjs' */
5008
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
4990
5009
 
4991
5010
  /**
4992
5011
  *
4993
5012
  * @template T
4994
5013
  * @param {Store<any, any>} store
4995
5014
  * @param {StoreLayout.Renderer<T>} fieldRenderer
4996
- * @param {StoreLayout.Field<T>?} layout
5015
+ * @param {StoreLayout.Field<T>} layout
4997
5016
  * @param {object} option
4998
- * @param {StoreLayout.Column[]} option.columns
5017
+ * @param {StoreLayout.Column<T>[]} option.columns
4999
5018
  * @param {() => void} option.remove
5000
5019
  * @param {() => void} option.dragenter
5001
5020
  * @param {() => void} option.dragstart
@@ -5005,8 +5024,8 @@ function FormFieldInline(store, fieldRenderer, layout, options) {
5005
5024
  * @returns {HTMLTableSectionElement}
5006
5025
  */
5007
5026
  function Line(store, fieldRenderer, layout, {
5008
- columns,
5009
- remove, dragenter, dragstart, dragend, deletable
5027
+ columns, deletable,
5028
+ remove, dragenter, dragstart, dragend,
5010
5029
  }, options) {
5011
5030
  const root = document.createElement('tbody');
5012
5031
  root.addEventListener('dragenter', () => {
@@ -5030,6 +5049,7 @@ function Line(store, fieldRenderer, layout, {
5030
5049
  const body = root.appendChild(document.createElement('tr'));
5031
5050
  const main = body.appendChild(document.createElement('td'));
5032
5051
  main.colSpan = columns.length;
5052
+ main.appendChild(form);
5033
5053
  body.hidden = true;
5034
5054
  trigger = () => {
5035
5055
  if (body.hidden) {
@@ -5068,13 +5088,13 @@ function Line(store, fieldRenderer, layout, {
5068
5088
 
5069
5089
  }
5070
5090
 
5071
- for (const name of columns) {
5072
- const { actions, field, pattern } = name;
5091
+ for (const column of columns) {
5092
+ const { actions, field, pattern } = column;
5073
5093
  if (!actions?.length) {
5074
5094
  const td = head.appendChild(document.createElement('td'));
5075
5095
  const child = field && store.child(field);
5076
5096
  if (child) {
5077
- const el = FormFieldInline(child, fieldRenderer, null, options);
5097
+ const el = FormFieldInline(child, fieldRenderer, column, options);
5078
5098
  if (el) { td.appendChild(el); }
5079
5099
  }
5080
5100
  continue;
@@ -5105,7 +5125,7 @@ function Line(store, fieldRenderer, layout, {
5105
5125
  const del = handle.appendChild(document.createElement('button'));
5106
5126
  del.classList.add('NeeloongForm-table-remove');
5107
5127
  del.addEventListener('click', remove);
5108
- watch(() => !deletable.get() || store.readonly || store.disabled, disabled => {
5128
+ watch(() => !deletable.get(), disabled => {
5109
5129
  del.disabled = disabled;
5110
5130
  }, true, options.signal);
5111
5131
  continue;
@@ -5122,56 +5142,26 @@ function Line(store, fieldRenderer, layout, {
5122
5142
  return root;
5123
5143
  }
5124
5144
 
5125
- /** @import { Store, ArrayStore } from '../Store/index.mjs' */
5126
- /** @import { StoreLayout } from '../types.mjs' */
5145
+ /** @import { ArrayStore } from '../Store/index.mjs' */
5146
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5127
5147
 
5128
- /**
5129
- *
5130
- * @param {AbortSignal | null | undefined} signal
5131
- * @param {HTMLElement} parent
5132
- * @param {StoreLayout.Column[]} columns
5133
- * @param {() => any} add
5134
- * @param {{get(): boolean}} addable
5135
- * @param {boolean?} [editable]
5136
- * @returns {void}
5137
- */
5138
- function renderHead(signal, parent, columns, add, addable, editable) {
5139
- const tr = parent.appendChild(document.createElement('tr'));
5140
- for (const { action, actions, width, label } of columns) {
5141
- const th = tr.appendChild(document.createElement('th'));
5142
- if (width) { th.setAttribute('width', `${width}`); }
5143
- if (![action, actions].flat().includes('add')) {
5144
- th.innerText = label || '';
5145
- continue;
5146
- }
5147
- if (!editable) { continue; }
5148
- const button = th.appendChild(document.createElement('button'));
5149
- button.addEventListener('click', add);
5150
- button.classList.add('NeeloongForm-table-add');
5151
- watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true, signal);
5152
- }
5153
- }
5154
5148
  /**
5155
5149
  *
5156
5150
  * @template T
5157
5151
  * @param {ArrayStore} store
5158
- * @param {StoreLayout.Renderer<T>} fieldRenderer
5159
- * @param {StoreLayout.Field<T>?} layout
5160
- * @param {StoreLayout.Options?} options
5161
- * @returns {HTMLTableElement?}
5152
+ * @param {StoreLayout.Field<T>} layout
5153
+ * @param {StoreLayout.Action[]} actionOptions
5154
+ * @param {(fields: { field: string; width: any; label: any; }[]) => StoreLayout.Column<T>[]} createDefault
5155
+ * @returns {StoreLayout.Column<T>[]}
5162
5156
  */
5163
- function Table(store, fieldRenderer, layout, options) {
5164
- if (options?.signal?.aborted) { return null; }
5165
- const headerColumns = layout?.columns;
5157
+ function getColumns(store, layout, actionOptions, createDefault) {
5166
5158
  const fieldList = Object.entries(store.type || {})
5167
5159
  .filter(([k, v]) => typeof v?.type !== 'object')
5168
5160
  .map(([field, { width, label }]) => ({ field, width, label }));
5169
- /** @type {StoreLayout.Column[]} */
5170
- let columns = [];
5161
+ const headerColumns = layout.columns;
5171
5162
  if (Array.isArray(headerColumns)) {
5172
5163
  const map = new Map(fieldList.map(v => [v.field, v]));
5173
-
5174
- /** @type {(StoreLayout.Column | null)[]} */
5164
+ /** @type {(StoreLayout.Column<T> | null)[]} */
5175
5165
  const allColumns = headerColumns.map(v => {
5176
5166
  if (!v) { return null; }
5177
5167
  if (typeof v === 'number') { return { placeholder: v }; }
@@ -5179,7 +5169,7 @@ function Table(store, fieldRenderer, layout, options) {
5179
5169
  if (typeof v !== 'object') { return null; }
5180
5170
  if (Array.isArray(v)) {
5181
5171
  /** @type {Set<StoreLayout.Action>} */
5182
- const options = new Set(['add', 'move', 'trigger', 'remove', 'serial', 'open', 'collapse']);
5172
+ const options = new Set(actionOptions);
5183
5173
  const actions = v.filter(v => options.delete(v));
5184
5174
  if (!actions) { return null; }
5185
5175
  return { actions };
@@ -5191,32 +5181,82 @@ function Table(store, fieldRenderer, layout, options) {
5191
5181
  return { field, placeholder, width, label: label || define.label };
5192
5182
  }
5193
5183
  }
5194
- const options = new Set(['add', 'move', 'trigger', 'remove', 'serial']);
5184
+ const options = new Set(actionOptions);
5195
5185
  const allActions = [action, actions].flat().filter(v => v && options.delete(v));
5196
5186
  if (allActions.length) {
5197
5187
  return { actions: /** @type {StoreLayout.Action[]} */(allActions), width, label };
5198
5188
  }
5199
- // if (pattern) {
5200
- // return { pattern, placeholder, width, label };
5201
- // }
5189
+ if (pattern) {
5190
+ return { pattern, placeholder, width, label };
5191
+ }
5192
+ if (placeholder || width) {
5193
+ return { placeholder, width, label };
5194
+ }
5202
5195
  return null;
5203
5196
  });
5204
- columns = /** @type {StoreLayout.Column[]} */(allColumns.filter(Boolean));
5197
+ const columns = /** @type {StoreLayout.Column<T>[]} */(allColumns.filter(Boolean));
5198
+ if (columns.length) { return columns; }
5199
+ }
5200
+ return createDefault(fieldList);
5201
+ }
5202
+
5203
+ /** @import { Store, ArrayStore } from '../Store/index.mjs' */
5204
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5205
5205
 
5206
+ /**
5207
+ *
5208
+ * @template T
5209
+ * @param {AbortSignal | null | undefined} signal
5210
+ * @param {HTMLElement} parent
5211
+ * @param {StoreLayout.Column<T>[]} columns
5212
+ * @param {() => any} add
5213
+ * @param {{get(): boolean}} addable
5214
+ * @param {boolean?} [editable]
5215
+ * @returns {void}
5216
+ */
5217
+ function renderHead(signal, parent, columns, add, addable, editable) {
5218
+ const tr = parent.appendChild(document.createElement('tr'));
5219
+ for (const { action, actions, width, label } of columns) {
5220
+ const th = tr.appendChild(document.createElement('th'));
5221
+ if (width) { th.setAttribute('width', `${width}`); }
5222
+ if (![action, actions].flat().includes('add')) {
5223
+ th.innerText = label || '';
5224
+ continue;
5225
+ }
5226
+ if (!editable) { continue; }
5227
+ const button = th.appendChild(document.createElement('button'));
5228
+ button.addEventListener('click', add);
5229
+ button.classList.add('NeeloongForm-table-add');
5230
+ watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true, signal);
5206
5231
  }
5207
- if (!columns.length) {
5208
- columns = [
5232
+ }
5233
+
5234
+ /**
5235
+ *
5236
+ * @template T
5237
+ * @param {ArrayStore} store
5238
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
5239
+ * @param {StoreLayout.Field<T>} layout
5240
+ * @param {StoreLayout.Options?} options
5241
+ * @returns {HTMLTableElement?}
5242
+ */
5243
+ function Table(store, fieldRenderer, layout, options) {
5244
+ if (options?.signal?.aborted) { return null; }
5245
+ const columns = getColumns(
5246
+ store,
5247
+ layout,
5248
+ ['add', 'move', 'trigger', 'remove', 'serial'],
5249
+ fields => [
5209
5250
  { actions: ['add', 'trigger', 'move', 'remove', 'serial'] },
5210
- ...fieldList.slice(0, 3),
5211
- ];
5212
- }
5251
+ ...fields.slice(0, 3),
5252
+ ],
5253
+ );
5213
5254
 
5214
5255
  const table = document.createElement('table');
5215
5256
  table.classList.add('NeeloongForm-table');
5216
5257
  const thead = table.appendChild(document.createElement('thead'));
5217
5258
 
5218
- const addable = new Signal.Computed(() => store.addable);
5219
- const deletable = { get: () => Boolean(options?.editable) };
5259
+ const addable = new Signal.Computed(() => !store.readonly && !store.disabled && store.addable);
5220
5260
  function add() {
5221
5261
  const data = {};
5222
5262
  store.add(data);
@@ -5254,7 +5294,7 @@ function Table(store, fieldRenderer, layout, options) {
5254
5294
 
5255
5295
  }
5256
5296
  renderHead(options?.signal, thead, columns, add, addable, Boolean(options?.editable));
5257
- switch (layout?.tableFoot) {
5297
+ switch (layout.tableFoot) {
5258
5298
  default:
5259
5299
  case 'header': {
5260
5300
  const tfoot = table.appendChild(document.createElement('tfoot'));
@@ -5288,11 +5328,11 @@ function Table(store, fieldRenderer, layout, options) {
5288
5328
  const ac = new AbortController();
5289
5329
  const el = Line(child, fieldRenderer, layout, {
5290
5330
  columns,
5331
+ deletable: new Signal.Computed(() => !store.readonly && !store.disabled && child.removable),
5291
5332
  remove: remove.bind(null, child),
5292
5333
  dragenter: dragenter.bind(null, child),
5293
5334
  dragstart: dragstart.bind(null, child),
5294
5335
  dragend,
5295
- deletable,
5296
5336
  }, {
5297
5337
  ...options,
5298
5338
  signal: options?.signal ? AbortSignal.any([options?.signal, ac.signal]) : ac.signal,
@@ -5318,16 +5358,16 @@ function Table(store, fieldRenderer, layout, options) {
5318
5358
  return table;
5319
5359
  }
5320
5360
 
5321
- /** @import { StoreLayout } from '../types.mjs' */
5361
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5322
5362
 
5323
5363
  /**
5324
5364
  *
5325
5365
  * @param {HTMLElement} root
5326
- * @param {StoreLayout.Grid?} [layout]
5366
+ * @param {StoreLayout.Grid} layout
5327
5367
  * @returns {void}
5328
5368
  */
5329
5369
  function bindGrid(root, layout) {
5330
- const { colStart, colSpan, colEnd, rowStart, rowSpan, rowEnd } = layout || {};
5370
+ const { colStart, colSpan, colEnd, rowStart, rowSpan, rowEnd } = layout;
5331
5371
  root.classList.add(`NeeloongForm-item-grid`);
5332
5372
  if (colStart && colEnd) {
5333
5373
  root.style.gridColumn = `${colStart} / ${colEnd}`;
@@ -5345,7 +5385,7 @@ function bindGrid(root, layout) {
5345
5385
  }
5346
5386
  }
5347
5387
 
5348
- /** @import { StoreLayout } from '../types.mjs' */
5388
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5349
5389
 
5350
5390
  /**
5351
5391
  * @typedef {object} CellValues
@@ -5383,7 +5423,7 @@ function createStdCell(signal, values) {
5383
5423
  /**
5384
5424
  *
5385
5425
  * @param {AbortSignal | null | undefined} signal
5386
- * @param {StoreLayout.Grid?} [layout]
5426
+ * @param {StoreLayout.Grid} layout
5387
5427
  * @param {CellValues} [values]
5388
5428
  * @param {StoreLayout.Grid['cell']?} [defCell]
5389
5429
  * @param {boolean?} [blockOnly]
@@ -5429,7 +5469,7 @@ function createCell(signal, layout, values, defCell, blockOnly) {
5429
5469
  }
5430
5470
  return null;
5431
5471
  }
5432
- const [root, content] = create(layout?.cell) || create(defCell) || createStdCell(signal, values);
5472
+ const [root, content] = create(layout.cell) || create(defCell) || createStdCell(signal, values);
5433
5473
  root.classList.add('NeeloongForm-item');
5434
5474
  effect(() => {
5435
5475
  if (values?.error) {
@@ -5450,7 +5490,7 @@ function createCell(signal, layout, values, defCell, blockOnly) {
5450
5490
 
5451
5491
  /** @import { Store } from '../Store/index.mjs' */
5452
5492
  /** @import { State } from './Tree.mjs' */
5453
- /** @import { StoreLayout } from '../types.mjs' */
5493
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5454
5494
 
5455
5495
  /**
5456
5496
  *
@@ -5458,10 +5498,11 @@ function createCell(signal, layout, values, defCell, blockOnly) {
5458
5498
  * @param {Store<any, any>} store
5459
5499
  * @param {Signal.State<Store<any, any>?>} currentStore
5460
5500
  * @param {StoreLayout.Renderer<T>} fieldRenderer
5461
- * @param {StoreLayout.Field<T>?} layout
5501
+ * @param {StoreLayout.Field<T>} layout
5462
5502
  * @param {Signal.State<State>} state
5463
5503
  * @param {object} option
5464
- * @param {StoreLayout.Column[]} option.columns
5504
+ * @param {{get(): boolean}} option.addable
5505
+ * @param {StoreLayout.Column<T>[]} option.columns
5465
5506
  * @param {() => void} option.remove
5466
5507
  * @param {(el: HTMLElement) => () => void} option.dragenter
5467
5508
  * @param {() => void} option.dragstart
@@ -5475,8 +5516,8 @@ function createCell(signal, layout, values, defCell, blockOnly) {
5475
5516
  */
5476
5517
  function TreeLine(
5477
5518
  store, currentStore, fieldRenderer, layout, state, {
5478
- columns,
5479
- remove, dragenter, dragstart, dragend, deletable, addNode, drop, createDetails,
5519
+ columns, addable, deletable,
5520
+ remove, dragenter, dragstart, dragend, addNode, drop, createDetails,
5480
5521
  }, options) {
5481
5522
  const root = document.createElement('div');
5482
5523
  root.addEventListener('dragstart', (event) => {
@@ -5537,9 +5578,9 @@ function TreeLine(
5537
5578
  }
5538
5579
  close = createDetails(store);
5539
5580
  }
5540
- const moveStart = layout?.mainMethod === 'move' ? pointerdown : null;
5541
- const click = moveStart ? null : layout?.mainMethod === 'collapse' ? switchCollapsed
5542
- : layout?.mainMethod === 'trigger' ? trigger : open;
5581
+ const moveStart = layout.mainMethod === 'move' ? pointerdown : null;
5582
+ const click = moveStart ? null : layout.mainMethod === 'collapse' ? switchCollapsed
5583
+ : layout.mainMethod === 'trigger' ? trigger : open;
5543
5584
 
5544
5585
  const line = root.appendChild(document.createElement('div'));
5545
5586
  line.classList.add('NeeloongForm-tree-line');
@@ -5562,7 +5603,8 @@ function TreeLine(
5562
5603
  dropFront.addEventListener('dragleave', () => dragleave());
5563
5604
  dropChildren.addEventListener('dragleave', () => dragleave());
5564
5605
 
5565
- for (const { actions, pattern, placeholder, width, field } of columns) {
5606
+ for (const column of columns) {
5607
+ const { actions, pattern, placeholder, width, field } = column;
5566
5608
  if (!actions?.length) {
5567
5609
  const td = line.appendChild(document.createElement('div'));
5568
5610
  td.classList.add('NeeloongForm-tree-cell');
@@ -5571,7 +5613,7 @@ function TreeLine(
5571
5613
  if (field) {
5572
5614
  const child = store.child(field);
5573
5615
  if (!child) { continue; }
5574
- const el = FormFieldInline(child, fieldRenderer, null, { ...options, editable: false });
5616
+ const el = FormFieldInline(child, fieldRenderer, column, { ...options, editable: false });
5575
5617
  if (el) { td.appendChild(el); }
5576
5618
  continue;
5577
5619
  }
@@ -5620,7 +5662,7 @@ function TreeLine(
5620
5662
  const move = line.appendChild(document.createElement('button'));
5621
5663
  move.classList.add('NeeloongForm-tree-add');
5622
5664
  move.addEventListener('click', addNode);
5623
- watch(() => store.readonly || store.disabled, disabled => {
5665
+ watch(() => !addable.get(), disabled => {
5624
5666
  move.disabled = disabled;
5625
5667
  }, true, options.signal);
5626
5668
  continue;
@@ -5630,7 +5672,7 @@ function TreeLine(
5630
5672
  const del = line.appendChild(document.createElement('button'));
5631
5673
  del.classList.add('NeeloongForm-tree-remove');
5632
5674
  del.addEventListener('click', remove);
5633
- watch(() => !deletable.get() || store.readonly || store.disabled, disabled => {
5675
+ watch(() => !deletable.get(), disabled => {
5634
5676
  del.disabled = disabled;
5635
5677
  }, true, options.signal);
5636
5678
  continue;
@@ -5670,7 +5712,7 @@ function TreeLine(
5670
5712
  }
5671
5713
 
5672
5714
  /** @import { Store, ArrayStore } from '../Store/index.mjs' */
5673
- /** @import { StoreLayout } from '../types.mjs' */
5715
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5674
5716
 
5675
5717
  const verticalWritingMode = new Set([
5676
5718
  'vertical-lr', 'vertical-rl', 'sideways-lr', 'sideways-rl',
@@ -5779,62 +5821,22 @@ function createState(store, states, drag, levelKey, index) {
5779
5821
  * @template T
5780
5822
  * @param {ArrayStore} store
5781
5823
  * @param {StoreLayout.Renderer<T>} fieldRenderer
5782
- * @param {StoreLayout.Field<T>?} layout
5824
+ * @param {StoreLayout.Field<T>} layout
5783
5825
  * @param {StoreLayout.Options?} options
5784
5826
  * @returns {HTMLElement?}
5785
5827
  */
5786
5828
  function Tree(store, fieldRenderer, layout, options) {
5787
5829
  if (options?.signal?.aborted) { return null; }
5788
- const headerColumns = layout?.columns;
5789
- const fieldList = Object.entries(store.type || {})
5790
- .filter(([k, v]) => typeof v?.type !== 'object')
5791
- .map(([field, { width, label }]) => ({ field, width, label }));
5792
- /** @type {StoreLayout.Column[]} */
5793
- let columns = [];
5794
- if (Array.isArray(headerColumns)) {
5795
- const map = new Map(fieldList.map(v => [v.field, v]));
5796
- /** @type {(StoreLayout.Column | null)[]} */
5797
- const allColumns = headerColumns.map(v => {
5798
- if (!v) { return null; }
5799
- if (typeof v === 'number') { return { placeholder: v }; }
5800
- if (typeof v === 'string') { return map.get(v) || null; }
5801
- if (typeof v !== 'object') { return null; }
5802
- if (Array.isArray(v)) {
5803
- /** @type {Set<StoreLayout.Action>} */
5804
- const options = new Set(['add', 'move', 'trigger', 'remove', 'serial', 'open', 'collapse']);
5805
- const actions = v.filter(v => options.delete(v));
5806
- if (!actions) { return null; }
5807
- return { actions };
5808
- }
5809
- const { action, actions, field, placeholder, pattern, width, label } = v;
5810
- if (field) {
5811
- const define = map.get(field);
5812
- if (define) {
5813
- return { field, placeholder, width, label: label || define.label };
5814
- }
5815
- }
5816
- const options = new Set(['add', 'move', 'trigger', 'remove', 'serial', 'open', 'collapse']);
5817
- const allActions = [action, actions].flat().filter(v => v && options.delete(v));
5818
- if (allActions.length) {
5819
- return { actions: /** @type {StoreLayout.Action[]} */(allActions), width, label };
5820
- }
5821
- if (pattern) {
5822
- return { pattern, placeholder, width, label };
5823
- }
5824
- if (placeholder || width) {
5825
- return { placeholder, width, label };
5826
- }
5827
- return null;
5828
- });
5829
- columns = /** @type {StoreLayout.Column[]} */(allColumns.filter(Boolean));
5830
- }
5831
- if (!columns.length) {
5832
- columns = [
5830
+ const columns = getColumns(
5831
+ store,
5832
+ layout,
5833
+ ['add', 'move', 'trigger', 'remove', 'serial', 'open', 'collapse'],
5834
+ fields => [
5833
5835
  { actions: ['collapse', 'move'] },
5834
- fieldList[0],
5836
+ fields[0],
5835
5837
  { actions: ['add', 'remove'] },
5836
- ];
5837
- }
5838
+ ]
5839
+ );
5838
5840
 
5839
5841
 
5840
5842
  const root = document.createElement('div');
@@ -5951,9 +5953,8 @@ function Tree(store, fieldRenderer, layout, options) {
5951
5953
  }
5952
5954
 
5953
5955
 
5954
- const levelKey = layout?.levelKey || 'level';
5955
- const addable = new Signal.Computed(() => store.addable);
5956
- const deletable = { get: () => Boolean(options?.editable) };
5956
+ const levelKey = layout.levelKey || 'level';
5957
+ const addable = new Signal.Computed(() => !store.readonly && !store.disabled && store.addable);
5957
5958
  /**
5958
5959
  *
5959
5960
  * @param {number} parent
@@ -6126,12 +6127,12 @@ function Tree(store, fieldRenderer, layout, options) {
6126
6127
  const elState = new Signal.State(state);
6127
6128
  const ac = new AbortController();
6128
6129
  const el = TreeLine(child, detailsStore, fieldRenderer, layout, elState, {
6129
- columns,
6130
+ columns, addable,
6131
+ deletable: new Signal.Computed(() => !store.readonly && !store.disabled && child.removable),
6130
6132
  remove: remove.bind(null, child),
6131
6133
  dragenter,
6132
6134
  dragstart: dragstart.bind(null, child),
6133
6135
  dragend,
6134
- deletable,
6135
6136
  addNode: () => addNode(Number(child.index)),
6136
6137
  createDetails,
6137
6138
  drop: drop.bind(null, child),
@@ -6169,7 +6170,7 @@ function Tree(store, fieldRenderer, layout, options) {
6169
6170
  * @param {Store<any, any>} store
6170
6171
  * @param {StoreLayout.Renderer<T>} fieldRenderer
6171
6172
  * @param {StoreLayout.Options?} options
6172
- * @param {StoreLayout.Field<T>?} layout
6173
+ * @param {StoreLayout.Field<T>} layout
6173
6174
  * @returns {ParentNode}
6174
6175
  */
6175
6176
  function Html(html, store, fieldRenderer, options, layout) {
@@ -6183,7 +6184,7 @@ function Html(html, store, fieldRenderer, options, layout) {
6183
6184
  * @param {StoreLayout.Field<T>['arrayStyle']?} arrayStyle
6184
6185
  * @param {ArrayStore} store
6185
6186
  * @param {StoreLayout.Renderer<T>} fieldRenderer
6186
- * @param {StoreLayout.Field<T>?} layout
6187
+ * @param {StoreLayout.Field<T>} layout
6187
6188
  * @param {StoreLayout.Options?} options
6188
6189
  * @returns {HTMLElement?}
6189
6190
  */
@@ -6200,14 +6201,14 @@ function renderArrayCell(arrayStyle, store, fieldRenderer, layout, options) {
6200
6201
  * @template T
6201
6202
  * @param {Store<any, any>} store
6202
6203
  * @param {StoreLayout.Renderer<T>} fieldRenderer
6203
- * @param {StoreLayout.Field<T>?} layout
6204
+ * @param {StoreLayout.Field<T>} layout
6204
6205
  * @param {StoreLayout.Options?} options
6205
6206
  * @returns {ParentNode}
6206
6207
  */
6207
6208
  function FormField(store, fieldRenderer, layout, options) {
6208
6209
  const { type } = store;
6209
6210
  const isObject = Boolean(type && typeof type === 'object');
6210
- const html = layout?.html;
6211
+ const html = layout.html;
6211
6212
  /** @type {StoreLayout.Grid['cell']} */
6212
6213
  const cellType = isObject
6213
6214
  ? store instanceof ArrayStore ? 'collapse' : 'fieldset'
@@ -6218,8 +6219,8 @@ function FormField(store, fieldRenderer, layout, options) {
6218
6219
  /** @type {false | ParentNode | null} */
6219
6220
  const r =
6220
6221
  html && Html(html, store, fieldRenderer, options, layout)
6221
- || fieldRenderer(store, layout?.renderer, options)
6222
- || store instanceof ArrayStore && renderArrayCell(layout?.arrayStyle, store, fieldRenderer, layout, options)
6222
+ || fieldRenderer(store, layout.renderer, options)
6223
+ || store instanceof ArrayStore && renderArrayCell(layout.arrayStyle, store, fieldRenderer, layout, options)
6223
6224
  || isObject && Form(store, fieldRenderer, layout, options);
6224
6225
  if (r) {
6225
6226
  content.appendChild(r);
@@ -6228,7 +6229,7 @@ function FormField(store, fieldRenderer, layout, options) {
6228
6229
  }
6229
6230
 
6230
6231
  /** @import { Store } from '../Store/index.mjs' */
6231
- /** @import { StoreLayout } from '../types.mjs' */
6232
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
6232
6233
 
6233
6234
  /**
6234
6235
  *
@@ -6265,7 +6266,7 @@ function FormButton(store, layout, options) {
6265
6266
  }
6266
6267
 
6267
6268
  /** @import { Store } from '../Store/index.mjs' */
6268
- /** @import { StoreLayout } from '../types.mjs' */
6269
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
6269
6270
 
6270
6271
  /**
6271
6272
  *
@@ -6287,7 +6288,7 @@ function FormHtml(store, fieldRenderer, layout, options) {
6287
6288
  }
6288
6289
 
6289
6290
  /** @import { Store } from '../Store/index.mjs' */
6290
- /** @import { StoreLayout } from '../types.mjs' */
6291
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
6291
6292
 
6292
6293
  /**
6293
6294
  *
@@ -6305,7 +6306,9 @@ function FormItem(store, fieldRenderer, item, options) {
6305
6306
  if (item.type === 'html') {
6306
6307
  return FormHtml(store, fieldRenderer, item, options);
6307
6308
  }
6308
- const fieldStore = store.child(item.field);
6309
+ const field = item.field;
6310
+ if (!field) { return null; }
6311
+ const fieldStore = store.child(field);
6309
6312
  if (fieldStore) {
6310
6313
  return FormField(fieldStore, fieldRenderer, item, options);
6311
6314
  }
@@ -6317,7 +6320,7 @@ function FormItem(store, fieldRenderer, item, options) {
6317
6320
  * @template T
6318
6321
  * @param {Store<any, any>} store
6319
6322
  * @param {StoreLayout.Renderer<T>} fieldRenderer
6320
- * @param {StoreLayout<T>?} layout
6323
+ * @param {StoreLayout<T>} layout
6321
6324
  * @param {StoreLayout.Options?} options
6322
6325
  * @param {HTMLElement} [parent]
6323
6326
  * @returns {HTMLElement?}
@@ -6326,8 +6329,8 @@ function Form(store, fieldRenderer, layout, options, parent) {
6326
6329
  if (options?.signal?.aborted) { return null; }
6327
6330
  const root = parent instanceof HTMLElement ? parent : document.createElement('div');
6328
6331
  root.classList.add('NeeloongForm');
6329
- const fieldLayouts = layout?.fields;
6330
- if (fieldLayouts) {
6332
+ const fieldLayouts = layout.fields || store.layout.fields;
6333
+ if (fieldLayouts?.length) {
6331
6334
  for (const fieldTemplate of fieldLayouts) {
6332
6335
  const el = FormItem(store, fieldRenderer, fieldTemplate, options);
6333
6336
  if (el) { root.appendChild(el); }
@@ -6335,7 +6338,7 @@ function Form(store, fieldRenderer, layout, options, parent) {
6335
6338
  } else {
6336
6339
  const fields = [...store].map(([, v]) => v);
6337
6340
  for (const field of fields) {
6338
- const el = FormField(field, fieldRenderer, null, options);
6341
+ const el = FormField(field, fieldRenderer, field.layout, options);
6339
6342
  if (el) { root.appendChild(el); }
6340
6343
  }
6341
6344
  }
@@ -6344,7 +6347,7 @@ function Form(store, fieldRenderer, layout, options, parent) {
6344
6347
  }
6345
6348
 
6346
6349
  /** @import { Store } from '../Store/index.mjs' */
6347
- /** @import { StoreLayout } from '../types.mjs' */
6350
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
6348
6351
 
6349
6352
  /**
6350
6353
  *
@@ -6352,19 +6355,20 @@ function Form(store, fieldRenderer, layout, options, parent) {
6352
6355
  * @param {Store} store
6353
6356
  * @param {StoreLayout.Renderer<T>} fieldRenderer
6354
6357
  * @param {HTMLElement} root
6355
- * @param {StoreLayout<T>?} [layout]
6358
+ * @param {StoreLayout<T>} layout
6356
6359
  * @param {StoreLayout.Options & {clone?: boolean} | null} [options]
6357
6360
  * @returns {void}
6358
6361
  */
6359
6362
  function renderStore(store, fieldRenderer, root, layout, options) {
6360
6363
  if (options?.signal?.aborted) { return; }
6361
- const html = layout?.html;
6364
+ const storeLayout = layout || store.layout;
6365
+ const html = storeLayout.html;
6362
6366
  if (!html) {
6363
- Form(store, fieldRenderer, layout || null, options || null, root);
6367
+ Form(store, fieldRenderer, storeLayout, options || null, root);
6364
6368
  return;
6365
6369
  }
6366
6370
  const content = getHtmlContent(html);
6367
- renderHtml(store, fieldRenderer, content, options || null, layout);
6371
+ renderHtml(store, fieldRenderer, content, options || null, storeLayout);
6368
6372
  root.appendChild(content);
6369
6373
  options?.signal?.addEventListener('abort', () => {
6370
6374
  root.removeChild(content);