@neeloong/form 0.18.0 → 0.20.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.18.0
2
+ * @neeloong/form v0.20.0
3
3
  * (c) 2024-2025 Fierflame
4
4
  * @license Apache-2.0
5
5
  */
@@ -85,7 +85,7 @@ const values = v => {
85
85
  * @param {T | ((store: Store) => T?) | null} [fn]
86
86
  * @returns {[Signal.State<T?>, Signal.Computed<T?>]}
87
87
  */
88
- function createState(self, toValue, defState, fn) {
88
+ function createState$1(self, toValue, defState, fn) {
89
89
 
90
90
  const selfState = new Signal.State(toValue(defState));
91
91
  /** @type {Signal.Computed<T>} */
@@ -466,17 +466,17 @@ class Store {
466
466
  [this.#selfRequired, this.#required] = createBooleanStates(this, required, schema.required, parent ? parent.#required : null);
467
467
  [this.#selfDisabled, this.#disabled] = createBooleanStates(this, disabled, schema.disabled, parent ? parent.#disabled : null);
468
468
 
469
- [this.#selfLabel, this.#label] = createState(this, string, label, schema.label);
470
- [this.#selfDescription, this.#description] = createState(this, string, description, schema.description);
471
- [this.#selfPlaceholder, this.#placeholder] = createState(this, string, placeholder, schema.placeholder);
472
- [this.#selfMin, this.#min] = createState(this, number, min, schema.min);
473
- [this.#selfMax, this.#max] = createState(this, number, max, schema.max);
474
- [this.#selfStep, this.#step] = createState(this, number, step, schema.step);
475
- [this.#selfMinLength, this.#minLength] = createState(this, number, minLength, schema.minLength);
476
- [this.#selfMaxLength, this.#maxLength] = createState(this, number, maxLength, schema.maxLength);
477
- [this.#selfPattern, this.#pattern] = createState(this, regex, pattern, schema.pattern);
469
+ [this.#selfLabel, this.#label] = createState$1(this, string, label, schema.label);
470
+ [this.#selfDescription, this.#description] = createState$1(this, string, description, schema.description);
471
+ [this.#selfPlaceholder, this.#placeholder] = createState$1(this, string, placeholder, schema.placeholder);
472
+ [this.#selfMin, this.#min] = createState$1(this, number, min, schema.min);
473
+ [this.#selfMax, this.#max] = createState$1(this, number, max, schema.max);
474
+ [this.#selfStep, this.#step] = createState$1(this, number, step, schema.step);
475
+ [this.#selfMinLength, this.#minLength] = createState$1(this, number, minLength, schema.minLength);
476
+ [this.#selfMaxLength, this.#maxLength] = createState$1(this, number, maxLength, schema.maxLength);
477
+ [this.#selfPattern, this.#pattern] = createState$1(this, regex, pattern, schema.pattern);
478
478
  // @ts-ignore
479
- [this.#selfValues, this.#values] = createState(this, values, values$1, schema.values);
479
+ [this.#selfValues, this.#values] = createState$1(this, values, values$1, schema.values);
480
480
 
481
481
  [this.#selfRemovable, this.#removable] = createBooleanStates(this, removable, schema.removable ?? true);
482
482
 
@@ -1043,11 +1043,11 @@ setObjectStore(ObjectStore);
1043
1043
  */
1044
1044
  class ArrayStore extends Store {
1045
1045
  /** @type {(index: number, isNew?: boolean) => Store} */
1046
- #create = () => {throw new Error}
1046
+ #create = () => { throw new Error; };
1047
1047
  /** @type {Signal.State<Store[]>} */
1048
- #children
1048
+ #children;
1049
1049
  get children() { return [...this.#children.get()]; }
1050
- *[Symbol.iterator]() { return yield*[...this.#children.get().entries()]; }
1050
+ *[Symbol.iterator]() { return yield* [...this.#children.get().entries()]; }
1051
1051
  /**
1052
1052
  *
1053
1053
  * @param {string | number} key
@@ -1071,7 +1071,7 @@ class ArrayStore extends Store {
1071
1071
  * @param {(value: any, index: any, store: Store) => void} [options.onUpdate]
1072
1072
  * @param {(value: any, index: any, store: Store) => void} [options.onUpdateState]
1073
1073
  */
1074
- constructor(schema, { parent, onUpdate, onUpdateState, index, new: isNew, addable} = {}) {
1074
+ constructor(schema, { parent, onUpdate, onUpdateState, index, new: isNew, addable } = {}) {
1075
1075
  const childrenState = new Signal.State(/** @type {Store[]} */([]));
1076
1076
  // @ts-ignore
1077
1077
  const updateChildren = (list) => {
@@ -1079,7 +1079,7 @@ class ArrayStore extends Store {
1079
1079
  const children = [...childrenState.get()];
1080
1080
  const oldLength = children.length;
1081
1081
  for (let i = children.length; i < length; i++) {
1082
- children.push(this.#create(i));
1082
+ children.push(this.#create(i));
1083
1083
  }
1084
1084
  children.length = length;
1085
1085
  if (oldLength !== length) {
@@ -1091,8 +1091,8 @@ class ArrayStore extends Store {
1091
1091
  index, new: isNew, parent,
1092
1092
  size: new Signal.Computed(() => childrenState.get().length),
1093
1093
  state: [],
1094
- setValue(v) { return Array.isArray(v) ? v : v == null ? null : [v] },
1095
- setState(v) { return Array.isArray(v) ? v : v == null ? null : [v] },
1094
+ setValue(v) { return Array.isArray(v) ? v : v == null ? null : [v]; },
1095
+ setState(v) { return Array.isArray(v) ? v : v == null ? null : [v]; },
1096
1096
  convert(v, state) {
1097
1097
  const val = Array.isArray(v) ? v : v == null ? null : [v];
1098
1098
  updateChildren(val);
@@ -1101,7 +1101,7 @@ class ArrayStore extends Store {
1101
1101
  (Array.isArray(state) ? state : v == null ? [] : [state]),
1102
1102
  ];
1103
1103
  },
1104
- onUpdate:(value, index, state) => {
1104
+ onUpdate: (value, index, state) => {
1105
1105
  updateChildren(value);
1106
1106
  onUpdate?.(value, index, state);
1107
1107
  },
@@ -1110,13 +1110,13 @@ class ArrayStore extends Store {
1110
1110
  });
1111
1111
 
1112
1112
  [this.#selfAddable, this.#addable] = createBooleanStates(this, addable, schema.addable ?? true);
1113
-
1113
+
1114
1114
  this.#children = childrenState;
1115
1115
  const childCommonOptions = {
1116
1116
  parent: this,
1117
1117
  /** @param {*} value @param {*} index @param {Store} store */
1118
1118
  onUpdate: (value, index, store) => {
1119
- if (childrenState.get()[index] !== store) { return;}
1119
+ if (childrenState.get()[index] !== store) { return; }
1120
1120
  const val = [...this.value || []];
1121
1121
  if (val.length < index) {
1122
1122
  val.length = index;
@@ -1126,7 +1126,7 @@ class ArrayStore extends Store {
1126
1126
  },
1127
1127
  /** @param {*} state @param {*} index @param {Store} store */
1128
1128
  onUpdateState: (state, index, store) => {
1129
- if (childrenState.get()[index] !== store) { return;}
1129
+ if (childrenState.get()[index] !== store) { return; }
1130
1130
  const sta = [...this.state || []];
1131
1131
  if (sta.length < index) {
1132
1132
  sta.length = index;
@@ -1135,18 +1135,18 @@ class ArrayStore extends Store {
1135
1135
  this.state = sta;
1136
1136
  },
1137
1137
  };
1138
- this.#create = (index, isNew) => {
1139
- const child = create(schema, {...childCommonOptions, index, new: isNew });
1138
+ this.#create = (index, isNew) => {
1139
+ const child = create(schema, { ...childCommonOptions, index, new: isNew });
1140
1140
  child.index = index;
1141
- return child
1141
+ return child;
1142
1142
  };
1143
1143
  }
1144
1144
 
1145
1145
 
1146
1146
  /** @readonly @type {Signal.State<boolean?>} */
1147
- #selfAddable
1147
+ #selfAddable;
1148
1148
  /** @readonly @type {Signal.Computed<boolean>} */
1149
- #addable
1149
+ #addable;
1150
1150
  get selfAddable() { return this.#selfAddable.get(); }
1151
1151
  set selfAddable(v) { this.#selfAddable.set(typeof v === 'boolean' ? v : null); }
1152
1152
  /** 是否禁用字段 */
@@ -1226,37 +1226,49 @@ class ArrayStore extends Store {
1226
1226
  *
1227
1227
  * @param {number} from
1228
1228
  * @param {number} to
1229
+ * @param {number} quantity
1229
1230
  * @returns
1230
1231
  */
1231
- move(from, to) {
1232
+ move(from, to, quantity = 1) {
1233
+ const q = Math.floor(quantity);
1234
+ if (q < 1) { return 0; }
1235
+ if (from <= to && from + q > to) { return 0; }
1236
+
1232
1237
  const data = this.value;
1233
- if (!Array.isArray(data)) { return false; }
1238
+ if (!Array.isArray(data)) { return 0; }
1239
+
1234
1240
  const children = [...this.#children.get()];
1235
- const [item] = children.splice(from, 1);
1236
- if (!item) { return false; }
1237
- children.splice(to, 0, item);
1238
- let lft = Math.min(from, to);
1239
- let rgt = Math.max(from, to);
1241
+ const list = children.splice(from, q);
1242
+ const len = list.length;
1243
+ if (!len) { return 0; }
1244
+ const toIndex = q > 1 && to > from ? to - q + 1 : to;
1245
+ children.splice(toIndex, 0, ...list);
1246
+
1247
+ let lft = Math.min(from, toIndex);
1248
+ let rgt = Math.max(from + q - 1, to);
1240
1249
  for (let i = lft; i <= rgt; i++) {
1241
1250
  children[i].index = i;
1242
1251
  }
1252
+
1243
1253
  const val = [...data];
1244
- const [value] = val.splice(from, 1);
1245
- val.splice(to, 0, value);
1254
+ const values = val.splice(from, len);
1255
+ for (let i = values.length; i < len; i++) { values.push(null); }
1256
+ while (toIndex > val.length) { val.push(null); }
1257
+ val.splice(toIndex, 0, ...values);
1258
+
1246
1259
  const state = this.state;
1247
1260
  if (Array.isArray(state)) {
1248
1261
  const sta = [...state];
1249
- const [value = {}] = sta.splice(from, 1);
1250
- if (to <= sta.length) {
1251
- sta.splice(to, 0, value);
1252
- } else {
1253
- sta[to] = value;
1254
- }
1262
+ const states = sta.splice(from, len);
1263
+ for (let i = states.length; i < len; i++) { states.push({}); }
1264
+ while (toIndex > sta.length) { sta.push({}); }
1265
+ sta.splice(toIndex, 0, ...states);
1266
+
1255
1267
  this.state = sta;
1256
1268
  }
1257
1269
  this.#children.set(children);
1258
1270
  this.value = val;
1259
- return true;
1271
+ return len;
1260
1272
 
1261
1273
  }
1262
1274
  /**
@@ -4937,6 +4949,7 @@ function Table(store, fieldRenderer, layout, options) {
4937
4949
  if (Array.isArray(headerColumns)) {
4938
4950
  const map = new Map(fieldList.map(v => [v.field, v]));
4939
4951
  columns = headerColumns.map(v => {
4952
+ if (typeof v === 'number') { return [] }
4940
4953
  if (typeof v === 'string') { return map.get(v) || [] }
4941
4954
  if (!Array.isArray(v)) { return []; }
4942
4955
  /** @type {Set<StoreLayout.Action>} */
@@ -5323,6 +5336,54 @@ function getHtmlContent(html) {
5323
5336
  return template.content;
5324
5337
  }
5325
5338
 
5339
+ /** @import { StoreLayout } from '../types.mjs' */
5340
+
5341
+ /**
5342
+ *
5343
+ * @param {HTMLElement} root
5344
+ * @param {StoreLayout.Grid?} [layout]
5345
+ */
5346
+ function bindGrid(root, layout) {
5347
+ const { colStart, colSpan, colEnd, rowStart, rowSpan, rowEnd } = layout || {};
5348
+ root.classList.add(`NeeloongForm-item-grid`);
5349
+ if (colStart && colEnd) {
5350
+ root.style.gridColumn = `${colStart} / ${colEnd}`;
5351
+ } else if (colStart && colSpan) {
5352
+ root.style.gridColumn = `${colStart} / span ${colSpan}`;
5353
+ } else if (colSpan) {
5354
+ root.style.gridColumn = `span ${colSpan}`;
5355
+ }
5356
+ if (rowStart && rowEnd) {
5357
+ root.style.gridRow = `${rowStart} / ${rowEnd}`;
5358
+ } else if (rowStart && rowSpan) {
5359
+ root.style.gridRow = `${rowStart} / span ${rowSpan}`;
5360
+ } else if (rowSpan) {
5361
+ root.style.gridRow = `span ${rowSpan}`;
5362
+ }
5363
+
5364
+
5365
+
5366
+
5367
+ }
5368
+
5369
+ /** @import { CellValues } from './createCell.mjs' */
5370
+
5371
+ /**
5372
+ *
5373
+ * @param {HTMLElement} root
5374
+ * @param {CellValues} [values]
5375
+ * @returns {() => void}
5376
+ */
5377
+ function bindErrored(root, values) {
5378
+ return effect(() => {
5379
+ if (values?.error) {
5380
+ root.classList.add('NeeloongForm-item-errored');
5381
+ } else {
5382
+ root.classList.remove('NeeloongForm-item-errored');
5383
+ }
5384
+ });
5385
+ }
5386
+
5326
5387
  /**
5327
5388
  *
5328
5389
  * @param {HTMLElement} root
@@ -5339,9 +5400,49 @@ function bindRequired(root, values) {
5339
5400
  });
5340
5401
  }
5341
5402
 
5403
+ /** @import { CellValues } from './createCell.mjs' */
5404
+
5342
5405
  /**
5343
5406
  *
5344
- * @param {{label?: string | null; description?: string | null; required?: boolean | null}} [values]
5407
+ * @param {CellValues} [values]
5408
+ * @returns {[HTMLDivElement, () => void, HTMLDivElement, (() => void)[]]}
5409
+ */
5410
+ function createStdCell(values) {
5411
+ /** @type {(() => void)[]} */
5412
+ const destroyList = [];
5413
+ const root = document.createElement('div');
5414
+ root.className = 'NeeloongForm-item';
5415
+ destroyList.push(bindRequired(root, values));
5416
+
5417
+ const label = root.appendChild(document.createElement('div'));
5418
+ label.className = 'NeeloongForm-item-label';
5419
+ destroyList.push(effect(() => label.innerText = values?.label || ''));
5420
+
5421
+ const content = root.appendChild(document.createElement('div'));
5422
+ content.className = 'NeeloongForm-item-content';
5423
+
5424
+ const description = root.appendChild(document.createElement('div'));
5425
+ description.className = 'NeeloongForm-item-description';
5426
+ destroyList.push(effect(() => description.innerText = values?.description || ''));
5427
+ const error = root.appendChild(document.createElement('div'));
5428
+ error.className = 'NeeloongForm-item-error';
5429
+ destroyList.push(effect(() => error.innerText = values?.error || ''));
5430
+ destroyList.push(bindErrored(root, values));
5431
+
5432
+ return [root, () => {
5433
+ for (const destroy of destroyList) {
5434
+ destroy();
5435
+ }
5436
+ }, content, destroyList];
5437
+
5438
+
5439
+ }
5440
+
5441
+ /** @import { CellValues } from './createCell.mjs' */
5442
+
5443
+ /**
5444
+ *
5445
+ * @param {CellValues} [values]
5345
5446
  * @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]}
5346
5447
  */
5347
5448
  function createCollapseCell(values) {
@@ -5354,6 +5455,7 @@ function createCollapseCell(values) {
5354
5455
  root.open = true;
5355
5456
  const summary = root.appendChild(document.createElement('summary'));
5356
5457
  destroyList.push(effect(() => summary.innerText = values?.label || ''));
5458
+ destroyList.push(bindErrored(root, values));
5357
5459
 
5358
5460
  return [root, () => {
5359
5461
  for (const destroy of destroyList) {
@@ -5364,48 +5466,48 @@ function createCollapseCell(values) {
5364
5466
 
5365
5467
  }
5366
5468
 
5367
- /** @import { StoreLayout } from '../types.mjs' */
5469
+ /** @import { CellValues } from './createCell.mjs' */
5368
5470
 
5369
5471
  /**
5370
5472
  *
5371
- * @param {HTMLElement} root
5372
- * @param {StoreLayout.Item?} layout
5473
+ * @param {CellValues} [values]
5474
+ * @returns {[HTMLDivElement, () => void, HTMLDivElement, (() => void)[]]}
5373
5475
  */
5374
- function bindGrid(root, layout) {
5375
- const { colStart, colSpan, colEnd, rowStart, rowSpan, rowEnd } = layout || {};
5376
- root.classList.add(`NeeloongForm-item-grid`);
5377
- if (colStart && colEnd) {
5378
- root.style.gridColumn = `${colStart} / ${colEnd}`;
5379
- } else if (colStart && colSpan) {
5380
- root.style.gridColumn = `${colStart} / span ${colSpan}`;
5381
- } else if (colSpan) {
5382
- root.style.gridColumn = `span ${colSpan}`;
5383
- }
5384
- if (rowStart && rowEnd) {
5385
- root.style.gridRow = `${rowStart} / ${rowEnd}`;
5386
- } else if (rowStart && rowSpan) {
5387
- root.style.gridRow = `${rowStart} / span ${rowSpan}`;
5388
- } else if (rowSpan) {
5389
- root.style.gridRow = `span ${rowSpan}`;
5390
- }
5476
+ function createNullCell(values) {
5477
+ /** @type {(() => void)[]} */
5478
+ const destroyList = [];
5479
+ const root = document.createElement('div');
5480
+ root.className = 'NeeloongForm-item';
5481
+ destroyList.push(bindRequired(root, values));
5482
+ destroyList.push(bindErrored(root, values));
5391
5483
 
5392
5484
 
5485
+ return [root, () => {
5486
+ for (const destroy of destroyList) {
5487
+ destroy();
5488
+ }
5489
+ }, root, destroyList];
5393
5490
 
5394
5491
 
5395
5492
  }
5396
5493
 
5494
+ /** @import { CellValues } from './createCell.mjs' */
5495
+
5397
5496
  /**
5398
5497
  *
5399
- * @param {{label?: string | null; description?: string | null; required?: boolean | null}} [values]
5400
- * @returns {[HTMLDivElement, () => void, HTMLDivElement, (() => void)[]]}
5498
+ * @param {CellValues} [values]
5499
+ * @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]}
5401
5500
  */
5402
- function createNullCell(values) {
5501
+ function createFieldsetCell(values) {
5403
5502
  /** @type {(() => void)[]} */
5404
5503
  const destroyList = [];
5405
- const root = document.createElement('div');
5504
+ const root = document.createElement('fieldset');
5406
5505
  root.className = 'NeeloongForm-item';
5407
5506
  destroyList.push(bindRequired(root, values));
5408
5507
 
5508
+ const legend = root.appendChild(document.createElement('legend'));
5509
+ destroyList.push(effect(() => legend.innerText = values?.label || ''));
5510
+ destroyList.push(bindErrored(root, values));
5409
5511
 
5410
5512
  return [root, () => {
5411
5513
  for (const destroy of destroyList) {
@@ -5416,62 +5518,769 @@ function createNullCell(values) {
5416
5518
 
5417
5519
  }
5418
5520
 
5521
+ /** @import { StoreLayout } from '../types.mjs' */
5522
+
5523
+ /**
5524
+ * @typedef {object} CellValues
5525
+ * @property {string?} [label]
5526
+ * @property {string?} [description]
5527
+ * @property {string?} [error]
5528
+ * @property {boolean?} [required]
5529
+ */
5419
5530
  /**
5420
5531
  *
5421
- * @param {{label?: string | null; description?: string | null; required?: boolean | null}} [values]
5422
- * @returns {[HTMLDivElement, () => void, HTMLDivElement, (() => void)[]]}
5532
+ * @param {StoreLayout.Grid?} [layout]
5533
+ * @param {CellValues} [values]
5534
+ * @param {StoreLayout.Grid['cell']?} [defCell]
5535
+ * @param {boolean?} [blockOnly]
5536
+ * @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]}
5423
5537
  */
5424
- function createBaseCell(values) {
5538
+ function createCell(layout, values, defCell, blockOnly) {
5539
+ /**
5540
+ *
5541
+ * @param {string?} [cellType]
5542
+ * @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]?}
5543
+ */
5544
+ function create(cellType) {
5545
+ if (!cellType) { return null; }
5546
+ if (!blockOnly) {
5547
+ switch (cellType) {
5548
+ case 'inline': {
5549
+ const result = createStdCell(values);
5550
+ bindGrid(result[0], layout);
5551
+ return result;
5552
+ }
5553
+ case 'base': {
5554
+ const result = createNullCell(values);
5555
+ bindGrid(result[0], layout);
5556
+ return result;
5557
+ }
5558
+ }
5559
+ }
5560
+ switch (cellType) {
5561
+ case 'block': return createStdCell(values);
5562
+ case 'fieldset': return createFieldsetCell(values);
5563
+ case 'collapse': return createCollapseCell(values);
5564
+ }
5565
+ return null;
5566
+ }
5567
+ return create(layout?.cell) || create(defCell) || createStdCell(values);
5568
+ }
5569
+
5570
+ /** @import { Store } from '../Store/index.mjs' */
5571
+ /** @import { State } from './Tree.mjs' */
5572
+ /** @import { StoreLayout } from '../types.mjs' */
5573
+
5574
+ /**
5575
+ *
5576
+ * @param {Store<any, any>} store
5577
+ * @param {Signal.State<Store<any, any>?>} currentStore
5578
+ * @param {StoreLayout.Renderer} fieldRenderer
5579
+ * @param {StoreLayout.Field?} layout
5580
+ * @param {State} initState
5581
+ * @param {object} option
5582
+ * @param {(string | number | StoreLayout.Action[])[]} option.columns
5583
+ * @param {() => void} option.remove
5584
+ * @param {(el: HTMLElement) => () => void} option.dragenter
5585
+ * @param {() => void} option.dragstart
5586
+ * @param {(inChildren?: boolean) => void} option.drop
5587
+ * @param {() => void} option.dragend
5588
+ * @param {{get(): boolean}} option.deletable
5589
+ * @param {() => void} option.addNode
5590
+ * @param {(store: Store<any, any>) => () => void} option.createDetails
5591
+ * @param {StoreLayout.Options?} options
5592
+
5593
+ * @returns {[HTMLElement, () => void, (s: State) => void]}
5594
+ */
5595
+ function TreeLine(
5596
+ store, currentStore, fieldRenderer, layout, initState, {
5597
+ columns,
5598
+ remove, dragenter, dragstart, dragend, deletable, addNode, drop, createDetails,
5599
+ }, options) {
5600
+ const state = new Signal.State(initState);
5601
+ const root = document.createElement('div');
5602
+ root.addEventListener('dragstart', (event) => {
5603
+ if (event.target !== event.currentTarget) { return; }
5604
+ dragstart();
5605
+ });
5606
+ root.addEventListener('dragend', dragend);
5607
+
5425
5608
  /** @type {(() => void)[]} */
5426
5609
  const destroyList = [];
5427
- const root = document.createElement('div');
5428
- root.className = 'NeeloongForm-item';
5429
- destroyList.push(bindRequired(root, values));
5610
+ root.classList.add('NeeloongForm-tree-item');
5430
5611
 
5431
- const label = root.appendChild(document.createElement('div'));
5432
- label.className = 'NeeloongForm-item-label';
5433
- destroyList.push(effect(() => label.innerText = values?.label || ''));
5612
+ destroyList.push(effect(() => {
5613
+ if (currentStore.get() === store) {
5614
+ root.classList.add('NeeloongForm-tree-current');
5615
+ } else {
5616
+ root.classList.remove('NeeloongForm-tree-current');
5617
+ }
5618
+ }));
5619
+ destroyList.push(effect(() => {
5620
+ const level = state.get().level;
5621
+ root.style.setProperty(`--NeeloongForm-tree-level`, `${level}`);
5622
+ }));
5434
5623
 
5435
- const content = root.appendChild(document.createElement('div'));
5436
- content.className = 'NeeloongForm-item-content';
5624
+ destroyList.push(effect(() => { root.hidden = state.get().hidden; }));
5437
5625
 
5438
- const description = root.appendChild(document.createElement('div'));
5439
- description.className = 'NeeloongForm-item-description';
5440
- destroyList.push(effect(() => description.innerText = values?.description || ''));
5626
+
5627
+ /** @type {HTMLButtonElement[]} */
5628
+ const collapseList = [];
5629
+
5630
+ /**
5631
+ *
5632
+ * @param {PointerEvent} event
5633
+ */
5634
+ function pointerdown({ pointerId }) {
5635
+ root.draggable = true;
5636
+ /** @param {PointerEvent} event */
5637
+ function pointerup(event) {
5638
+ if (event.pointerId !== pointerId) { return; }
5639
+ if (!root) { return; }
5640
+ root.draggable = false;
5641
+ window.removeEventListener('pointerup', pointerup, { capture: true });
5642
+ window.removeEventListener('pointercancel', pointerup, { capture: true });
5643
+ }
5644
+ window.addEventListener('pointerup', pointerup, { capture: true });
5645
+ window.addEventListener('pointercancel', pointerup, { capture: true });
5646
+
5647
+ }
5648
+ function switchCollapsed() {
5649
+ const s = state.get();
5650
+ s.collapsed = !s.collapsed;
5651
+ }
5652
+ let close = () => { };
5653
+ function open() {
5654
+ close = createDetails(store);
5655
+ }
5656
+ function trigger() {
5657
+ if (currentStore.get() === store) {
5658
+ close(); return;
5659
+ }
5660
+ close = createDetails(store);
5661
+ }
5662
+ const moveStart = layout?.mainMethod === 'move' ? pointerdown : null;
5663
+ const click = moveStart ? null : layout?.mainMethod === 'collapse' ? switchCollapsed
5664
+ : layout?.mainMethod === 'trigger' ? trigger : open;
5665
+
5666
+ const line = root.appendChild(document.createElement('div'));
5667
+ line.classList.add('NeeloongForm-tree-line');
5668
+
5669
+ const dropFront = root.appendChild(document.createElement('div'));
5670
+ dropFront.classList.add('NeeloongForm-tree-drop');
5671
+ const dropChildren = root.appendChild(document.createElement('div'));
5672
+ dropChildren.classList.add('NeeloongForm-tree-drop-children');
5673
+ dropFront.addEventListener('dragover', (e) => e.preventDefault());
5674
+ dropChildren.addEventListener('dragover', (e) => e.preventDefault());
5675
+ dropFront.addEventListener('drop', () => drop());
5676
+ dropChildren.addEventListener('drop', () => drop(true));
5677
+ destroyList.push(effect(() => {
5678
+ dropFront.hidden = dropChildren.hidden = !state.get().droppable;
5679
+ }));
5680
+
5681
+ let dragleave = () => {};
5682
+ dropFront.addEventListener('dragenter', () => dragleave = dragenter(dropFront));
5683
+ dropChildren.addEventListener('dragenter', () => dragleave = dragenter(dropChildren));
5684
+ dropFront.addEventListener('dragleave', () => dragleave());
5685
+ dropChildren.addEventListener('dragleave', () => dragleave());
5686
+
5687
+ for (const name of columns) {
5688
+ if (typeof name === 'number') {
5689
+ const td = line.appendChild(document.createElement('div'));
5690
+ td.classList.add('NeeloongForm-tree-placeholder');
5691
+ td.style.flex = `${name}`;
5692
+ if (click) { td.addEventListener('click', click); }
5693
+ if (moveStart) { td.addEventListener('pointerdown', moveStart); }
5694
+ continue;
5695
+ }
5696
+ if (!Array.isArray(name)) {
5697
+ const td = line.appendChild(document.createElement('div'));
5698
+ td.classList.add('NeeloongForm-tree-cell');
5699
+ const child = store.child(name);
5700
+ if (!child) { continue; }
5701
+ const [el, destroy] = FormField(child, fieldRenderer, null, { ...options, editable: false }, true);
5702
+ destroyList.push(destroy);
5703
+ td.appendChild(el);
5704
+ if (click) { td.addEventListener('click', click); }
5705
+ if (moveStart) { td.addEventListener('pointerdown', moveStart); }
5706
+ continue;
5707
+ }
5708
+ for (const k of name) {
5709
+ switch (k) {
5710
+ case 'trigger': {
5711
+ const btn = line.appendChild(document.createElement('button'));
5712
+ btn.classList.add('NeeloongForm-tree-trigger');
5713
+ btn.addEventListener('click', trigger);
5714
+ continue;
5715
+ }
5716
+ case 'open': {
5717
+ const btn = line.appendChild(document.createElement('button'));
5718
+ btn.classList.add('NeeloongForm-tree-open');
5719
+ btn.addEventListener('click', open);
5720
+ continue;
5721
+ }
5722
+ case 'collapse': {
5723
+ const btn = line.appendChild(document.createElement('button'));
5724
+ btn.classList.add('NeeloongForm-tree-collapse');
5725
+ btn.addEventListener('click', switchCollapsed);
5726
+ collapseList.push(btn);
5727
+ continue;
5728
+ }
5729
+ case 'move': {
5730
+ if (!options?.editable) { continue; }
5731
+ const move = line.appendChild(document.createElement('button'));
5732
+ move.classList.add('NeeloongForm-tree-move');
5733
+ move.addEventListener('pointerdown', pointerdown);
5734
+ destroyList.push(watch(() => store.readonly || store.disabled, disabled => {
5735
+ move.disabled = disabled;
5736
+ }, true));
5737
+ continue;
5738
+ }
5739
+ case 'add': {
5740
+ if (!options?.editable) { continue; }
5741
+ const move = line.appendChild(document.createElement('button'));
5742
+ move.classList.add('NeeloongForm-tree-add');
5743
+ move.addEventListener('click', addNode);
5744
+ destroyList.push(watch(() => store.readonly || store.disabled, disabled => {
5745
+ move.disabled = disabled;
5746
+ }, true));
5747
+ continue;
5748
+ }
5749
+ case 'remove': {
5750
+ if (!options?.editable) { continue; }
5751
+ const del = line.appendChild(document.createElement('button'));
5752
+ del.classList.add('NeeloongForm-tree-remove');
5753
+ del.addEventListener('click', remove);
5754
+ destroyList.push(watch(() => !deletable.get() || store.readonly || store.disabled, disabled => {
5755
+ del.disabled = disabled;
5756
+ }, true));
5757
+ continue;
5758
+ }
5759
+ case 'serial': {
5760
+ const serial = line.appendChild(document.createElement('span'));
5761
+ serial.classList.add('NeeloongForm-tree-serial');
5762
+ continue;
5763
+ }
5764
+ }
5765
+ }
5766
+ }
5767
+ destroyList.push(effect(() => {
5768
+ const s = state.get();
5769
+ if (!s.hasChildren) {
5770
+ for (const btn of collapseList) {
5771
+ btn.classList.remove('NeeloongForm-tree-collapse-close');
5772
+ btn.classList.remove('NeeloongForm-tree-collapse-open');
5773
+ btn.disabled = true;
5774
+ }
5775
+ } else if (s.collapsed) {
5776
+ for (const btn of collapseList) {
5777
+ btn.classList.remove('NeeloongForm-tree-collapse-close');
5778
+ btn.classList.add('NeeloongForm-tree-collapse-open');
5779
+ btn.disabled = false;
5780
+ }
5781
+ } else {
5782
+ for (const btn of collapseList) {
5783
+ btn.classList.remove('NeeloongForm-tree-collapse-open');
5784
+ btn.classList.add('NeeloongForm-tree-collapse-close');
5785
+ btn.disabled = false;
5786
+ }
5787
+ }
5788
+ }));
5441
5789
 
5442
5790
  return [root, () => {
5443
5791
  for (const destroy of destroyList) {
5444
5792
  destroy();
5445
5793
  }
5446
- }, content, destroyList];
5794
+ }, s => state.set(s)];
5795
+ }
5447
5796
 
5797
+ /** @import { Store, ArrayStore } from '../Store/index.mjs' */
5798
+ /** @import { StoreLayout } from '../types.mjs' */
5448
5799
 
5800
+ const verticalWritingMode = new Set([
5801
+ 'vertical-lr', 'vertical-rl', 'sideways-lr', 'sideways-rl',
5802
+ ]);
5803
+ /**
5804
+ *
5805
+ * @param {Element} root
5806
+ * @returns {[boolean, boolean]}
5807
+ */
5808
+ function getLayout(root) {
5809
+ const style = getComputedStyle(root);
5810
+ const writingMode = style.writingMode?.toLowerCase();
5811
+ const vertical = verticalWritingMode.has(writingMode);
5812
+ const reverse = style.direction.toLowerCase() === 'rtl' !== (writingMode === 'sideways-lr');
5813
+ return [vertical, reverse];
5449
5814
  }
5450
5815
 
5451
- /** @import { StoreLayout } from '../types.mjs' */
5816
+ /**
5817
+ * @typedef {object} State
5818
+ * @property {number} level
5819
+ * @property {number} levelValue
5820
+ * @property {boolean} collapsed
5821
+ * @property {boolean} hidden
5822
+ * @property {boolean} droppable
5823
+ * @property {Set<number>} parents
5824
+ * @property {boolean} hasChildren
5825
+ */
5452
5826
 
5827
+ /**
5828
+ *
5829
+ * @param {ArrayStore} store
5830
+ * @param {State[]} states
5831
+ * @param {Signal.State<number>} drag
5832
+ * @param {string} levelKey
5833
+ * @param {*} index
5834
+ * @returns {State}
5835
+ */
5836
+ function createState(store, states, drag, levelKey, index) {
5837
+ const levelValue = new Signal.Computed(() => {
5838
+ const child = store.child(index);
5839
+ if (!child) { return 0; }
5840
+ return Math.max(0, Math.floor(child.value?.[levelKey]) || 0);
5841
+ });
5842
+ const parentIndex = new Signal.Computed(() => {
5843
+ const child = store.child(index);
5844
+ if (!child) { return -1; }
5845
+ const level = levelValue.get();
5846
+ if (!level) { return -1; }
5847
+ for (let k = index - 1; k >= 0; k--) {
5848
+ if (level > states[k]?.levelValue) { return k; }
5849
+ }
5850
+ return -1;
5851
+ });
5852
+ const hasChildren = new Signal.Computed(() => {
5853
+ const children = store.children;
5854
+ const next = children[index + 1];
5855
+ if (!next) { return false; }
5856
+ const child = children[index];
5857
+ if (!child) { return false; }
5858
+ const level = levelValue.get();
5859
+ return Math.floor(next.child(levelKey)?.value) > level;
5860
+ });
5861
+ const droppable = new Signal.Computed(() => {
5862
+ const dragRow = drag.get();
5863
+ if (dragRow === index) { return false; }
5864
+ const pIndex = parentIndex.get();
5865
+ const parent = states[pIndex];
5866
+ if (!parent) { return true; }
5867
+ return parent.droppable;
5868
+ });
5869
+ const level = new Signal.Computed(() => {
5870
+ const pIndex = parentIndex.get();
5871
+ const parent = states[pIndex];
5872
+ if (!parent) { return 0; }
5873
+ return parent.level + 1;
5874
+ });
5875
+ const collapsed = new Signal.State(false);
5876
+ const hidden = new Signal.Computed(() => {
5877
+ const pIndex = parentIndex.get();
5878
+ const parent = states[pIndex];
5879
+ if (!parent) { return false; }
5880
+ return parent.collapsed || parent.hidden;
5881
+ });
5882
+ /** @type {Signal.Computed<Set<number>>} */
5883
+ const parents = new Signal.Computed(() => {
5884
+ const pIndex = parentIndex.get();
5885
+ const parent = states[pIndex];
5886
+ if (!parent) { return new Set(); }
5887
+ const set = new Set(parent.parents);
5888
+ set.add(pIndex);
5889
+ return set;
5890
+ });
5891
+ return {
5892
+ get level() { return level.get(); },
5893
+ get levelValue() { return levelValue.get(); },
5894
+ get collapsed() { return collapsed.get(); },
5895
+ set collapsed(v) { collapsed.set(v); },
5896
+ get hidden() { return hidden.get(); },
5897
+ get hasChildren() { return hasChildren.get(); },
5898
+ get droppable() { return droppable.get(); },
5899
+ get parents() { return parents.get(); },
5900
+ };
5901
+ }
5453
5902
  /**
5454
5903
  *
5455
- * @param {StoreLayout.Item} layout
5456
- * @param {{label?: string | null; description?: string | null; required?: boolean | null}} [values]
5457
- * @param {string?} [defCell]
5458
- * @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]}
5904
+ * @param {ArrayStore} store
5905
+ * @param {StoreLayout.Renderer} fieldRenderer
5906
+ * @param {StoreLayout.Field?} layout
5907
+ * @param {StoreLayout.Options?} options
5908
+ * @returns {[HTMLElement, () => void]}
5459
5909
  */
5460
- function createCell(layout, values, defCell) {
5461
- switch (layout.cell || defCell) {
5462
- default:
5463
- case 'block': return createBaseCell(values);
5464
- case 'collapse': return createCollapseCell(values);
5465
- case 'inline': {
5466
- const result = createBaseCell(values);
5467
- bindGrid(result[0], layout);
5468
- return result;
5910
+ function Tree(store, fieldRenderer, layout, options) {
5911
+ const headerColumns = layout?.columns;
5912
+ const fieldList = Object.entries(store.type || {})
5913
+ .filter(([k, v]) => typeof v?.type !== 'object')
5914
+ .map(([field, { width, label }]) => ({ field, width, label }));
5915
+ /** @type {({ field: string; width: any; label: any; } | number | StoreLayout.Action[])[]} */
5916
+ let columns = [];
5917
+ if (Array.isArray(headerColumns)) {
5918
+ const map = new Map(fieldList.map(v => [v.field, v]));
5919
+ columns = headerColumns.map(v => {
5920
+ if (typeof v === 'number') { return v; }
5921
+ if (typeof v === 'string') { return map.get(v) || []; }
5922
+ if (!Array.isArray(v)) { return []; }
5923
+ /** @type {Set<StoreLayout.Action>} */
5924
+ const options = new Set(['add', 'move', 'trigger', 'remove', 'serial', 'open', 'collapse']);
5925
+ return v.filter(v => options.delete(v));
5926
+ }).filter(v => !Array.isArray(v) || v.length);
5927
+ }
5928
+ if (!columns.length) {
5929
+ columns = [['collapse', 'move'], fieldList[0], ['add', 'remove']];
5930
+ }
5931
+ if (!columns.find(v => !Array.isArray(v))) {
5932
+ columns.push(...fieldList.slice(0, 3));
5933
+ }
5934
+
5935
+
5936
+
5937
+ const root = document.createElement('div');
5938
+ root.className = 'NeeloongForm-tree';
5939
+ const main = root.appendChild(document.createElement('div'));
5940
+ main.className = 'NeeloongForm-tree-main';
5941
+ const splitter = root.appendChild(document.createElement('div'));
5942
+ splitter.className = 'NeeloongForm-tree-splitter';
5943
+ const details = root.appendChild(document.createElement('div'));
5944
+ details.className = 'NeeloongForm-tree-details';
5945
+ splitter.hidden = true;
5946
+ details.hidden = true;
5947
+ /** @type {number?} */
5948
+ let splitterPointerId = null;
5949
+ let splitterOffset = 0;
5950
+ function stopMove() {
5951
+ const pointerId = splitterPointerId;
5952
+ if (pointerId === null) { return; }
5953
+ splitterPointerId = null;
5954
+ splitter.releasePointerCapture(pointerId);
5955
+ }
5956
+
5957
+ splitter.addEventListener('pointerdown', e => {
5958
+ const { pointerId } = e;
5959
+ if (![null, pointerId].includes(splitterPointerId)) { return; }
5960
+ splitterPointerId = pointerId;
5961
+ splitter.setPointerCapture(pointerId);
5962
+ switch (getLayout(splitter).map((v, i) => v ? 2 ** i : 0).reduce((a, b) => a + b)) {
5963
+ case 0: splitterOffset = -e.offsetX; break;
5964
+ case 1: splitterOffset = -e.offsetY; break;
5965
+ case 2: splitterOffset = e.offsetX - splitter.offsetWidth; break;
5966
+ case 3: splitterOffset = e.offsetY - splitter.offsetHeight; break;
5967
+ }
5968
+ });
5969
+ /**
5970
+ * @param {boolean} vertical
5971
+ * @returns {number}
5972
+ */
5973
+ const getSize = vertical => vertical
5974
+ ? root.clientHeight - splitter.offsetHeight
5975
+ : root.clientWidth - splitter.offsetWidth;
5976
+ /** @param {PointerEvent} e */
5977
+ const updateMove = e => {
5978
+ const [vertical, reverse] = getLayout(splitter);
5979
+ let os = 0;
5980
+ switch ([vertical, reverse].map((v, i) => v ? 2 ** i : 0).reduce((a, b) => a + b)) {
5981
+ case 0: os = e.clientX - root.getBoundingClientRect().left; break;
5982
+ case 1: os = e.clientY - root.getBoundingClientRect().top; break;
5983
+ case 2: os = root.getBoundingClientRect().right - e.clientX; break;
5984
+ case 3: os = root.getBoundingClientRect().bottom - e.clientY; break;
5985
+ }
5986
+ const value = 1 - Math.max(0, Math.min((os + splitterOffset) / getSize(vertical), 1));
5987
+ details.style.inlineSize = `${value * 100}%`;
5988
+ };
5989
+ splitter.addEventListener('pointermove', e => {
5990
+ const { pointerId } = e;
5991
+ if (!splitter.hasPointerCapture(pointerId)) { return; }
5992
+ updateMove(e);
5993
+ });
5994
+ splitter.addEventListener('pointerup', e => {
5995
+ const { pointerId } = e;
5996
+ if (pointerId !== splitterPointerId) { return; }
5997
+ updateMove(e);
5998
+ stopMove();
5999
+ });
6000
+ splitter.addEventListener('pointercancel', e => {
6001
+ const { pointerId } = e;
6002
+ if (pointerId !== splitterPointerId) { return; }
6003
+ updateMove(e);
6004
+ stopMove();
6005
+ });
6006
+
6007
+
6008
+ /** @type {(() => void)[]} */
6009
+ const destroyList = [];
6010
+ let destroyDetails = () => { };
6011
+ const detailsStore = new Signal.State(/** @type{Store<any, any>?}*/(null));
6012
+ /**
6013
+ *
6014
+ * @param {Store<any, any>} store
6015
+ * @returns
6016
+ */
6017
+ function createDetails(store) {
6018
+ if (detailsStore.get() === store) { return destroyDetails; }
6019
+ destroyDetails();
6020
+ detailsStore.set(store);
6021
+ const [form, destroy] = Form(store, fieldRenderer, layout, options);
6022
+ details.appendChild(form);
6023
+ details.hidden = false;
6024
+ splitter.hidden = false;
6025
+ let done = false;
6026
+ destroyDetails = () => {
6027
+ if (done) { return; }
6028
+ done = true;
6029
+ detailsStore.set(null);
6030
+ form.remove();
6031
+ destroy();
6032
+ stopMove();
6033
+ splitter.hidden = true;
6034
+ details.hidden = true;
6035
+ };
6036
+ return destroyDetails;
6037
+
6038
+ }
6039
+
6040
+
6041
+ const levelKey = layout?.levelKey || 'level';
6042
+ const addable = new Signal.Computed(() => store.addable);
6043
+ const deletable = { get: () => Boolean(options?.editable) };
6044
+ /**
6045
+ *
6046
+ * @param {number} parent
6047
+ */
6048
+ function addNode(parent) {
6049
+ /** @type {Record<string, any>} */
6050
+ const data = {};
6051
+ if (parent === -2) {
6052
+ data[levelKey] = 0;
6053
+ store.add(data);
6054
+ return;
6055
+ }
6056
+ data[levelKey] = (states[parent]?.levelValue ?? -1) + 1;
6057
+ store.insert(parent + 1, data);
6058
+
6059
+ }
6060
+ /**
6061
+ *
6062
+ * @param {Store} child
6063
+ */
6064
+ function remove(child) {
6065
+ store.remove(Number(child.index));
6066
+ }
6067
+ let dragRow = -1;
6068
+ let drag = new Signal.State(dragRow);
6069
+ /**
6070
+ *
6071
+ * @param {Store} [child]
6072
+ * @param {boolean} [inChildren]
6073
+ */
6074
+ function getLevel(child, inChildren) {
6075
+ if (!child) { return 0; }
6076
+ const newLevel = Number(states[Number(child.index)]?.levelValue) || 0;
6077
+ if (!inChildren) { return newLevel; }
6078
+ return newLevel + 1;
6079
+ }
6080
+ /**
6081
+ *
6082
+ * @param {number} start
6083
+ */
6084
+ function getQuantity(start) {
6085
+ let last = start + 1;
6086
+ const level = states[dragRow]?.level ?? 0;
6087
+ for (; (states[last]?.level ?? -1) > level; last++) { }
6088
+ return last - dragRow;
6089
+ }
6090
+ /**
6091
+ *
6092
+ * @param {Store} [child]
6093
+ * @param {boolean} [inChildren]
6094
+ */
6095
+ function drop(child, inChildren) {
6096
+ if (dragRow < 0) { return; }
6097
+ const newLevel = Number(getLevel(child, inChildren)) || 0;
6098
+ let quantity = getQuantity(dragRow);
6099
+ let index = -1;
6100
+ if (child) {
6101
+ index = Number(child.index);
6102
+ if (dragRow === index || states[index]?.parents?.has(dragRow)) { return; }
6103
+ if (inChildren || index !== dragRow + quantity) {
6104
+ if (inChildren) {
6105
+ for (const currentLevel = states[index]?.level || 0; states[index + 1]?.level > currentLevel; index++);
6106
+ if (index < dragRow) { index += 1; }
6107
+ } else {
6108
+ if (index > dragRow) { index -= 1; }
6109
+ }
6110
+ if (index < 0) { return; }
6111
+ } else {
6112
+ index = -1;
6113
+ }
6114
+
6115
+ } else {
6116
+ index = states.length - 1;
6117
+ if (index < 0) { return; }
6118
+ }
6119
+ if (index >= 0 && dragRow !== index) {
6120
+ quantity = store.move(dragRow, index, quantity);
6121
+ if (!quantity) { return; }
6122
+ dragRow = quantity > 1 && index > dragRow ? index - quantity + 1 : index;
6123
+ drag.set(dragRow);
6124
+ }
6125
+ const levelSub = Array(quantity).fill(states[dragRow]?.level ?? 0).map((l, i) => Math.max((states[i + dragRow]?.level ?? 0) - l, 0));
6126
+ for (let i = 0; i < quantity; i++) {
6127
+ const c = store.children[dragRow + i]?.child(levelKey);
6128
+ if (c) {
6129
+ c.value = newLevel + levelSub[i];
6130
+ }
5469
6131
  }
5470
- case 'base': {
5471
- const result = createNullCell(values);
5472
- bindGrid(result[0], layout);
5473
- return result;
6132
+
6133
+
6134
+ }
6135
+ /**
6136
+ *
6137
+ * @param {Store} child
6138
+ */
6139
+ function dragstart(child) {
6140
+ dragRow = Number(child.index);
6141
+ drag.set(dragRow);
6142
+ main.classList.add('NeeloongForm-tree-moving');
6143
+
6144
+ }
6145
+ /** @type {HTMLElement?} */
6146
+ let dragenterEl = null;
6147
+ /** @param {HTMLElement} el */
6148
+ function dragenter(el) {
6149
+ if (dragRow === -1) { return () => { }; }
6150
+ dragenterEl?.classList.remove('NeeloongForm-tree-drag-over');
6151
+ dragenterEl = el;
6152
+ dragenterEl?.classList.add('NeeloongForm-tree-drag-over');
6153
+ return () => {
6154
+ if (dragenterEl !== el) { return; }
6155
+ dragenterEl?.classList.remove('NeeloongForm-tree-drag-over');
6156
+ dragenterEl = null;
6157
+ };
6158
+
6159
+ }
6160
+ function dragend() {
6161
+ dragRow = -1;
6162
+ drag.set(dragRow);
6163
+ main.classList.remove('NeeloongForm-tree-moving');
6164
+ dragenterEl?.classList.remove('NeeloongForm-tree-drag-over');
6165
+ dragenterEl = null;
6166
+
6167
+ }
6168
+ if (options?.editable) {
6169
+ const button = main.appendChild(document.createElement('button'));
6170
+ button.addEventListener('click', () => addNode(-1));
6171
+ button.classList.add('NeeloongForm-tree-head-add');
6172
+ destroyList.push(watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true));
6173
+ }
6174
+ const start = main.appendChild(document.createComment(''));
6175
+ if (options?.editable) {
6176
+ const foot = main.appendChild(document.createElement('div'));
6177
+ foot.classList.add('NeeloongForm-tree-foot');
6178
+ const button = foot.appendChild(document.createElement('button'));
6179
+ button.addEventListener('click', () => addNode(-2));
6180
+ button.classList.add('NeeloongForm-tree-foot-add');
6181
+ const dropFront = foot.appendChild(document.createElement('div'));
6182
+ dropFront.classList.add('NeeloongForm-tree-drop');
6183
+
6184
+
6185
+ let dragleave = () => { };
6186
+ dropFront.addEventListener('dragover', (e) => e.preventDefault());
6187
+ dropFront.addEventListener('dragenter', () => dragleave = dragenter(dropFront));
6188
+ dropFront.addEventListener('dragleave', () => dragleave());
6189
+ dropFront.addEventListener('drop', () => drop());
6190
+
6191
+
6192
+ destroyList.push(watch(() => !addable.get(), disabled => { button.disabled = disabled; }, true));
6193
+ }
6194
+ /** @type {Map<Store, [HTMLElement, () => void, (s: State) => void]>} */
6195
+ let seMap = new Map();
6196
+ /** @param {Map<Store, [tbody: HTMLElement, destroy: () => void, (s: State) => void]>} map */
6197
+ function destroyMap(map) {
6198
+ for (const [el, destroy] of map.values()) {
6199
+ destroy();
6200
+ el.remove();
5474
6201
  }
6202
+
6203
+ }
6204
+ /** @type {State[]} */
6205
+ const states = [];
6206
+ const columnNames = columns.map((v) => Array.isArray(v) || typeof v === 'number' ? v : v.field);
6207
+ const childrenResult = watch(() => store.children, function render(children) {
6208
+ let nextNode = start.nextSibling;
6209
+ const oldSeMap = seMap;
6210
+ seMap = new Map();
6211
+ const childrenLength = children.length;
6212
+ for (let i = states.length; i < childrenLength; i++) {
6213
+ states[i] = createState(store, states, drag, 'level', i);
6214
+ }
6215
+
6216
+ let i = -1;
6217
+ for (let child of children) {
6218
+ i++;
6219
+ const state = states[i];
6220
+ const old = oldSeMap.get(child);
6221
+ if (!old) {
6222
+ const [el, destroy, setState] = TreeLine(child, detailsStore, fieldRenderer, layout, state, {
6223
+ columns: columnNames,
6224
+ remove: remove.bind(null, child),
6225
+ dragenter,
6226
+ dragstart: dragstart.bind(null, child),
6227
+ dragend,
6228
+ deletable,
6229
+ addNode: () => addNode(Number(child.index)),
6230
+ createDetails,
6231
+ drop: drop.bind(null, child),
6232
+ }, options);
6233
+ main.insertBefore(el, nextNode);
6234
+ seMap.set(child, [el, destroy, setState]);
6235
+ continue;
6236
+ }
6237
+ oldSeMap.delete(child);
6238
+ seMap.set(child, old);
6239
+ old[2](state);
6240
+ if (nextNode === old[0]) {
6241
+ nextNode = nextNode.nextSibling;
6242
+ continue;
6243
+ }
6244
+ main.insertBefore(old[0], nextNode);
6245
+ }
6246
+ states.splice(childrenLength);
6247
+ destroyMap(oldSeMap);
6248
+ }, true);
6249
+
6250
+ return [root, () => {
6251
+ start.remove();
6252
+ destroyMap(seMap);
6253
+ childrenResult();
6254
+ destroyDetails?.();
6255
+ for (const destroy of destroyList) {
6256
+ destroy();
6257
+ }
6258
+ }];
6259
+ }
6260
+
6261
+ /**
6262
+ *
6263
+ * @param {string | ParentNode} html
6264
+ * @param {Store<any, any>} store
6265
+ * @param {StoreLayout.Renderer} fieldRenderer
6266
+ * @param {StoreLayout.Options?} options
6267
+ * @param {StoreLayout.Field?} layout
6268
+ * @returns
6269
+ */
6270
+ function Html(html, store, fieldRenderer, options, layout) {
6271
+ const htmlContent = getHtmlContent(html);
6272
+ const destroy = renderHtml(store, fieldRenderer, htmlContent, options, layout);
6273
+ return [htmlContent, destroy];
6274
+ }
6275
+
6276
+ /**
6277
+ *
6278
+ * @param {StoreLayout.Field['arrayStyle']?} arrayStyle
6279
+ */
6280
+ function getArrayCell(arrayStyle) {
6281
+ switch(arrayStyle) {
6282
+ case 'tree': return Tree;
6283
+ default: return Table;
5475
6284
  }
5476
6285
 
5477
6286
  }
@@ -5500,46 +6309,22 @@ function FormField(store, fieldRenderer, layout, options, inline = false) {
5500
6309
  }
5501
6310
  const isObject = type && typeof type === 'object';
5502
6311
  const html = layout?.html;
5503
- if (html) {
5504
- const [root, destroy, content, destroyList] = createCell(layout, store, isObject ? 'collapse' : 'base');
5505
- const htmlContent = getHtmlContent(html);
5506
- destroyList.push(renderHtml(store, fieldRenderer, htmlContent, options, layout));
5507
- content.appendChild(htmlContent);
5508
- return [root, destroy];
5509
- }
5510
- if (isObject) {
5511
- const [root, destroy, content, destroyList] = createCollapseCell(store);
5512
- destroyList.push(effect(() => root.hidden = store.hidden));
5513
- if (typeof component === 'function') {
5514
- const r = fieldRenderer(store, component, options);
5515
- if (r) {
5516
- const [el, destroy] = r;
5517
- content.appendChild(el);
5518
- destroyList.push(destroy);
5519
- }
5520
- } else if (store instanceof ArrayStore) {
5521
- const [table, destroy] = Table(store, fieldRenderer, layout, options);
5522
- content.appendChild(table);
5523
- destroyList.push(destroy);
5524
- } else {
5525
- const [form, destroy] = Form(store, fieldRenderer, layout, options);
5526
- content.appendChild(form);
5527
- destroyList.push(destroy);
5528
- }
5529
- return [root, destroy];
5530
- }
5531
-
5532
-
5533
- const [root, destroy, content, destroyList] = createNullCell(store);
5534
- bindGrid(root, layout);
6312
+ /** @type {StoreLayout.Grid['cell']} */
6313
+ const cellType = isObject
6314
+ ? store instanceof ArrayStore ? 'collapse' : 'fieldset'
6315
+ : html ? 'base' : 'inline';
6316
+ const [root, destroy, content, destroyList] = createCell(layout, store, cellType, isObject);
5535
6317
  destroyList.push(effect(() => root.hidden = store.hidden));
5536
- if (typeof component === 'function') {
5537
- const r = fieldRenderer(store, component, options);
5538
- if (r) {
5539
- const [el, destroy] = r;
5540
- content.appendChild(el);
5541
- destroyList.push(destroy);
5542
- }
6318
+
6319
+ const r =
6320
+ html && Html(html, store, fieldRenderer, options, layout)
6321
+ || typeof component === 'function' && fieldRenderer(store, component, options)
6322
+ || store instanceof ArrayStore && getArrayCell(layout?.arrayStyle)(store, fieldRenderer, layout, options)
6323
+ || isObject && Form(store, fieldRenderer, layout, options);
6324
+ if (r) {
6325
+ const [el, destroy] = r;
6326
+ content.appendChild(el);
6327
+ destroyList.push(destroy);
5543
6328
  }
5544
6329
  return [root, destroy];
5545
6330
  }
@@ -5555,9 +6340,18 @@ function FormField(store, fieldRenderer, layout, options, inline = false) {
5555
6340
  * @returns {[ParentNode, () => void]}
5556
6341
  */
5557
6342
  function FormButton(store, layout, options) {
5558
- const [root, destroy, content] = createCell(layout, store);
6343
+ const [root, destroy, content, destroyList] = createCell(layout, store);
5559
6344
  const button = document.createElement('button');
5560
- button.innerText = layout.text || '';
6345
+ destroyList.push(() => {
6346
+ const t = layout.text;
6347
+ const text = typeof t === 'function' ? t(store, options) : t;
6348
+ button.innerText = text ?? '';
6349
+ });
6350
+ destroyList.push(() => {
6351
+ const d = layout.disabled;
6352
+ const disabled = typeof d === 'function' ? d(store, options) : d;
6353
+ button.disabled = Boolean(disabled);
6354
+ });
5561
6355
  button.className = 'NeeloongForm-item-button';
5562
6356
  content.appendChild(button);
5563
6357
  const click = layout.click;