@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.d.mts +22 -8
- package/index.full.js +937 -143
- package/index.full.min.js +6 -6
- package/index.full.min.mjs +6 -6
- package/index.min.mjs +2 -2
- package/index.mjs +937 -143
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @neeloong/form v0.
|
|
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
|
-
|
|
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
|
|
1238
|
+
if (!Array.isArray(data)) { return 0; }
|
|
1239
|
+
|
|
1234
1240
|
const children = [...this.#children.get()];
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
|
1245
|
-
|
|
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
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
5469
|
+
/** @import { CellValues } from './createCell.mjs' */
|
|
5368
5470
|
|
|
5369
5471
|
/**
|
|
5370
5472
|
*
|
|
5371
|
-
* @param {
|
|
5372
|
-
* @
|
|
5473
|
+
* @param {CellValues} [values]
|
|
5474
|
+
* @returns {[HTMLDivElement, () => void, HTMLDivElement, (() => void)[]]}
|
|
5373
5475
|
*/
|
|
5374
|
-
function
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
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 {
|
|
5400
|
-
* @returns {[
|
|
5498
|
+
* @param {CellValues} [values]
|
|
5499
|
+
* @returns {[HTMLElement, () => void, HTMLElement, (() => void)[]]}
|
|
5401
5500
|
*/
|
|
5402
|
-
function
|
|
5501
|
+
function createFieldsetCell(values) {
|
|
5403
5502
|
/** @type {(() => void)[]} */
|
|
5404
5503
|
const destroyList = [];
|
|
5405
|
-
const root = document.createElement('
|
|
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 {
|
|
5422
|
-
* @
|
|
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
|
|
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
|
-
|
|
5428
|
-
root.className = 'NeeloongForm-item';
|
|
5429
|
-
destroyList.push(bindRequired(root, values));
|
|
5610
|
+
root.classList.add('NeeloongForm-tree-item');
|
|
5430
5611
|
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
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
|
-
|
|
5436
|
-
content.className = 'NeeloongForm-item-content';
|
|
5624
|
+
destroyList.push(effect(() => { root.hidden = state.get().hidden; }));
|
|
5437
5625
|
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
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
|
-
},
|
|
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
|
-
/**
|
|
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 {
|
|
5456
|
-
* @param {
|
|
5457
|
-
* @param {
|
|
5458
|
-
* @
|
|
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
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
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
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
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
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
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
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
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
|
-
|
|
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;
|