@neeloong/form 0.22.0 → 0.24.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.22.0
2
+ * @neeloong/form v0.24.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); }
@@ -933,7 +938,7 @@ class Store {
933
938
  }
934
939
  }
935
940
 
936
- /** @import { Schema } from '../types.mjs' */
941
+ /** @import { Schema } from '../Schema.types.mjs' */
937
942
 
938
943
  /**
939
944
  * @template {Record<string, any>} [T=Record<string, any>]
@@ -997,7 +1002,7 @@ class ObjectStore extends Store {
997
1002
  // @ts-ignore
998
1003
  setObjectStore(ObjectStore);
999
1004
 
1000
- /** @import { Schema } from '../types.mjs' */
1005
+ /** @import { Schema } from '../Schema.types.mjs' */
1001
1006
 
1002
1007
 
1003
1008
 
@@ -1051,7 +1056,7 @@ class ArrayStore extends Store {
1051
1056
  }
1052
1057
 
1053
1058
  };
1054
- super(schema, {
1059
+ super({ ...schema, immutable: false }, {
1055
1060
  index, new: isNew, parent,
1056
1061
  size: new Signal.Computed(() => childrenState.get().length),
1057
1062
  setValue(v) {
@@ -1088,7 +1093,10 @@ class ArrayStore extends Store {
1088
1093
  },
1089
1094
  };
1090
1095
  this.#create = (index, isNew) => {
1091
- const child = create(schema, { ...childCommonOptions, index, new: isNew });
1096
+ const child = create(
1097
+ { ...schema, creatable: true },
1098
+ { ...childCommonOptions, index, new: isNew },
1099
+ );
1092
1100
  child.index = index;
1093
1101
  return child;
1094
1102
  };
@@ -2314,13 +2322,36 @@ var index = /*#__PURE__*/Object.freeze({
2314
2322
 
2315
2323
  /**
2316
2324
  * 相应式执行
2325
+ * @overload
2317
2326
  * @param {() => void} fn 执行函数
2327
+ * @param {null} [signal]
2318
2328
  * @returns {() => void} 取消函数
2319
2329
  */
2320
- function effect(fn) {
2330
+ /**
2331
+ * 相应式执行
2332
+ * @overload
2333
+ * @param {() => void} fn 执行函数
2334
+ * @param {AbortSignal} signal
2335
+ * @returns {void} 取消函数
2336
+ */
2337
+ /**
2338
+ * 相应式执行
2339
+ * @overload
2340
+ * @param {() => void} fn 执行函数
2341
+ * @param {AbortSignal?} [signal]
2342
+ * @returns {(() => void) | void} 取消函数
2343
+ */
2344
+ /**
2345
+ * 相应式执行
2346
+ * @param {() => void} fn 执行函数
2347
+ * @param {AbortSignal?} [signal]
2348
+ * @returns {(() => void) | void} 取消函数
2349
+ */
2350
+ function effect(fn, signal) {
2351
+ if (signal?.aborted) { return; }
2321
2352
  let needsEnqueue = true;
2322
2353
  const w = new Signal.subtle.Watcher(() => {
2323
- if (!needsEnqueue) { return }
2354
+ if (!needsEnqueue) { return; }
2324
2355
  needsEnqueue = false;
2325
2356
  queueMicrotask(() => {
2326
2357
  needsEnqueue = true;
@@ -2334,19 +2365,54 @@ function effect(fn) {
2334
2365
 
2335
2366
  w.watch(computed);
2336
2367
  computed.get();
2368
+ if (!signal) { return () => { w.unwatch(computed); }; }
2369
+ signal.addEventListener('abort', () => {
2370
+ w.unwatch(computed);
2371
+ }, { once: true });
2337
2372
 
2338
- return () => { w.unwatch(computed); };
2339
2373
  }
2340
2374
 
2341
2375
  /**
2342
2376
  * 创建可赋值计算值
2343
2377
  * @template T
2378
+ * @overload
2344
2379
  * @param {() => T} getter 取值方法
2345
2380
  * @param {(value: T) => void} callback 取值方法
2346
2381
  * @param {boolean} immediate 是否立即执行一次
2382
+ * @param {null} [signal]
2347
2383
  * @returns {() => void}
2348
2384
  */
2349
- function watch(getter, callback, immediate) {
2385
+
2386
+ /**
2387
+ * 创建可赋值计算值
2388
+ * @template T
2389
+ * @overload
2390
+ * @param {() => T} getter 取值方法
2391
+ * @param {(value: T) => void} callback 取值方法
2392
+ * @param {boolean} immediate 是否立即执行一次
2393
+ * @param {AbortSignal?} [signal]
2394
+ * @returns {(() => void) | void}
2395
+ */
2396
+ /**
2397
+ * 创建可赋值计算值
2398
+ * @template T
2399
+ * @overload
2400
+ * @param {() => T} getter 取值方法
2401
+ * @param {(value: T) => void} callback 取值方法
2402
+ * @param {boolean} immediate 是否立即执行一次
2403
+ * @param {AbortSignal} signal
2404
+ * @returns {void}
2405
+ */
2406
+ /**
2407
+ * 创建可赋值计算值
2408
+ * @template T
2409
+ * @param {() => T} getter 取值方法
2410
+ * @param {(value: T) => void} callback 取值方法
2411
+ * @param {boolean} immediate 是否立即执行一次
2412
+ * @param {AbortSignal?} [signal]
2413
+ * @returns {(() => void) | void}
2414
+ */
2415
+ function watch(getter, callback, immediate, signal) {
2350
2416
  let run = false;
2351
2417
  /** @type {any} */
2352
2418
  let value;
@@ -2361,7 +2427,7 @@ function watch(getter, callback, immediate) {
2361
2427
  if (Object.is(val, value)) { return; }
2362
2428
  value = val;
2363
2429
  callback(val);
2364
- });
2430
+ }, signal);
2365
2431
  }
2366
2432
 
2367
2433
  const bindable = {
@@ -3428,8 +3494,7 @@ class EventEmitter {
3428
3494
  }
3429
3495
  }
3430
3496
 
3431
- /** @import { Component, Relatedness } from '../types.mjs' */
3432
- /** @import Store from '../Store/index.mjs' */
3497
+ /** @import { Component } from '../types.mjs' */
3433
3498
  /** @import Environment from './Environment/index.mjs' */
3434
3499
  /** @import * as Layout from '../Layout/index.mjs' */
3435
3500
 
@@ -3599,7 +3664,8 @@ function bindFilters(env, fn, filterFns) {
3599
3664
  }
3600
3665
  }
3601
3666
 
3602
- /** @import { Component, Relatedness } from '../types.mjs' */
3667
+ /** @import { Component } from '../types.mjs' */
3668
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
3603
3669
  /** @import { ComponentHandler } from './types.mjs' */
3604
3670
  /** @import Store from '../Store/index.mjs' */
3605
3671
  /** @import Environment from './Environment/index.mjs' */
@@ -3610,7 +3676,7 @@ function bindFilters(env, fn, filterFns) {
3610
3676
  * @param {Component | string} component
3611
3677
  * @param {Environment} env
3612
3678
  * @param {Store?} store
3613
- * @param {((store: Store, el: Element | Relatedness) => () => void)?} [relate]
3679
+ * @param {((store: Store, el: Element | StoreLayout.Relatedness) => () => void)?} [relate]
3614
3680
  * @returns
3615
3681
  */
3616
3682
  function createContext(component, env, store, relate) {
@@ -4488,7 +4554,7 @@ function divergent(layout, parent, next, env, renderItem) {
4488
4554
  * @param {Record<string, [Layout.Template, Environment]>} templates
4489
4555
  * @param {string[]} componentPath
4490
4556
  * @param {Record<string, Enhancement>} enhancements
4491
- * @param {((store: Store, el: Element | Relatedness) => () => void)?} [relate]
4557
+ * @param {((store: Store, el: Element | StoreLayout.Relatedness) => () => void)?} [relate]
4492
4558
  * @param {Component.Getter?} [getComponent]
4493
4559
  */
4494
4560
  function renderNode(layout, parent, next, env, templates, componentPath, enhancements, relate, getComponent) {
@@ -4570,7 +4636,7 @@ function createTemplates(env, parentTemplates, newTemplates) {
4570
4636
  * @param {Record<string, [Layout.Template, Environment]>} parentTemplates
4571
4637
  * @param {string[]} componentPath
4572
4638
  * @param {Record<string, Enhancement>} enhancements
4573
- * @param {((store: Store, el: Element | Relatedness) => () => void)?} [relate]
4639
+ * @param {((store: Store, el: Element | StoreLayout.Relatedness) => () => void)?} [relate]
4574
4640
  * @param {Component.Getter?} [getComponent]
4575
4641
  * @returns {() => void}
4576
4642
  */
@@ -4635,7 +4701,7 @@ function renderChild(layout, parent, next, parentEnv, parentTemplates, component
4635
4701
  * @param {Record<string, [Layout.Template, Environment]>} templates
4636
4702
  * @param {string[]} componentPath
4637
4703
  * @param {Record<string, Enhancement>} enhancements
4638
- * @param {((store: Store, el: Element | Relatedness) => () => void)?} [relate]
4704
+ * @param {((store: Store, el: Element | StoreLayout.Relatedness) => () => void)?} [relate]
4639
4705
  * @param {Component.Getter?} [getComponent]
4640
4706
  * @returns {() => void}
4641
4707
  */
@@ -4658,7 +4724,7 @@ function renderChildren(layouts, parent, next, env, templates, componentPath, en
4658
4724
  * @param {object} [options] 选项
4659
4725
  * @param {Record<string, Store | {get?(): any; set?(v: any): void; exec?(...p: any[]): any; calc?(...p: any[]): any }>} [options.global] 全局数据
4660
4726
  * @param {Component.Getter?} [options.component] 自定义组件
4661
- * @param {(store: Store, el: Element | Relatedness) => () => void} [options.relate] 关联函数
4727
+ * @param {(store: Store, el: Element | StoreLayout.Relatedness) => () => void} [options.relate] 关联函数
4662
4728
  * @param {Record<string, Enhancement>} [options.enhancements] 增强信息
4663
4729
  * @returns {() => void}
4664
4730
  */
@@ -4671,69 +4737,69 @@ function render(store, layouts, parent, { component, global, relate, enhancement
4671
4737
  }
4672
4738
 
4673
4739
  /** @import { Store } from '../Store/index.mjs' */
4674
- /** @import { StoreLayout } from '../types.mjs' */
4675
-
4740
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
4676
4741
 
4677
4742
  /**
4678
4743
  *
4744
+ * @template T
4679
4745
  * @param {string} field
4746
+ * @returns {(v: StoreLayout.Item<T>) => v is StoreLayout.Field<T>}
4680
4747
  */
4681
4748
  function createFieldFilter(field) {
4682
-
4683
4749
  /**
4684
4750
  *
4685
- * @param {StoreLayout.Item} v
4686
- * @returns {v is StoreLayout.Field}
4751
+ * @param {StoreLayout.Item<T>} v
4752
+ * @returns {v is StoreLayout.Field<T>}
4687
4753
  */
4688
-
4689
- return function (v) {
4754
+ return v => {
4690
4755
  if (v.type && v.type !== 'field') { return false; }
4691
4756
  return v.field === field;
4692
4757
 
4693
4758
  };
4694
4759
  }
4695
4760
  /**
4696
- *
4697
- * @param {StoreLayout.Renderer} fieldRenderer
4761
+ * @template T
4762
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
4698
4763
  * @param {Store} store
4699
4764
  * @param {Node} node
4700
4765
  * @param {StoreLayout.Options?} options
4701
- * @param {StoreLayout?} [layout]
4766
+ * @param {StoreLayout<T>} layout
4702
4767
  * @param {Node} [anchor]
4703
4768
  * @param {(child?: Store<any, any> | undefined) => void} [dragenter]
4769
+ * @returns {void}
4704
4770
  */
4705
4771
  function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragenter) {
4706
- /** @type {(() => void)[]} */
4707
- const destroyList = [];
4772
+ if (options?.signal?.aborted) { return; }
4708
4773
  if (node instanceof Element) {
4709
4774
  const tagName = node.tagName.toLowerCase();
4710
- if (!node.parentNode) { return () => { }; }
4775
+ if (!node.parentNode) { return; }
4711
4776
  if (tagName === 'nl-form-field') {
4712
4777
  const field = node.getAttribute('name') || '';
4713
4778
  const mode = node.getAttribute('mode') || '';
4714
4779
  const fieldStore = field ? store.child(field) : store;
4715
- const fieldLayout = field ? layout?.fields?.find(createFieldFilter(field)) || null : null;
4716
- if (!fieldStore) { return () => { }; }
4780
+ if (!fieldStore) { return; }
4781
+ const fieldLayout = field
4782
+ ? layout?.fields?.find(createFieldFilter(field)) || fieldStore.layout
4783
+ : { ...layout, html: '' };
4717
4784
  switch (mode) {
4718
4785
  case 'grid': {
4719
- const [el, destroy] = Form(store, fieldRenderer, fieldLayout, options);
4720
- node.replaceWith(el);
4721
- return destroy;
4786
+ const el = Form(store, fieldRenderer, fieldLayout, options);
4787
+ if (el) { node.replaceWith(el); }
4788
+ return;
4722
4789
  }
4723
4790
  }
4724
- const res = fieldRenderer(fieldStore, options);
4791
+ const res = fieldRenderer(fieldStore, layout.renderer, options);
4725
4792
  if (res) {
4726
- const [el, destroy] = res;
4727
- node.replaceWith(el);
4728
- return destroy;
4793
+ node.replaceWith(res);
4794
+ return;
4729
4795
  }
4730
4796
  const value = node.getAttribute('placeholder') || '';
4731
4797
  node.replaceWith(document.createTextNode(value));
4732
- return () => { };
4798
+ return;
4733
4799
  }
4734
4800
  if (tagName === 'nl-form-button') {
4735
4801
  const button = document.createElement('button');
4736
- button.className = 'NeeloongForm-item-button';
4802
+ button.classList.add('NeeloongForm-item-button');
4737
4803
  const click = node.getAttribute('click') || '';
4738
4804
  const call = options?.call;
4739
4805
  if (click && typeof call === 'function') {
@@ -4743,7 +4809,7 @@ function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragent
4743
4809
  button.appendChild(n);
4744
4810
  }
4745
4811
  node.replaceWith(button);
4746
- return () => { };
4812
+ return;
4747
4813
  }
4748
4814
  const name = node.getAttribute('nl-form-field');
4749
4815
  if (name) {
@@ -4752,32 +4818,37 @@ function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragent
4752
4818
 
4753
4819
  const fieldStore = field ? store.child(field) : store;
4754
4820
  if (!fieldStore) {
4755
- return () => { };
4821
+ return;
4756
4822
  }
4757
4823
  node.removeAttribute('nl-form-field');
4758
- const fieldLayout = field ? layout?.fields?.find(createFieldFilter(field)) : layout;
4759
- if (!array) { return renderHtml(fieldStore, fieldRenderer, node, options, fieldLayout, anchor); }
4824
+ const fieldLayout = field
4825
+ ? layout.fields?.find(createFieldFilter(field)) || fieldStore.layout
4826
+ : { ...layout, html: '' };
4827
+ if (!array) {
4828
+ renderHtml(fieldStore, fieldRenderer, node, options, fieldLayout, anchor);
4829
+ return;
4830
+ }
4760
4831
  if (!(fieldStore instanceof ArrayStore)) {
4761
4832
  node.remove();
4762
- return () => { };
4833
+ return;
4763
4834
  }
4764
4835
  const parentElement = node.parentElement;
4765
4836
  if (!parentElement) {
4766
4837
  node.remove();
4767
- return () => { };
4838
+ return;
4768
4839
  }
4769
4840
  const comment = parentElement.insertBefore(document.createComment(''), node) || null;
4770
4841
  node.remove();
4771
4842
 
4772
- /** @type {Map<Store, [Node, () => void]>} */
4843
+ /** @type {Map<Store, [Node, AbortController]>} */
4773
4844
  let seMap = new Map();
4774
- /** @param {Map<Store, [tbody: Node, destroy: () => void]>} map */
4845
+ /** @param {Map<Store, [Node, AbortController]>} map */
4775
4846
  function destroyMap(map) {
4776
- for (const [el, destroy] of map.values()) {
4777
- destroy();
4847
+ for (const [el, ac] of map.values()) {
4778
4848
  if (el instanceof Element) {
4779
4849
  el.remove();
4780
4850
  }
4851
+ ac.abort();
4781
4852
  }
4782
4853
  }
4783
4854
  let dragRow = -1;
@@ -4794,22 +4865,26 @@ function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragent
4794
4865
  }
4795
4866
  };
4796
4867
 
4797
- const childrenResult = watch(() => fieldStore.children, children => {
4868
+ watch(() => fieldStore.children, children => {
4798
4869
  let nextNode = comment.nextSibling;
4799
4870
  const oldSeMap = seMap;
4800
4871
  seMap = new Map();
4801
4872
  for (const child of children) {
4802
4873
  const old = oldSeMap.get(child);
4803
4874
  if (!old) {
4875
+ const ac = new AbortController();
4804
4876
  const el = parentElement.insertBefore(node.cloneNode(true), nextNode);
4805
- const d = renderHtml(child, fieldRenderer, el, options, fieldLayout, el);
4877
+ renderHtml(child, fieldRenderer, el, {
4878
+ ...options,
4879
+ signal: options?.signal ? AbortSignal.any([options?.signal, ac.signal]) : ac.signal,
4880
+ }, fieldLayout, el);
4806
4881
  el.addEventListener('dragenter', () => { newDragenter(child); });
4807
4882
  el.addEventListener('dragstart', (event) => {
4808
4883
  if (event.target !== event.currentTarget) { return; }
4809
4884
  dragRow = Number(child.index);
4810
4885
  });
4811
4886
  el.addEventListener('dragend', () => { dragRow = -1; });
4812
- seMap.set(child, [el, d]);
4887
+ seMap.set(child, [el, ac]);
4813
4888
  continue;
4814
4889
  }
4815
4890
  oldSeMap.delete(child);
@@ -4821,13 +4896,13 @@ function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragent
4821
4896
  parentElement.insertBefore(old[0], nextNode);
4822
4897
  }
4823
4898
  destroyMap(oldSeMap);
4824
- }, true);
4899
+ }, true, options?.signal);
4825
4900
 
4826
- return () => {
4901
+ options?.signal?.addEventListener('abort', () => {
4827
4902
  comment.remove();
4828
4903
  destroyMap(seMap);
4829
- childrenResult();
4830
- };
4904
+ }, { once: true });
4905
+ return;
4831
4906
 
4832
4907
  }
4833
4908
  if (node.getAttribute('nl-form-remove') !== null) {
@@ -4870,11 +4945,11 @@ function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragent
4870
4945
  const field = node.getAttribute(attr);
4871
4946
  const fieldStore = field ? store.child(field) : store;
4872
4947
  if (!fieldStore) { continue; }
4873
- destroyList.push(effect(() => {
4948
+ effect(() => {
4874
4949
  try {
4875
4950
  node.setAttribute(name, fieldStore.value);
4876
4951
  } catch { }
4877
- }));
4952
+ }, options?.signal);
4878
4953
  } else if (prefix === 'nl-form-event' && typeof call === 'function') {
4879
4954
  const event = node.getAttribute(attr);
4880
4955
  if (!event) { continue; }
@@ -4886,15 +4961,9 @@ function renderHtml(store, fieldRenderer, node, options, layout, anchor, dragent
4886
4961
  }
4887
4962
  if (node instanceof Element || node instanceof DocumentFragment) {
4888
4963
  for (const n of [...node.children]) {
4889
- destroyList.push(renderHtml(store, fieldRenderer, n, options, layout, anchor));
4964
+ renderHtml(store, fieldRenderer, n, options, layout, anchor);
4890
4965
  }
4891
4966
  }
4892
- return () => {
4893
- for (const destroy of destroyList) {
4894
- destroy();
4895
- }
4896
- };
4897
-
4898
4967
  }
4899
4968
 
4900
4969
  /**
@@ -4916,38 +4985,46 @@ function getHtmlContent(html) {
4916
4985
 
4917
4986
  /**
4918
4987
  *
4988
+ * @template T
4919
4989
  * @param {Store<any, any>} store
4920
- * @param {StoreLayout.Renderer} fieldRenderer
4921
- * @param {StoreLayout.Field?} layout
4990
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
4991
+ * @param {StoreLayout<T>} layout
4922
4992
  * @param {StoreLayout.Options?} options
4923
- * @returns {[ParentNode, () => void]}
4993
+ * @returns {ParentNode?}
4924
4994
  */
4925
4995
  function FormFieldInline(store, fieldRenderer, layout, options) {
4926
- return fieldRenderer(store, options) || [document.createElement('div'), () => { }];
4996
+ if (options?.signal?.aborted) { return null; }
4997
+ const html = layout.html;
4998
+ if (html) {
4999
+ const content = getHtmlContent(html);
5000
+ renderHtml(store, fieldRenderer, content, options, layout);
5001
+ return content;
5002
+ }
5003
+ return fieldRenderer(store, layout.renderer, options);
4927
5004
  }
4928
5005
 
4929
5006
  /** @import { Store } from '../Store/index.mjs' */
4930
- /** @import { StoreLayout } from '../types.mjs' */
5007
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
4931
5008
 
4932
5009
  /**
4933
5010
  *
5011
+ * @template T
4934
5012
  * @param {Store<any, any>} store
4935
- * @param {StoreLayout.Renderer} fieldRenderer
4936
- * @param {StoreLayout.Field?} layout
5013
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
5014
+ * @param {StoreLayout.Field<T>} layout
4937
5015
  * @param {object} option
4938
- * @param {StoreLayout.Column[]} option.columns
5016
+ * @param {StoreLayout.Column<T>[]} option.columns
4939
5017
  * @param {() => void} option.remove
4940
5018
  * @param {() => void} option.dragenter
4941
5019
  * @param {() => void} option.dragstart
4942
5020
  * @param {() => void} option.dragend
4943
5021
  * @param {{get(): boolean}} option.deletable
4944
5022
  * @param {StoreLayout.Options?} options
4945
-
4946
- * @returns {[HTMLTableSectionElement, () => void]}
5023
+ * @returns {HTMLTableSectionElement}
4947
5024
  */
4948
5025
  function Line(store, fieldRenderer, layout, {
4949
- columns,
4950
- remove, dragenter, dragstart, dragend, deletable
5026
+ columns, deletable,
5027
+ remove, dragenter, dragstart, dragend,
4951
5028
  }, options) {
4952
5029
  const root = document.createElement('tbody');
4953
5030
  root.addEventListener('dragenter', () => {
@@ -4960,38 +5037,34 @@ function Line(store, fieldRenderer, layout, {
4960
5037
  root.addEventListener('dragend', dragend);
4961
5038
  const head = root.appendChild(document.createElement('tr'));
4962
5039
 
4963
- /** @type {(() => void)[]} */
4964
- const destroyList = [];
4965
5040
 
4966
5041
 
4967
5042
  let trigger = () => { };
4968
5043
  /** @type {HTMLButtonElement[]} */
4969
5044
  const triggerList = [];
4970
5045
  if (columns.find(v => v.actions?.includes('trigger'))) {
4971
- const body = root.appendChild(document.createElement('tr'));
4972
- const main = body.appendChild(document.createElement('td'));
4973
- main.colSpan = columns.length;
4974
-
4975
- const [form, destroy] = Form(store, fieldRenderer, layout, options);
4976
- main.appendChild(form);
4977
- destroyList.push(destroy);
4978
- body.hidden = true;
4979
- trigger = function click() {
4980
- if (body.hidden) {
4981
- body.hidden = false;
4982
- for (const ext of triggerList) {
4983
- ext.classList.remove('NeeloongForm-table-line-open');
4984
- ext.classList.add('NeeloongForm-table-line-close');
4985
- }
4986
- } else {
4987
- body.hidden = true;
4988
- for (const ext of triggerList) {
4989
- ext.classList.remove('NeeloongForm-table-line-close');
4990
- ext.classList.add('NeeloongForm-table-line-open');
5046
+ const form = Form(store, fieldRenderer, layout, options);
5047
+ if (form) {
5048
+ const body = root.appendChild(document.createElement('tr'));
5049
+ const main = body.appendChild(document.createElement('td'));
5050
+ main.colSpan = columns.length;
5051
+ body.hidden = true;
5052
+ trigger = () => {
5053
+ if (body.hidden) {
5054
+ body.hidden = false;
5055
+ for (const ext of triggerList) {
5056
+ ext.classList.remove('NeeloongForm-table-line-open');
5057
+ ext.classList.add('NeeloongForm-table-line-close');
5058
+ }
5059
+ } else {
5060
+ body.hidden = true;
5061
+ for (const ext of triggerList) {
5062
+ ext.classList.remove('NeeloongForm-table-line-close');
5063
+ ext.classList.add('NeeloongForm-table-line-open');
5064
+ }
4991
5065
  }
4992
- }
4993
- };
4994
-
5066
+ };
5067
+ }
4995
5068
  }
4996
5069
 
4997
5070
  /**
@@ -5013,15 +5086,14 @@ function Line(store, fieldRenderer, layout, {
5013
5086
 
5014
5087
  }
5015
5088
 
5016
- for (const name of columns) {
5017
- const { actions, field, pattern } = name;
5089
+ for (const column of columns) {
5090
+ const { actions, field, pattern } = column;
5018
5091
  if (!actions?.length) {
5019
5092
  const td = head.appendChild(document.createElement('td'));
5020
5093
  const child = field && store.child(field);
5021
5094
  if (child) {
5022
- const [el, destroy] = FormFieldInline(child, fieldRenderer, null, options);
5023
- destroyList.push(destroy);
5024
- td.appendChild(el);
5095
+ const el = FormFieldInline(child, fieldRenderer, column, options);
5096
+ if (el) { td.appendChild(el); }
5025
5097
  }
5026
5098
  continue;
5027
5099
  }
@@ -5041,9 +5113,9 @@ function Line(store, fieldRenderer, layout, {
5041
5113
  const move = handle.appendChild(document.createElement('button'));
5042
5114
  move.classList.add('NeeloongForm-table-move');
5043
5115
  move.addEventListener('pointerdown', pointerdown);
5044
- destroyList.push(watch(() => store.readonly || store.disabled, disabled => {
5116
+ watch(() => store.readonly || store.disabled, disabled => {
5045
5117
  move.disabled = disabled;
5046
- }, true));
5118
+ }, true, options.signal);
5047
5119
  continue;
5048
5120
  }
5049
5121
  case 'remove': {
@@ -5051,9 +5123,9 @@ function Line(store, fieldRenderer, layout, {
5051
5123
  const del = handle.appendChild(document.createElement('button'));
5052
5124
  del.classList.add('NeeloongForm-table-remove');
5053
5125
  del.addEventListener('click', remove);
5054
- destroyList.push(watch(() => !deletable.get() || store.readonly || store.disabled, disabled => {
5126
+ watch(() => !deletable.get(), disabled => {
5055
5127
  del.disabled = disabled;
5056
- }, true));
5128
+ }, true, options.signal);
5057
5129
  continue;
5058
5130
  }
5059
5131
  case 'serial': {
@@ -5065,28 +5137,25 @@ function Line(store, fieldRenderer, layout, {
5065
5137
  }
5066
5138
  }
5067
5139
 
5068
- return [root, () => {
5069
- for (const destroy of destroyList) {
5070
- destroy();
5071
- }
5072
- }];
5140
+ return root;
5073
5141
  }
5074
5142
 
5075
5143
  /** @import { Store, ArrayStore } from '../Store/index.mjs' */
5076
- /** @import { StoreLayout } from '../types.mjs' */
5144
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5077
5145
 
5078
5146
  /**
5079
5147
  *
5148
+ * @template T
5149
+ * @param {AbortSignal | null | undefined} signal
5080
5150
  * @param {HTMLElement} parent
5081
- * @param {StoreLayout.Column[]} columns
5151
+ * @param {StoreLayout.Column<T>[]} columns
5082
5152
  * @param {() => any} add
5083
5153
  * @param {{get(): boolean}} addable
5084
5154
  * @param {boolean?} [editable]
5155
+ * @returns {void}
5085
5156
  */
5086
- function renderHead(parent, columns, add, addable, editable) {
5157
+ function renderHead(signal, parent, columns, add, addable, editable) {
5087
5158
  const tr = parent.appendChild(document.createElement('tr'));
5088
- /** @type {(() => void)[]} */
5089
- const destroyList = [];
5090
5159
  for (const { action, actions, width, label } of columns) {
5091
5160
  const th = tr.appendChild(document.createElement('th'));
5092
5161
  if (width) { th.setAttribute('width', `${width}`); }
@@ -5098,33 +5167,30 @@ function renderHead(parent, columns, add, addable, editable) {
5098
5167
  const button = th.appendChild(document.createElement('button'));
5099
5168
  button.addEventListener('click', add);
5100
5169
  button.classList.add('NeeloongForm-table-add');
5101
- destroyList.push(watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true));
5170
+ watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true, signal);
5102
5171
  }
5103
- return () => {
5104
- for (const destroy of destroyList) {
5105
- destroy();
5106
- }
5107
- };
5108
5172
  }
5109
5173
  /**
5110
5174
  *
5175
+ * @template T
5111
5176
  * @param {ArrayStore} store
5112
- * @param {StoreLayout.Renderer} fieldRenderer
5113
- * @param {StoreLayout.Field?} layout
5177
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
5178
+ * @param {StoreLayout.Field<T>} layout
5114
5179
  * @param {StoreLayout.Options?} options
5115
- * @returns {[HTMLTableElement, () => void]}
5180
+ * @returns {HTMLTableElement?}
5116
5181
  */
5117
5182
  function Table(store, fieldRenderer, layout, options) {
5118
- const headerColumns = layout?.columns;
5183
+ if (options?.signal?.aborted) { return null; }
5184
+ const headerColumns = layout.columns;
5119
5185
  const fieldList = Object.entries(store.type || {})
5120
5186
  .filter(([k, v]) => typeof v?.type !== 'object')
5121
5187
  .map(([field, { width, label }]) => ({ field, width, label }));
5122
- /** @type {StoreLayout.Column[]} */
5188
+ /** @type {StoreLayout.Column<T>[]} */
5123
5189
  let columns = [];
5124
5190
  if (Array.isArray(headerColumns)) {
5125
5191
  const map = new Map(fieldList.map(v => [v.field, v]));
5126
5192
 
5127
- /** @type {(StoreLayout.Column | null)[]} */
5193
+ /** @type {(StoreLayout.Column<T> | null)[]} */
5128
5194
  const allColumns = headerColumns.map(v => {
5129
5195
  if (!v) { return null; }
5130
5196
  if (typeof v === 'number') { return { placeholder: v }; }
@@ -5154,7 +5220,7 @@ function Table(store, fieldRenderer, layout, options) {
5154
5220
  // }
5155
5221
  return null;
5156
5222
  });
5157
- columns = /** @type {StoreLayout.Column[]} */(allColumns.filter(Boolean));
5223
+ columns = /** @type {StoreLayout.Column<T>[]} */(allColumns.filter(Boolean));
5158
5224
 
5159
5225
  }
5160
5226
  if (!columns.length) {
@@ -5165,11 +5231,10 @@ function Table(store, fieldRenderer, layout, options) {
5165
5231
  }
5166
5232
 
5167
5233
  const table = document.createElement('table');
5168
- table.className = 'NeeloongForm-table';
5234
+ table.classList.add('NeeloongForm-table');
5169
5235
  const thead = table.appendChild(document.createElement('thead'));
5170
5236
 
5171
- const addable = new Signal.Computed(() => store.addable);
5172
- const deletable = { get: () => Boolean(options?.editable) };
5237
+ const addable = new Signal.Computed(() => !store.readonly && !store.disabled && store.addable);
5173
5238
  function add() {
5174
5239
  const data = {};
5175
5240
  store.add(data);
@@ -5206,15 +5271,13 @@ function Table(store, fieldRenderer, layout, options) {
5206
5271
  dragRow = -1;
5207
5272
 
5208
5273
  }
5209
- /** @type {(() => void)[]} */
5210
- const destroyList = [];
5211
- destroyList.push(renderHead(thead, columns, add, addable, Boolean(options?.editable)));
5212
- switch (layout?.tableFoot) {
5274
+ renderHead(options?.signal, thead, columns, add, addable, Boolean(options?.editable));
5275
+ switch (layout.tableFoot) {
5213
5276
  default:
5214
5277
  case 'header': {
5215
5278
  const tfoot = table.appendChild(document.createElement('tfoot'));
5216
5279
  tfoot.addEventListener('dragenter', () => { dragenter(); });
5217
- destroyList.push(renderHead(tfoot, columns, add, addable, Boolean(options?.editable)));
5280
+ renderHead(options?.signal, tfoot, columns, add, addable, Boolean(options?.editable));
5218
5281
  break;
5219
5282
  }
5220
5283
  case 'add': {
@@ -5226,39 +5289,34 @@ function Table(store, fieldRenderer, layout, options) {
5226
5289
  const button = th.appendChild(document.createElement('button'));
5227
5290
  button.addEventListener('click', add);
5228
5291
  button.classList.add('NeeloongForm-table-foot-add');
5229
- destroyList.push(watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true));
5292
+ watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true, options?.signal);
5230
5293
  break;
5231
5294
  }
5232
5295
  case 'none':
5233
5296
  }
5234
- const start = thead;
5235
- /** @type {Map<Store, [HTMLTableSectionElement, () => void]>} */
5297
+ /** @type {Map<Store, [HTMLTableSectionElement, AbortController]>} */
5236
5298
  let seMap = new Map();
5237
- /** @param {Map<Store, [tbody: HTMLTableSectionElement, destroy: () => void]>} map */
5238
- function destroyMap(map) {
5239
- for (const [el, destroy] of map.values()) {
5240
- destroy();
5241
- el.remove();
5242
- }
5243
-
5244
- }
5245
- const childrenResult = watch(() => store.children, function render(children) {
5299
+ watch(() => store.children, children => {
5246
5300
  let nextNode = thead.nextSibling;
5247
5301
  const oldSeMap = seMap;
5248
5302
  seMap = new Map();
5249
5303
  for (let child of children) {
5250
5304
  const old = oldSeMap.get(child);
5251
5305
  if (!old) {
5252
- const [el, destroy] = Line(child, fieldRenderer, layout, {
5306
+ const ac = new AbortController();
5307
+ const el = Line(child, fieldRenderer, layout, {
5253
5308
  columns,
5309
+ deletable: new Signal.Computed(() => !store.readonly && !store.disabled && child.removable),
5254
5310
  remove: remove.bind(null, child),
5255
5311
  dragenter: dragenter.bind(null, child),
5256
5312
  dragstart: dragstart.bind(null, child),
5257
5313
  dragend,
5258
- deletable,
5259
- }, options);
5314
+ }, {
5315
+ ...options,
5316
+ signal: options?.signal ? AbortSignal.any([options?.signal, ac.signal]) : ac.signal,
5317
+ });
5260
5318
  table.insertBefore(el, nextNode);
5261
- seMap.set(child, [el, destroy]);
5319
+ seMap.set(child, [el, ac]);
5262
5320
  continue;
5263
5321
  }
5264
5322
  oldSeMap.delete(child);
@@ -5269,29 +5327,25 @@ function Table(store, fieldRenderer, layout, options) {
5269
5327
  }
5270
5328
  table.insertBefore(old[0], nextNode);
5271
5329
  }
5272
- destroyMap(oldSeMap);
5273
- }, true);
5274
-
5275
- return [table, () => {
5276
- start.remove();
5277
- thead.remove();
5278
- destroyMap(seMap);
5279
- childrenResult();
5280
- for (const destroy of destroyList) {
5281
- destroy();
5330
+ for (const [el, ac] of oldSeMap.values()) {
5331
+ el.remove();
5332
+ ac.abort();
5282
5333
  }
5283
- }];
5334
+ }, true, options?.signal);
5335
+
5336
+ return table;
5284
5337
  }
5285
5338
 
5286
- /** @import { StoreLayout } from '../types.mjs' */
5339
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5287
5340
 
5288
5341
  /**
5289
5342
  *
5290
5343
  * @param {HTMLElement} root
5291
- * @param {StoreLayout.Grid?} [layout]
5344
+ * @param {StoreLayout.Grid} layout
5345
+ * @returns {void}
5292
5346
  */
5293
5347
  function bindGrid(root, layout) {
5294
- const { colStart, colSpan, colEnd, rowStart, rowSpan, rowEnd } = layout || {};
5348
+ const { colStart, colSpan, colEnd, rowStart, rowSpan, rowEnd } = layout;
5295
5349
  root.classList.add(`NeeloongForm-item-grid`);
5296
5350
  if (colStart && colEnd) {
5297
5351
  root.style.gridColumn = `${colStart} / ${colEnd}`;
@@ -5307,226 +5361,126 @@ function bindGrid(root, layout) {
5307
5361
  } else if (rowSpan) {
5308
5362
  root.style.gridRow = `span ${rowSpan}`;
5309
5363
  }
5310
-
5311
-
5312
-
5313
-
5314
5364
  }
5315
5365
 
5316
- /** @import { CellValues } from './createCell.mjs' */
5366
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5317
5367
 
5318
5368
  /**
5319
- *
5320
- * @param {HTMLElement} root
5321
- * @param {CellValues} [values]
5322
- * @returns {() => void}
5323
- */
5324
- function bindErrored(root, values) {
5325
- return effect(() => {
5326
- if (values?.error) {
5327
- root.classList.add('NeeloongForm-item-errored');
5328
- } else {
5329
- root.classList.remove('NeeloongForm-item-errored');
5330
- }
5331
- });
5332
- }
5333
-
5334
- /**
5335
- *
5336
- * @param {HTMLElement} root
5337
- * @param {{ required?: boolean | null }} [values]
5338
- * @returns {() => void}
5369
+ * @typedef {object} CellValues
5370
+ * @property {string?} [label]
5371
+ * @property {string?} [description]
5372
+ * @property {string?} [error]
5373
+ * @property {boolean?} [required]
5339
5374
  */
5340
- function bindRequired(root, values) {
5341
- return effect(() => {
5342
- if (values?.required) {
5343
- root.classList.add('NeeloongForm-item-required');
5344
- } else {
5345
- root.classList.remove('NeeloongForm-item-required');
5346
- }
5347
- });
5348
- }
5349
-
5350
- /** @import { CellValues } from './createCell.mjs' */
5351
-
5352
5375
  /**
5353
5376
  *
5377
+ * @param {AbortSignal | null | undefined} signal
5354
5378
  * @param {CellValues} [values]
5355
- * @returns {[HTMLDivElement, () => void, HTMLDivElement, (() => void)[]]}
5379
+ * @returns {[HTMLDivElement, HTMLDivElement]}
5356
5380
  */
5357
- function createStdCell(values) {
5358
- /** @type {(() => void)[]} */
5359
- const destroyList = [];
5381
+ function createStdCell(signal, values) {
5360
5382
  const root = document.createElement('div');
5361
- root.className = 'NeeloongForm-item';
5362
- destroyList.push(bindRequired(root, values));
5363
-
5364
5383
  const label = root.appendChild(document.createElement('div'));
5365
- label.className = 'NeeloongForm-item-label';
5366
- destroyList.push(effect(() => label.innerText = values?.label || ''));
5384
+ label.classList.add('NeeloongForm-item-label');
5385
+ effect(() => label.innerText = values?.label || '', signal);
5367
5386
 
5368
5387
  const content = root.appendChild(document.createElement('div'));
5369
- content.className = 'NeeloongForm-item-content';
5388
+ content.classList.add('NeeloongForm-item-content');
5370
5389
 
5371
5390
  const description = root.appendChild(document.createElement('div'));
5372
- description.className = 'NeeloongForm-item-description';
5373
- destroyList.push(effect(() => description.innerText = values?.description || ''));
5391
+ description.classList.add('NeeloongForm-item-description');
5392
+ effect(() => description.innerText = values?.description || '', signal);
5374
5393
  const error = root.appendChild(document.createElement('div'));
5375
- error.className = 'NeeloongForm-item-error';
5376
- destroyList.push(effect(() => error.innerText = values?.error || ''));
5377
- destroyList.push(bindErrored(root, values));
5378
-
5379
- return [root, () => {
5380
- for (const destroy of destroyList) {
5381
- destroy();
5382
- }
5383
- }, content, destroyList];
5394
+ error.classList.add('NeeloongForm-item-error');
5395
+ effect(() => error.innerText = values?.error || '', signal);
5396
+ return [root, content];
5384
5397
 
5385
5398
 
5386
5399
  }
5387
5400
 
5388
- /** @import { CellValues } from './createCell.mjs' */
5389
-
5390
- /**
5391
- *
5392
- * @param {CellValues} [values]
5393
- * @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]}
5394
- */
5395
- function createCollapseCell(values) {
5396
- /** @type {(() => void)[]} */
5397
- const destroyList = [];
5398
- const root = document.createElement('details');
5399
- root.className = 'NeeloongForm-item';
5400
- destroyList.push(bindRequired(root, values));
5401
-
5402
- root.open = true;
5403
- const summary = root.appendChild(document.createElement('summary'));
5404
- destroyList.push(effect(() => summary.innerText = values?.label || ''));
5405
- destroyList.push(bindErrored(root, values));
5406
-
5407
- return [root, () => {
5408
- for (const destroy of destroyList) {
5409
- destroy();
5410
- }
5411
- }, root, destroyList];
5412
-
5413
-
5414
- }
5415
-
5416
- /** @import { CellValues } from './createCell.mjs' */
5417
-
5418
- /**
5419
- *
5420
- * @param {CellValues} [values]
5421
- * @returns {[HTMLDivElement, () => void, HTMLDivElement, (() => void)[]]}
5422
- */
5423
- function createNullCell(values) {
5424
- /** @type {(() => void)[]} */
5425
- const destroyList = [];
5426
- const root = document.createElement('div');
5427
- root.className = 'NeeloongForm-item';
5428
- destroyList.push(bindRequired(root, values));
5429
- destroyList.push(bindErrored(root, values));
5430
-
5431
-
5432
- return [root, () => {
5433
- for (const destroy of destroyList) {
5434
- destroy();
5435
- }
5436
- }, root, destroyList];
5437
-
5438
-
5439
- }
5440
-
5441
- /** @import { CellValues } from './createCell.mjs' */
5442
-
5443
- /**
5444
- *
5445
- * @param {CellValues} [values]
5446
- * @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]}
5447
- */
5448
- function createFieldsetCell(values) {
5449
- /** @type {(() => void)[]} */
5450
- const destroyList = [];
5451
- const root = document.createElement('fieldset');
5452
- root.className = 'NeeloongForm-item';
5453
- destroyList.push(bindRequired(root, values));
5454
-
5455
- const legend = root.appendChild(document.createElement('legend'));
5456
- destroyList.push(effect(() => legend.innerText = values?.label || ''));
5457
- destroyList.push(bindErrored(root, values));
5458
-
5459
- return [root, () => {
5460
- for (const destroy of destroyList) {
5461
- destroy();
5462
- }
5463
- }, root, destroyList];
5464
-
5465
-
5466
- }
5467
-
5468
- /** @import { StoreLayout } from '../types.mjs' */
5469
-
5470
- /**
5471
- * @typedef {object} CellValues
5472
- * @property {string?} [label]
5473
- * @property {string?} [description]
5474
- * @property {string?} [error]
5475
- * @property {boolean?} [required]
5476
- */
5477
5401
  /**
5478
5402
  *
5479
- * @param {StoreLayout.Grid?} [layout]
5403
+ * @param {AbortSignal | null | undefined} signal
5404
+ * @param {StoreLayout.Grid} layout
5480
5405
  * @param {CellValues} [values]
5481
5406
  * @param {StoreLayout.Grid['cell']?} [defCell]
5482
5407
  * @param {boolean?} [blockOnly]
5483
- * @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]}
5408
+ * @returns {[HTMLElement, HTMLElement]}
5484
5409
  */
5485
- function createCell(layout, values, defCell, blockOnly) {
5410
+ function createCell(signal, layout, values, defCell, blockOnly) {
5486
5411
  /**
5487
5412
  *
5488
5413
  * @param {string?} [cellType]
5489
- * @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]?}
5414
+ * @returns {[HTMLElement, HTMLElement]?}
5490
5415
  */
5491
5416
  function create(cellType) {
5492
5417
  if (!cellType) { return null; }
5493
5418
  if (!blockOnly) {
5494
5419
  switch (cellType) {
5495
5420
  case 'inline': {
5496
- const result = createStdCell(values);
5497
- bindGrid(result[0], layout);
5498
- return result;
5421
+ const [root, content] = createStdCell(signal, values);
5422
+ bindGrid(root, layout);
5423
+ return [root, content];
5499
5424
  }
5500
5425
  case 'base': {
5501
- const result = createNullCell(values);
5502
- bindGrid(result[0], layout);
5503
- return result;
5426
+ const root = document.createElement('div');
5427
+ bindGrid(root, layout);
5428
+ return [root, root];
5504
5429
  }
5505
5430
  }
5506
5431
  }
5507
5432
  switch (cellType) {
5508
- case 'block': return createStdCell(values);
5509
- case 'fieldset': return createFieldsetCell(values);
5510
- case 'collapse': return createCollapseCell(values);
5433
+ case 'block': return createStdCell(signal, values);
5434
+ case 'fieldset': {
5435
+ const root = document.createElement('fieldset');
5436
+ const legend = root.appendChild(document.createElement('legend'));
5437
+ effect(() => legend.innerText = values?.label || '', signal);
5438
+ return [root, root];
5439
+ }
5440
+ case 'collapse': {
5441
+ const root = document.createElement('details');
5442
+ root.open = true;
5443
+ const summary = root.appendChild(document.createElement('summary'));
5444
+ effect(() => summary.innerText = values?.label || '', signal);
5445
+ return [root, root];
5446
+ }
5511
5447
  }
5512
5448
  return null;
5513
5449
  }
5514
- return create(layout?.cell) || create(defCell) || createStdCell(values);
5450
+ const [root, content] = create(layout.cell) || create(defCell) || createStdCell(signal, values);
5451
+ root.classList.add('NeeloongForm-item');
5452
+ effect(() => {
5453
+ if (values?.error) {
5454
+ root.classList.add('NeeloongForm-item-errored');
5455
+ } else {
5456
+ root.classList.remove('NeeloongForm-item-errored');
5457
+ }
5458
+ }, signal);
5459
+ effect(() => {
5460
+ if (values?.required) {
5461
+ root.classList.add('NeeloongForm-item-required');
5462
+ } else {
5463
+ root.classList.remove('NeeloongForm-item-required');
5464
+ }
5465
+ }, signal);
5466
+ return [root, content];
5515
5467
  }
5516
5468
 
5517
5469
  /** @import { Store } from '../Store/index.mjs' */
5518
5470
  /** @import { State } from './Tree.mjs' */
5519
- /** @import { StoreLayout } from '../types.mjs' */
5471
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5520
5472
 
5521
5473
  /**
5522
5474
  *
5475
+ * @template T
5523
5476
  * @param {Store<any, any>} store
5524
5477
  * @param {Signal.State<Store<any, any>?>} currentStore
5525
- * @param {StoreLayout.Renderer} fieldRenderer
5526
- * @param {StoreLayout.Field?} layout
5527
- * @param {State} initState
5478
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
5479
+ * @param {StoreLayout.Field<T>} layout
5480
+ * @param {Signal.State<State>} state
5528
5481
  * @param {object} option
5529
- * @param {StoreLayout.Column[]} option.columns
5482
+ * @param {{get(): boolean}} option.addable
5483
+ * @param {StoreLayout.Column<T>[]} option.columns
5530
5484
  * @param {() => void} option.remove
5531
5485
  * @param {(el: HTMLElement) => () => void} option.dragenter
5532
5486
  * @param {() => void} option.dragstart
@@ -5536,15 +5490,13 @@ function createCell(layout, values, defCell, blockOnly) {
5536
5490
  * @param {() => void} option.addNode
5537
5491
  * @param {(store: Store<any, any>) => () => void} option.createDetails
5538
5492
  * @param {StoreLayout.Options?} options
5539
-
5540
- * @returns {[HTMLElement, () => void, (s: State) => void]}
5493
+ * @returns {HTMLElement}
5541
5494
  */
5542
5495
  function TreeLine(
5543
- store, currentStore, fieldRenderer, layout, initState, {
5544
- columns,
5545
- remove, dragenter, dragstart, dragend, deletable, addNode, drop, createDetails,
5496
+ store, currentStore, fieldRenderer, layout, state, {
5497
+ columns, addable, deletable,
5498
+ remove, dragenter, dragstart, dragend, addNode, drop, createDetails,
5546
5499
  }, options) {
5547
- const state = new Signal.State(initState);
5548
5500
  const root = document.createElement('div');
5549
5501
  root.addEventListener('dragstart', (event) => {
5550
5502
  if (event.target !== event.currentTarget) { return; }
@@ -5552,23 +5504,21 @@ function TreeLine(
5552
5504
  });
5553
5505
  root.addEventListener('dragend', dragend);
5554
5506
 
5555
- /** @type {(() => void)[]} */
5556
- const destroyList = [];
5557
5507
  root.classList.add('NeeloongForm-tree-item');
5558
5508
 
5559
- destroyList.push(effect(() => {
5509
+ effect(() => {
5560
5510
  if (currentStore.get() === store) {
5561
5511
  root.classList.add('NeeloongForm-tree-current');
5562
5512
  } else {
5563
5513
  root.classList.remove('NeeloongForm-tree-current');
5564
5514
  }
5565
- }));
5566
- destroyList.push(effect(() => {
5515
+ }, options?.signal);
5516
+ effect(() => {
5567
5517
  const level = state.get().level;
5568
5518
  root.style.setProperty(`--NeeloongForm-tree-level`, `${level}`);
5569
- }));
5519
+ }, options?.signal);
5570
5520
 
5571
- destroyList.push(effect(() => { root.hidden = state.get().hidden; }));
5521
+ effect(() => { root.hidden = state.get().hidden; }, options?.signal);
5572
5522
 
5573
5523
 
5574
5524
  /** @type {HTMLButtonElement[]} */
@@ -5606,9 +5556,9 @@ function TreeLine(
5606
5556
  }
5607
5557
  close = createDetails(store);
5608
5558
  }
5609
- const moveStart = layout?.mainMethod === 'move' ? pointerdown : null;
5610
- const click = moveStart ? null : layout?.mainMethod === 'collapse' ? switchCollapsed
5611
- : layout?.mainMethod === 'trigger' ? trigger : open;
5559
+ const moveStart = layout.mainMethod === 'move' ? pointerdown : null;
5560
+ const click = moveStart ? null : layout.mainMethod === 'collapse' ? switchCollapsed
5561
+ : layout.mainMethod === 'trigger' ? trigger : open;
5612
5562
 
5613
5563
  const line = root.appendChild(document.createElement('div'));
5614
5564
  line.classList.add('NeeloongForm-tree-line');
@@ -5621,9 +5571,9 @@ function TreeLine(
5621
5571
  dropChildren.addEventListener('dragover', (e) => e.preventDefault());
5622
5572
  dropFront.addEventListener('drop', () => drop());
5623
5573
  dropChildren.addEventListener('drop', () => drop(true));
5624
- destroyList.push(effect(() => {
5574
+ effect(() => {
5625
5575
  dropFront.hidden = dropChildren.hidden = !state.get().droppable;
5626
- }));
5576
+ }, options?.signal);
5627
5577
 
5628
5578
  let dragleave = () => { };
5629
5579
  dropFront.addEventListener('dragenter', () => dragleave = dragenter(dropFront));
@@ -5631,7 +5581,8 @@ function TreeLine(
5631
5581
  dropFront.addEventListener('dragleave', () => dragleave());
5632
5582
  dropChildren.addEventListener('dragleave', () => dragleave());
5633
5583
 
5634
- for (const { actions, pattern, placeholder, width, field } of columns) {
5584
+ for (const column of columns) {
5585
+ const { actions, pattern, placeholder, width, field } = column;
5635
5586
  if (!actions?.length) {
5636
5587
  const td = line.appendChild(document.createElement('div'));
5637
5588
  td.classList.add('NeeloongForm-tree-cell');
@@ -5640,9 +5591,8 @@ function TreeLine(
5640
5591
  if (field) {
5641
5592
  const child = store.child(field);
5642
5593
  if (!child) { continue; }
5643
- const [el, destroy] = FormFieldInline(child, fieldRenderer, null, { ...options, editable: false });
5644
- destroyList.push(destroy);
5645
- td.appendChild(el);
5594
+ const el = FormFieldInline(child, fieldRenderer, column, { ...options, editable: false });
5595
+ if (el) { td.appendChild(el); }
5646
5596
  continue;
5647
5597
  }
5648
5598
  if (typeof placeholder === 'number') {
@@ -5680,9 +5630,9 @@ function TreeLine(
5680
5630
  const move = line.appendChild(document.createElement('button'));
5681
5631
  move.classList.add('NeeloongForm-tree-move');
5682
5632
  move.addEventListener('pointerdown', pointerdown);
5683
- destroyList.push(watch(() => store.readonly || store.disabled, disabled => {
5633
+ watch(() => store.readonly || store.disabled, disabled => {
5684
5634
  move.disabled = disabled;
5685
- }, true));
5635
+ }, true, options.signal);
5686
5636
  continue;
5687
5637
  }
5688
5638
  case 'add': {
@@ -5690,9 +5640,9 @@ function TreeLine(
5690
5640
  const move = line.appendChild(document.createElement('button'));
5691
5641
  move.classList.add('NeeloongForm-tree-add');
5692
5642
  move.addEventListener('click', addNode);
5693
- destroyList.push(watch(() => store.readonly || store.disabled, disabled => {
5643
+ watch(() => !addable.get(), disabled => {
5694
5644
  move.disabled = disabled;
5695
- }, true));
5645
+ }, true, options.signal);
5696
5646
  continue;
5697
5647
  }
5698
5648
  case 'remove': {
@@ -5700,9 +5650,9 @@ function TreeLine(
5700
5650
  const del = line.appendChild(document.createElement('button'));
5701
5651
  del.classList.add('NeeloongForm-tree-remove');
5702
5652
  del.addEventListener('click', remove);
5703
- destroyList.push(watch(() => !deletable.get() || store.readonly || store.disabled, disabled => {
5653
+ watch(() => !deletable.get(), disabled => {
5704
5654
  del.disabled = disabled;
5705
- }, true));
5655
+ }, true, options.signal);
5706
5656
  continue;
5707
5657
  }
5708
5658
  case 'serial': {
@@ -5713,7 +5663,7 @@ function TreeLine(
5713
5663
  }
5714
5664
  }
5715
5665
  }
5716
- destroyList.push(effect(() => {
5666
+ effect(() => {
5717
5667
  const s = state.get();
5718
5668
  if (!s.hasChildren) {
5719
5669
  for (const btn of collapseList) {
@@ -5734,17 +5684,13 @@ function TreeLine(
5734
5684
  btn.disabled = false;
5735
5685
  }
5736
5686
  }
5737
- }));
5687
+ }, options?.signal);
5738
5688
 
5739
- return [root, () => {
5740
- for (const destroy of destroyList) {
5741
- destroy();
5742
- }
5743
- }, s => state.set(s)];
5689
+ return root;
5744
5690
  }
5745
5691
 
5746
5692
  /** @import { Store, ArrayStore } from '../Store/index.mjs' */
5747
- /** @import { StoreLayout } from '../types.mjs' */
5693
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
5748
5694
 
5749
5695
  const verticalWritingMode = new Set([
5750
5696
  'vertical-lr', 'vertical-rl', 'sideways-lr', 'sideways-rl',
@@ -5850,22 +5796,24 @@ function createState(store, states, drag, levelKey, index) {
5850
5796
  }
5851
5797
  /**
5852
5798
  *
5799
+ * @template T
5853
5800
  * @param {ArrayStore} store
5854
- * @param {StoreLayout.Renderer} fieldRenderer
5855
- * @param {StoreLayout.Field?} layout
5801
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
5802
+ * @param {StoreLayout.Field<T>} layout
5856
5803
  * @param {StoreLayout.Options?} options
5857
- * @returns {[HTMLElement, () => void]}
5804
+ * @returns {HTMLElement?}
5858
5805
  */
5859
5806
  function Tree(store, fieldRenderer, layout, options) {
5860
- const headerColumns = layout?.columns;
5807
+ if (options?.signal?.aborted) { return null; }
5808
+ const headerColumns = layout.columns;
5861
5809
  const fieldList = Object.entries(store.type || {})
5862
5810
  .filter(([k, v]) => typeof v?.type !== 'object')
5863
5811
  .map(([field, { width, label }]) => ({ field, width, label }));
5864
- /** @type {StoreLayout.Column[]} */
5812
+ /** @type {StoreLayout.Column<T>[]} */
5865
5813
  let columns = [];
5866
5814
  if (Array.isArray(headerColumns)) {
5867
5815
  const map = new Map(fieldList.map(v => [v.field, v]));
5868
- /** @type {(StoreLayout.Column | null)[]} */
5816
+ /** @type {(StoreLayout.Column<T> | null)[]} */
5869
5817
  const allColumns = headerColumns.map(v => {
5870
5818
  if (!v) { return null; }
5871
5819
  if (typeof v === 'number') { return { placeholder: v }; }
@@ -5898,7 +5846,7 @@ function Tree(store, fieldRenderer, layout, options) {
5898
5846
  }
5899
5847
  return null;
5900
5848
  });
5901
- columns = /** @type {StoreLayout.Column[]} */(allColumns.filter(Boolean));
5849
+ columns = /** @type {StoreLayout.Column<T>[]} */(allColumns.filter(Boolean));
5902
5850
  }
5903
5851
  if (!columns.length) {
5904
5852
  columns = [
@@ -5910,13 +5858,13 @@ function Tree(store, fieldRenderer, layout, options) {
5910
5858
 
5911
5859
 
5912
5860
  const root = document.createElement('div');
5913
- root.className = 'NeeloongForm-tree';
5861
+ root.classList.add('NeeloongForm-tree');
5914
5862
  const main = root.appendChild(document.createElement('div'));
5915
- main.className = 'NeeloongForm-tree-main';
5863
+ main.classList.add('NeeloongForm-tree-main');
5916
5864
  const splitter = root.appendChild(document.createElement('div'));
5917
- splitter.className = 'NeeloongForm-tree-splitter';
5865
+ splitter.classList.add('NeeloongForm-tree-splitter');
5918
5866
  const details = root.appendChild(document.createElement('div'));
5919
- details.className = 'NeeloongForm-tree-details';
5867
+ details.classList.add('NeeloongForm-tree-details');
5920
5868
  splitter.hidden = true;
5921
5869
  details.hidden = true;
5922
5870
  /** @type {number?} */
@@ -5980,9 +5928,8 @@ function Tree(store, fieldRenderer, layout, options) {
5980
5928
  });
5981
5929
 
5982
5930
 
5983
- /** @type {(() => void)[]} */
5984
- const destroyList = [];
5985
- let destroyDetails = () => { };
5931
+ /** @type {AbortController?} */
5932
+ let detailAbortController = null;
5986
5933
  const detailsStore = new Signal.State(/** @type{Store<any, any>?}*/(null));
5987
5934
  /**
5988
5935
  *
@@ -5990,32 +5937,42 @@ function Tree(store, fieldRenderer, layout, options) {
5990
5937
  * @returns
5991
5938
  */
5992
5939
  function createDetails(store) {
5993
- if (detailsStore.get() === store) { return destroyDetails; }
5994
- destroyDetails();
5940
+ if (options?.signal?.aborted) { return () => { }; }
5941
+ if (detailsStore.get() === store && detailAbortController) {
5942
+ const ac = detailAbortController;
5943
+ return () => { ac.abort(); };
5944
+ }
5945
+ detailAbortController?.abort();
5995
5946
  detailsStore.set(store);
5996
- const [form, destroy] = Form(store, fieldRenderer, layout, options);
5997
- details.appendChild(form);
5998
- details.hidden = false;
5999
- splitter.hidden = false;
6000
- let done = false;
6001
- destroyDetails = () => {
6002
- if (done) { return; }
6003
- done = true;
5947
+ const ac = new AbortController();
5948
+ detailAbortController = ac;
5949
+ const signal = options?.signal ? AbortSignal.any([options?.signal, ac.signal]) : ac.signal;
5950
+ const form = Form(store, fieldRenderer, layout, {
5951
+ ...options,
5952
+ signal: options?.signal ? AbortSignal.any([options?.signal, signal]) : signal,
5953
+ });
5954
+ signal.addEventListener('abort', () => {
6004
5955
  detailsStore.set(null);
6005
- form.remove();
6006
- destroy();
6007
5956
  stopMove();
6008
- splitter.hidden = true;
6009
- details.hidden = true;
6010
- };
6011
- return destroyDetails;
5957
+ }, { once: true });
5958
+ if (form) {
5959
+ details.appendChild(form);
5960
+ details.hidden = false;
5961
+ splitter.hidden = false;
5962
+ signal.addEventListener('abort', () => {
5963
+ form.remove();
5964
+ splitter.hidden = true;
5965
+ details.hidden = true;
5966
+ }, { once: true });
5967
+
5968
+ }
5969
+ return () => { ac.abort(); };
6012
5970
 
6013
5971
  }
6014
5972
 
6015
5973
 
6016
- const levelKey = layout?.levelKey || 'level';
6017
- const addable = new Signal.Computed(() => store.addable);
6018
- const deletable = { get: () => Boolean(options?.editable) };
5974
+ const levelKey = layout.levelKey || 'level';
5975
+ const addable = new Signal.Computed(() => !store.readonly && !store.disabled && store.addable);
6019
5976
  /**
6020
5977
  *
6021
5978
  * @param {number} parent
@@ -6144,7 +6101,7 @@ function Tree(store, fieldRenderer, layout, options) {
6144
6101
  const button = main.appendChild(document.createElement('button'));
6145
6102
  button.addEventListener('click', () => addNode(-1));
6146
6103
  button.classList.add('NeeloongForm-tree-head-add');
6147
- destroyList.push(watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true));
6104
+ watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true, options.signal);
6148
6105
  }
6149
6106
  const start = main.appendChild(document.createComment(''));
6150
6107
  if (options?.editable) {
@@ -6164,21 +6121,13 @@ function Tree(store, fieldRenderer, layout, options) {
6164
6121
  dropFront.addEventListener('drop', () => drop());
6165
6122
 
6166
6123
 
6167
- destroyList.push(watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true));
6124
+ watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true, options.signal);
6168
6125
  }
6169
- /** @type {Map<Store, [HTMLElement, () => void, (s: State) => void]>} */
6126
+ /** @type {Map<Store, [tbody: HTMLElement, AbortController, (s: State) => void]>} */
6170
6127
  let seMap = new Map();
6171
- /** @param {Map<Store, [tbody: HTMLElement, destroy: () => void, (s: State) => void]>} map */
6172
- function destroyMap(map) {
6173
- for (const [el, destroy] of map.values()) {
6174
- destroy();
6175
- el.remove();
6176
- }
6177
-
6178
- }
6179
6128
  /** @type {State[]} */
6180
6129
  const states = [];
6181
- const childrenResult = watch(() => store.children, function render(children) {
6130
+ watch(() => store.children, children => {
6182
6131
  let nextNode = start.nextSibling;
6183
6132
  const oldSeMap = seMap;
6184
6133
  seMap = new Map();
@@ -6193,19 +6142,24 @@ function Tree(store, fieldRenderer, layout, options) {
6193
6142
  const state = states[i];
6194
6143
  const old = oldSeMap.get(child);
6195
6144
  if (!old) {
6196
- const [el, destroy, setState] = TreeLine(child, detailsStore, fieldRenderer, layout, state, {
6197
- columns,
6145
+ const elState = new Signal.State(state);
6146
+ const ac = new AbortController();
6147
+ const el = TreeLine(child, detailsStore, fieldRenderer, layout, elState, {
6148
+ columns, addable,
6149
+ deletable: new Signal.Computed(() => !store.readonly && !store.disabled && child.removable),
6198
6150
  remove: remove.bind(null, child),
6199
6151
  dragenter,
6200
6152
  dragstart: dragstart.bind(null, child),
6201
6153
  dragend,
6202
- deletable,
6203
6154
  addNode: () => addNode(Number(child.index)),
6204
6155
  createDetails,
6205
6156
  drop: drop.bind(null, child),
6206
- }, options);
6157
+ }, {
6158
+ ...options,
6159
+ signal: options?.signal ? AbortSignal.any([options?.signal, ac.signal]) : ac.signal,
6160
+ });
6207
6161
  main.insertBefore(el, nextNode);
6208
- seMap.set(child, [el, destroy, setState]);
6162
+ seMap.set(child, [el, ac, s => elState.set(s)]);
6209
6163
  continue;
6210
6164
  }
6211
6165
  oldSeMap.delete(child);
@@ -6218,103 +6172,104 @@ function Tree(store, fieldRenderer, layout, options) {
6218
6172
  main.insertBefore(old[0], nextNode);
6219
6173
  }
6220
6174
  states.splice(childrenLength);
6221
- destroyMap(oldSeMap);
6222
- }, true);
6175
+ for (const [el, ac] of oldSeMap.values()) {
6176
+ el.remove();
6177
+ ac.abort();
6178
+ }
6179
+ }, true, options?.signal);
6223
6180
 
6224
- return [root, () => {
6225
- start.remove();
6226
- destroyMap(seMap);
6227
- childrenResult();
6228
- destroyDetails?.();
6229
- for (const destroy of destroyList) {
6230
- destroy();
6231
- }
6232
- }];
6181
+ return root;
6233
6182
  }
6234
6183
 
6235
6184
  /**
6236
6185
  *
6186
+ * @template T
6237
6187
  * @param {string | ParentNode} html
6238
6188
  * @param {Store<any, any>} store
6239
- * @param {StoreLayout.Renderer} fieldRenderer
6189
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
6240
6190
  * @param {StoreLayout.Options?} options
6241
- * @param {StoreLayout.Field?} layout
6242
- * @returns
6191
+ * @param {StoreLayout.Field<T>} layout
6192
+ * @returns {ParentNode}
6243
6193
  */
6244
6194
  function Html(html, store, fieldRenderer, options, layout) {
6245
6195
  const htmlContent = getHtmlContent(html);
6246
- const destroy = renderHtml(store, fieldRenderer, htmlContent, options, layout);
6247
- return [htmlContent, destroy];
6196
+ renderHtml(store, fieldRenderer, htmlContent, options, layout);
6197
+ return htmlContent;
6248
6198
  }
6249
-
6250
6199
  /**
6251
6200
  *
6252
- * @param {StoreLayout.Field['arrayStyle']?} arrayStyle
6201
+ * @template T
6202
+ * @param {StoreLayout.Field<T>['arrayStyle']?} arrayStyle
6203
+ * @param {ArrayStore} store
6204
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
6205
+ * @param {StoreLayout.Field<T>} layout
6206
+ * @param {StoreLayout.Options?} options
6207
+ * @returns {HTMLElement?}
6253
6208
  */
6254
- function getArrayCell(arrayStyle) {
6255
- switch(arrayStyle) {
6256
- case 'tree': return Tree;
6257
- default: return Table;
6209
+ function renderArrayCell(arrayStyle, store, fieldRenderer, layout, options) {
6210
+ switch (arrayStyle) {
6211
+ case 'tree': return Tree(store, fieldRenderer, layout, options);
6212
+ default: return Table(store, fieldRenderer, layout, options);
6258
6213
  }
6259
6214
 
6260
6215
  }
6261
6216
 
6262
6217
  /**
6263
6218
  *
6219
+ * @template T
6264
6220
  * @param {Store<any, any>} store
6265
- * @param {StoreLayout.Renderer} fieldRenderer
6266
- * @param {StoreLayout.Field?} layout
6221
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
6222
+ * @param {StoreLayout.Field<T>} layout
6267
6223
  * @param {StoreLayout.Options?} options
6268
- * @returns {[ParentNode, () => void]}
6224
+ * @returns {ParentNode}
6269
6225
  */
6270
6226
  function FormField(store, fieldRenderer, layout, options) {
6271
6227
  const { type } = store;
6272
- const isObject = type && typeof type === 'object';
6273
- const html = layout?.html;
6228
+ const isObject = Boolean(type && typeof type === 'object');
6229
+ const html = layout.html;
6274
6230
  /** @type {StoreLayout.Grid['cell']} */
6275
6231
  const cellType = isObject
6276
6232
  ? store instanceof ArrayStore ? 'collapse' : 'fieldset'
6277
6233
  : html ? 'base' : 'inline';
6278
- const [root, destroy, content, destroyList] = createCell(layout, store, cellType, isObject);
6279
- destroyList.push(effect(() => root.hidden = store.hidden));
6234
+ const [root, content] = createCell(options?.signal, layout, store, cellType, isObject);
6235
+ effect(() => root.hidden = store.hidden, options?.signal);
6280
6236
 
6237
+ /** @type {false | ParentNode | null} */
6281
6238
  const r =
6282
6239
  html && Html(html, store, fieldRenderer, options, layout)
6283
- || fieldRenderer(store, options)
6284
- || store instanceof ArrayStore && getArrayCell(layout?.arrayStyle)(store, fieldRenderer, layout, options)
6240
+ || fieldRenderer(store, layout.renderer, options)
6241
+ || store instanceof ArrayStore && renderArrayCell(layout.arrayStyle, store, fieldRenderer, layout, options)
6285
6242
  || isObject && Form(store, fieldRenderer, layout, options);
6286
6243
  if (r) {
6287
- const [el, destroy] = r;
6288
- content.appendChild(el);
6289
- destroyList.push(destroy);
6244
+ content.appendChild(r);
6290
6245
  }
6291
- return [root, destroy];
6246
+ return root;
6292
6247
  }
6293
6248
 
6294
6249
  /** @import { Store } from '../Store/index.mjs' */
6295
- /** @import { StoreLayout } from '../types.mjs' */
6250
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
6296
6251
 
6297
6252
  /**
6298
6253
  *
6299
6254
  * @param {Store<any, any>} store
6300
6255
  * @param {StoreLayout.Button} layout
6301
6256
  * @param {StoreLayout.Options?} options
6302
- * @returns {[ParentNode, () => void]}
6257
+ * @returns {ParentNode}
6303
6258
  */
6304
6259
  function FormButton(store, layout, options) {
6305
- const [root, destroy, content, destroyList] = createCell(layout, store);
6260
+ const [root, content] = createCell(options?.signal, layout, store);
6306
6261
  const button = document.createElement('button');
6307
- destroyList.push(() => {
6262
+ effect(() => {
6308
6263
  const t = layout.text;
6309
6264
  const text = typeof t === 'function' ? t(store, options) : t;
6310
6265
  button.innerText = text ?? '';
6311
- });
6312
- destroyList.push(() => {
6266
+ }, options?.signal);
6267
+ effect(() => {
6313
6268
  const d = layout.disabled;
6314
6269
  const disabled = typeof d === 'function' ? d(store, options) : d;
6315
6270
  button.disabled = Boolean(disabled);
6316
- });
6317
- button.className = 'NeeloongForm-item-button';
6271
+ }, options?.signal);
6272
+ button.classList.add('NeeloongForm-item-button');
6318
6273
  content.appendChild(button);
6319
6274
  const click = layout.click;
6320
6275
  if (typeof click === 'function') {
@@ -6325,40 +6280,42 @@ function FormButton(store, layout, options) {
6325
6280
  button.addEventListener('click', e => call(click, e, store, options));
6326
6281
  }
6327
6282
  }
6328
- return [root, destroy];
6283
+ return root;
6329
6284
  }
6330
6285
 
6331
6286
  /** @import { Store } from '../Store/index.mjs' */
6332
- /** @import { StoreLayout } from '../types.mjs' */
6287
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
6333
6288
 
6334
6289
  /**
6335
6290
  *
6291
+ * @template T
6336
6292
  * @param {Store<any, any>} store
6337
- * @param {StoreLayout.Renderer} fieldRenderer
6293
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
6338
6294
  * @param {StoreLayout.Html} layout
6339
6295
  * @param {StoreLayout.Options?} options
6340
- * @returns {[ParentNode, () => void]}
6296
+ * @returns {ParentNode?}
6341
6297
  */
6342
6298
  function FormHtml(store, fieldRenderer, layout, options) {
6343
- const [root, destroy, content, destroyList] = createCell(layout, store);
6344
6299
  const html = layout.html;
6345
- if (!html) { return [root, destroy]; }
6300
+ if (!html) { return null; }
6301
+ const [root, content] = createCell(options?.signal, layout, store);
6346
6302
  const htmlContent = getHtmlContent(html);
6347
- destroyList.push(renderHtml(store, fieldRenderer, htmlContent, options, layout));
6303
+ renderHtml(store, fieldRenderer, htmlContent, options, layout);
6348
6304
  content.appendChild(htmlContent);
6349
- return [root, destroy];
6305
+ return root;
6350
6306
  }
6351
6307
 
6352
6308
  /** @import { Store } from '../Store/index.mjs' */
6353
- /** @import { StoreLayout } from '../types.mjs' */
6309
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
6354
6310
 
6355
6311
  /**
6356
6312
  *
6313
+ * @template T
6357
6314
  * @param {Store<any, any>} store
6358
- * @param {StoreLayout.Renderer} fieldRenderer
6359
- * @param {StoreLayout.Item} item
6315
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
6316
+ * @param {StoreLayout.Item<T>} item
6360
6317
  * @param {StoreLayout.Options?} options
6361
- * @returns {[ParentNode, () => void]?}
6318
+ * @returns {ParentNode?}
6362
6319
  */
6363
6320
  function FormItem(store, fieldRenderer, item, options) {
6364
6321
  if (item.type === 'button') {
@@ -6367,7 +6324,9 @@ function FormItem(store, fieldRenderer, item, options) {
6367
6324
  if (item.type === 'html') {
6368
6325
  return FormHtml(store, fieldRenderer, item, options);
6369
6326
  }
6370
- const fieldStore = store.child(item.field);
6327
+ const field = item.field;
6328
+ if (!field) { return null; }
6329
+ const fieldStore = store.child(field);
6371
6330
  if (fieldStore) {
6372
6331
  return FormField(fieldStore, fieldRenderer, item, options);
6373
6332
  }
@@ -6376,64 +6335,62 @@ function FormItem(store, fieldRenderer, item, options) {
6376
6335
 
6377
6336
  /**
6378
6337
  *
6338
+ * @template T
6379
6339
  * @param {Store<any, any>} store
6380
- * @param {StoreLayout.Renderer} fieldRenderer
6381
- * @param {StoreLayout?} layout
6340
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
6341
+ * @param {StoreLayout<T>} layout
6382
6342
  * @param {StoreLayout.Options?} options
6383
6343
  * @param {HTMLElement} [parent]
6384
- * @returns {[HTMLElement, () => void]}
6344
+ * @returns {HTMLElement?}
6385
6345
  */
6386
6346
  function Form(store, fieldRenderer, layout, options, parent) {
6347
+ if (options?.signal?.aborted) { return null; }
6387
6348
  const root = parent instanceof HTMLElement ? parent : document.createElement('div');
6388
- root.className = 'NeeloongForm';
6389
- /** @type {(() => void)[]} */
6390
- const destroyList = [];
6391
- const fieldLayouts = layout?.fields;
6392
- if (fieldLayouts) {
6349
+ root.classList.add('NeeloongForm');
6350
+ const fieldLayouts = layout.fields || store.layout.fields;
6351
+ if (fieldLayouts?.length) {
6393
6352
  for (const fieldTemplate of fieldLayouts) {
6394
- const result = FormItem(store, fieldRenderer, fieldTemplate, options);
6395
- if (!result) { continue; }
6396
- const [el, destroy] = result;
6397
- root.appendChild(el);
6398
- destroyList.push(destroy);
6353
+ const el = FormItem(store, fieldRenderer, fieldTemplate, options);
6354
+ if (el) { root.appendChild(el); }
6399
6355
  }
6400
6356
  } else {
6401
6357
  const fields = [...store].map(([, v]) => v);
6402
6358
  for (const field of fields) {
6403
- const [el, destroy] = FormField(field, fieldRenderer, null, options);
6404
- root.appendChild(el);
6405
- destroyList.push(destroy);
6359
+ const el = FormField(field, fieldRenderer, field.layout, options);
6360
+ if (el) { root.appendChild(el); }
6406
6361
  }
6407
6362
  }
6408
- return [root, () => {
6409
- for (const destroy of destroyList) {
6410
- destroy();
6411
- }
6412
- }];
6363
+ return root;
6413
6364
 
6414
6365
  }
6415
6366
 
6416
6367
  /** @import { Store } from '../Store/index.mjs' */
6417
- /** @import { StoreLayout } from '../types.mjs' */
6368
+ /** @import { StoreLayout } from '../StoreLayout.types.mjs' */
6418
6369
 
6419
6370
  /**
6420
6371
  *
6372
+ * @template T
6421
6373
  * @param {Store} store
6422
- * @param {StoreLayout.Renderer} fieldRenderer
6374
+ * @param {StoreLayout.Renderer<T>} fieldRenderer
6423
6375
  * @param {HTMLElement} root
6424
- * @param {StoreLayout?} [layout]
6376
+ * @param {StoreLayout<T>} layout
6425
6377
  * @param {StoreLayout.Options & {clone?: boolean} | null} [options]
6378
+ * @returns {void}
6426
6379
  */
6427
6380
  function renderStore(store, fieldRenderer, root, layout, options) {
6428
- const html = layout?.html;
6429
- if (html) {
6430
- const content = getHtmlContent(html);
6431
- const destroy = renderHtml(store, fieldRenderer, content, options || null, layout);
6432
- root.appendChild(content);
6433
- return destroy;
6381
+ if (options?.signal?.aborted) { return; }
6382
+ const storeLayout = layout || store.layout;
6383
+ const html = storeLayout.html;
6384
+ if (!html) {
6385
+ Form(store, fieldRenderer, storeLayout, options || null, root);
6386
+ return;
6434
6387
  }
6435
- const s = Form(store, fieldRenderer, layout || null, options || null, root);
6436
- return s[1];
6388
+ const content = getHtmlContent(html);
6389
+ renderHtml(store, fieldRenderer, content, options || null, storeLayout);
6390
+ root.appendChild(content);
6391
+ options?.signal?.addEventListener('abort', () => {
6392
+ root.removeChild(content);
6393
+ }, { once: true });
6437
6394
  }
6438
6395
 
6439
6396
  export { ArrayStore, index as Layout, ObjectStore, Store, effect, render, renderStore, watch };