2024-02-21 19:22:01 +00:00
|
|
|
import {FormLayoutNode, FormLayoutNodeType} from 'app/client/components/FormRenderer';
|
2023-12-12 09:58:20 +00:00
|
|
|
import * as elements from 'app/client/components/Forms/elements';
|
2024-01-18 17:23:50 +00:00
|
|
|
import {FormView} from 'app/client/components/Forms/FormView';
|
2024-04-11 06:50:30 +00:00
|
|
|
import {MaybePromise} from 'app/plugin/gutil';
|
|
|
|
import {
|
|
|
|
bundleChanges,
|
|
|
|
Computed,
|
|
|
|
Disposable,
|
|
|
|
dom,
|
|
|
|
IDomArgs,
|
|
|
|
MutableObsArray,
|
|
|
|
obsArray,
|
|
|
|
Observable,
|
|
|
|
} from 'grainjs';
|
2023-12-12 09:58:20 +00:00
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
type Callback = () => Promise<void>;
|
2023-12-12 09:58:20 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A place where to insert a box.
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
export type Place = (box: FormLayoutNode) => BoxModel;
|
2023-12-12 09:58:20 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* View model constructed from a box JSON structure.
|
|
|
|
*/
|
|
|
|
export abstract class BoxModel extends Disposable {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A factory method that creates a new BoxModel from a Box JSON by picking the right class based on the type.
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
public static new(box: FormLayoutNode, parent: BoxModel | null, view: FormView | null = null): BoxModel {
|
2023-12-12 09:58:20 +00:00
|
|
|
const subClassName = `${box.type.split(':')[0]}Model`;
|
|
|
|
const factories = elements as any;
|
|
|
|
const factory = factories[subClassName];
|
2024-01-18 17:23:50 +00:00
|
|
|
if (!parent && !view) { throw new Error('Cannot create detached box'); }
|
2023-12-12 09:58:20 +00:00
|
|
|
// If we have a factory, use it.
|
|
|
|
if (factory) {
|
|
|
|
return new factory(box, parent, view || parent!.view);
|
|
|
|
}
|
|
|
|
// Otherwise, use the default.
|
|
|
|
return new DefaultBoxModel(box, parent, view || parent!.view);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-03-20 14:51:59 +00:00
|
|
|
* The unique id of the box.
|
2023-12-12 09:58:20 +00:00
|
|
|
*/
|
|
|
|
public id: string;
|
|
|
|
/**
|
|
|
|
* Type of the box. As the type is bounded to the class that is used to render the box, it is possible
|
|
|
|
* to change the type of the box just by changing this value. The box is then replaced in the parent.
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
public type: FormLayoutNodeType;
|
2023-12-12 09:58:20 +00:00
|
|
|
/**
|
|
|
|
* List of children boxes.
|
|
|
|
*/
|
|
|
|
public children: MutableObsArray<BoxModel>;
|
|
|
|
/**
|
|
|
|
* Publicly exposed state if the element was just cut.
|
|
|
|
* TODO: this should be moved to FormView, as this model doesn't care about that.
|
|
|
|
*/
|
|
|
|
public cut = Observable.create(this, false);
|
|
|
|
|
2024-01-24 16:14:34 +00:00
|
|
|
/**
|
|
|
|
* Computed if this box is selected or not.
|
|
|
|
*/
|
|
|
|
public selected: Computed<boolean>;
|
2024-01-23 20:52:57 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Any other dynamically added properties (that are not concrete fields in the derived classes)
|
|
|
|
*/
|
|
|
|
private _props: Record<string, Observable<any>> = {};
|
2023-12-12 09:58:20 +00:00
|
|
|
/**
|
|
|
|
* Don't use it directly, use the BoxModel.new factory method instead.
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
constructor(box: FormLayoutNode, public parent: BoxModel | null, public view: FormView) {
|
2023-12-12 09:58:20 +00:00
|
|
|
super();
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
this.selected = Computed.create(this, (use) => use(view.selectedBox) === this && use(view.viewSection.hasFocus));
|
|
|
|
|
|
|
|
this.children = this.autoDispose(obsArray([]));
|
|
|
|
|
|
|
|
// We are owned by the parent children list.
|
|
|
|
if (parent) {
|
|
|
|
parent.children.autoDispose(this);
|
|
|
|
}
|
|
|
|
|
2024-03-20 14:51:59 +00:00
|
|
|
this.id = box.id;
|
2023-12-12 09:58:20 +00:00
|
|
|
|
|
|
|
// Create observables for all properties.
|
2024-01-18 17:23:50 +00:00
|
|
|
this.type = box.type;
|
2023-12-12 09:58:20 +00:00
|
|
|
|
|
|
|
// And now update this and all children based on the box JSON.
|
|
|
|
bundleChanges(() => {
|
|
|
|
this.update(box);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Some boxes need to do some work after initialization, so we call this method.
|
|
|
|
// Of course, they also can override the constructor, but this is a bit easier.
|
|
|
|
this.onCreate();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The only method that derived classes need to implement. It should return a DOM element that
|
|
|
|
* represents this box.
|
|
|
|
*/
|
2024-01-18 17:23:50 +00:00
|
|
|
public abstract render(...args: IDomArgs<HTMLElement>): HTMLElement;
|
2023-12-12 09:58:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
public removeChild(box: BoxModel) {
|
|
|
|
const myIndex = this.children.get().indexOf(box);
|
|
|
|
if (myIndex < 0) { throw new Error('Cannot remove box that is not in parent'); }
|
|
|
|
this.children.splice(myIndex, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove self from the parent without saving.
|
|
|
|
*/
|
|
|
|
public removeSelf() {
|
|
|
|
this.parent?.removeChild(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove self from the parent and save. Use to bundle layout save with any other changes.
|
|
|
|
* See Fields for the implementation.
|
|
|
|
* TODO: this is needed as action bundling is very limited.
|
|
|
|
*/
|
|
|
|
public async deleteSelf() {
|
|
|
|
const parent = this.parent;
|
|
|
|
this.removeSelf();
|
|
|
|
await parent!.save();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-03-20 14:51:59 +00:00
|
|
|
* Copies self and puts it into clipboard.
|
2023-12-12 09:58:20 +00:00
|
|
|
*/
|
2024-03-20 14:51:59 +00:00
|
|
|
public async copySelf() {
|
2024-01-24 16:14:34 +00:00
|
|
|
[...this.root().traverse()].forEach(box => box?.cut.set(false));
|
2023-12-12 09:58:20 +00:00
|
|
|
// Add this box as a json to clipboard.
|
|
|
|
await navigator.clipboard.writeText(JSON.stringify(this.toJSON()));
|
2024-03-20 14:51:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cuts self and puts it into clipboard.
|
|
|
|
*/
|
|
|
|
public async cutSelf() {
|
|
|
|
await this.copySelf();
|
2023-12-12 09:58:20 +00:00
|
|
|
this.cut.set(true);
|
|
|
|
}
|
|
|
|
|
2024-01-24 16:14:34 +00:00
|
|
|
/**
|
|
|
|
* The way this box will accept dropped content.
|
|
|
|
* - sibling: it will add it as a sibling
|
|
|
|
* - child: it will add it as a child.
|
|
|
|
* - swap: swaps with the box
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
public willAccept(box?: FormLayoutNode|BoxModel|null): 'sibling' | 'child' | 'swap' | null {
|
2024-01-24 16:14:34 +00:00
|
|
|
// If myself and the dropped element share the same parent, and the parent is a column
|
|
|
|
// element, just swap us.
|
|
|
|
if (this.parent && box instanceof BoxModel && this.parent === box?.parent && box.parent?.type === 'Columns') {
|
|
|
|
return 'swap';
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are in column, we won't accept anything.
|
|
|
|
if (this.parent?.type === 'Columns') { return null; }
|
|
|
|
|
|
|
|
return 'sibling';
|
|
|
|
}
|
|
|
|
|
2023-12-12 09:58:20 +00:00
|
|
|
/**
|
|
|
|
* Accepts box from clipboard and inserts it before this box or if this is a container box, then
|
|
|
|
* as a first child. Default implementation is to insert before self.
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
public accept(dropped: FormLayoutNode, hint: 'above'|'below' = 'above') {
|
2023-12-12 09:58:20 +00:00
|
|
|
// Get the box that was dropped.
|
|
|
|
if (!dropped) { return null; }
|
|
|
|
if (dropped.id === this.id) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
// We need to remove it from the parent, so find it first.
|
|
|
|
const droppedId = dropped.id;
|
2024-01-24 16:14:34 +00:00
|
|
|
const droppedRef = droppedId ? this.root().find(droppedId) : null;
|
2023-12-12 09:58:20 +00:00
|
|
|
if (droppedRef) {
|
|
|
|
droppedRef.removeSelf();
|
|
|
|
}
|
2024-01-18 17:23:50 +00:00
|
|
|
return hint === 'above' ? this.placeBeforeMe()(dropped) : this.placeAfterMe()(dropped);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public prop(name: string, defaultValue?: any) {
|
2024-01-23 20:52:57 +00:00
|
|
|
if (!this._props[name]) {
|
|
|
|
this._props[name] = Observable.create(this, defaultValue ?? null);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
2024-01-23 20:52:57 +00:00
|
|
|
return this._props[name];
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
public hasProp(name: string) {
|
2024-01-23 20:52:57 +00:00
|
|
|
return this._props.hasOwnProperty(name);
|
2024-01-18 17:23:50 +00:00
|
|
|
}
|
|
|
|
|
2024-04-11 06:50:30 +00:00
|
|
|
public async save(before?: () => MaybePromise<void>): Promise<void> {
|
2023-12-12 09:58:20 +00:00
|
|
|
if (!this.parent) { throw new Error('Cannot save detached box'); }
|
2024-01-18 17:23:50 +00:00
|
|
|
return this.parent.save(before);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replaces children at index.
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
public replaceAtIndex(box: FormLayoutNode, index: number) {
|
2023-12-12 09:58:20 +00:00
|
|
|
const newOne = BoxModel.new(box, this);
|
|
|
|
this.children.splice(index, 1, newOne);
|
|
|
|
return newOne;
|
|
|
|
}
|
|
|
|
|
2024-01-24 16:14:34 +00:00
|
|
|
public swap(box1: BoxModel, box2: BoxModel) {
|
|
|
|
const index1 = this.children.get().indexOf(box1);
|
|
|
|
const index2 = this.children.get().indexOf(box2);
|
|
|
|
if (index1 < 0 || index2 < 0) { throw new Error('Cannot swap boxes that are not in parent'); }
|
|
|
|
const box1JSON = box1.toJSON();
|
|
|
|
const box2JSON = box2.toJSON();
|
|
|
|
this.replace(box1, box2JSON);
|
|
|
|
this.replace(box2, box1JSON);
|
|
|
|
}
|
|
|
|
|
2024-02-21 19:22:01 +00:00
|
|
|
public append(box: FormLayoutNode) {
|
2023-12-12 09:58:20 +00:00
|
|
|
const newOne = BoxModel.new(box, this);
|
|
|
|
this.children.push(newOne);
|
|
|
|
return newOne;
|
|
|
|
}
|
|
|
|
|
2024-02-21 19:22:01 +00:00
|
|
|
public insert(box: FormLayoutNode, index: number) {
|
2023-12-12 09:58:20 +00:00
|
|
|
const newOne = BoxModel.new(box, this);
|
|
|
|
this.children.splice(index, 0, newOne);
|
|
|
|
return newOne;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replaces existing box with a new one, whenever it is found.
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
public replace(existing: BoxModel, newOne: FormLayoutNode|BoxModel) {
|
2023-12-12 09:58:20 +00:00
|
|
|
const index = this.children.get().indexOf(existing);
|
|
|
|
if (index < 0) { throw new Error('Cannot replace box that is not in parent'); }
|
|
|
|
const model = newOne instanceof BoxModel ? newOne : BoxModel.new(newOne, this);
|
|
|
|
model.parent = this;
|
|
|
|
model.view = this.view;
|
|
|
|
this.children.splice(index, 1, model);
|
|
|
|
return model;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a place to insert a box before this box.
|
|
|
|
*/
|
|
|
|
public placeBeforeFirstChild() {
|
2024-02-21 19:22:01 +00:00
|
|
|
return (box: FormLayoutNode) => this.insert(box, 0);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Some other places.
|
|
|
|
public placeAfterListChild() {
|
2024-02-21 19:22:01 +00:00
|
|
|
return (box: FormLayoutNode) => this.insert(box, this.children.get().length);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public placeAt(index: number) {
|
2024-02-21 19:22:01 +00:00
|
|
|
return (box: FormLayoutNode) => this.insert(box, index);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public placeAfterChild(child: BoxModel) {
|
2024-02-21 19:22:01 +00:00
|
|
|
return (box: FormLayoutNode) => this.insert(box, this.children.get().indexOf(child) + 1);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public placeAfterMe() {
|
|
|
|
return this.parent!.placeAfterChild(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
public placeBeforeMe() {
|
|
|
|
return this.parent!.placeAt(this.parent!.children.get().indexOf(this));
|
|
|
|
}
|
|
|
|
|
|
|
|
public insertAfter(json: any) {
|
|
|
|
return this.parent!.insert(json, this.parent!.children.get().indexOf(this) + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
public insertBefore(json: any) {
|
|
|
|
return this.parent!.insert(json, this.parent!.children.get().indexOf(this));
|
|
|
|
}
|
|
|
|
|
|
|
|
public root() {
|
|
|
|
let root: BoxModel = this;
|
|
|
|
while (root.parent) { root = root.parent; }
|
|
|
|
return root;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds a box with a given id in the tree.
|
|
|
|
*/
|
2024-01-24 16:14:34 +00:00
|
|
|
public find(droppedId: string|undefined|null): BoxModel | null {
|
|
|
|
if (!droppedId) { return null; }
|
2023-12-12 09:58:20 +00:00
|
|
|
for (const child of this.kids()) {
|
|
|
|
if (child.id === droppedId) { return child; }
|
2024-01-24 16:14:34 +00:00
|
|
|
const found = child.find(droppedId);
|
2023-12-12 09:58:20 +00:00
|
|
|
if (found) { return found; }
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
public* filter(filter: (box: BoxModel) => boolean): Iterable<BoxModel> {
|
|
|
|
for (const child of this.kids()) {
|
|
|
|
if (filter(child)) { yield child; }
|
|
|
|
yield* child.filter(filter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public includes(box: BoxModel) {
|
|
|
|
for (const child of this.kids()) {
|
|
|
|
if (child === box) { return true; }
|
|
|
|
if (child.includes(box)) { return true; }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-12 09:58:20 +00:00
|
|
|
public kids() {
|
|
|
|
return this.children.get().filter(Boolean);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The core responsibility of this method is to update this box and all children based on the box JSON.
|
|
|
|
* This is counterpart of the FloatingRowModel, that enables this instance to point to a different box.
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
public update(boxDef: FormLayoutNode) {
|
2023-12-12 09:58:20 +00:00
|
|
|
// If we have a type and the type is changed, then we need to replace the box.
|
2024-01-18 17:23:50 +00:00
|
|
|
if (this.type && boxDef.type !== this.type) {
|
|
|
|
if (!this.parent) { throw new Error('Cannot replace detached box'); }
|
|
|
|
this.parent.replace(this, BoxModel.new(boxDef, this.parent));
|
2023-12-12 09:58:20 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update all properties of self.
|
2024-01-23 20:52:57 +00:00
|
|
|
for (const someKey in boxDef) {
|
2024-02-21 19:22:01 +00:00
|
|
|
const key = someKey as keyof FormLayoutNode;
|
2024-01-18 17:23:50 +00:00
|
|
|
// Skip some keys.
|
2023-12-12 09:58:20 +00:00
|
|
|
if (key === 'id' || key === 'type' || key === 'children') { continue; }
|
2024-01-18 17:23:50 +00:00
|
|
|
// Skip any inherited properties.
|
2023-12-12 09:58:20 +00:00
|
|
|
if (!boxDef.hasOwnProperty(key)) { continue; }
|
2024-01-18 17:23:50 +00:00
|
|
|
// Skip if the value is the same.
|
2023-12-12 09:58:20 +00:00
|
|
|
if (this.prop(key).get() === boxDef[key]) { continue; }
|
|
|
|
this.prop(key).set(boxDef[key]);
|
|
|
|
}
|
|
|
|
|
2024-03-20 14:51:59 +00:00
|
|
|
// First remove any children from the model that aren't in `boxDef`.
|
|
|
|
const boxDefChildren = boxDef.children ?? [];
|
|
|
|
const boxDefChildrenIds = new Set(boxDefChildren.map(c => c.id));
|
|
|
|
for (const child of this.children.get()) {
|
|
|
|
if (!boxDefChildrenIds.has(child.id)) {
|
|
|
|
child.removeSelf();
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-20 14:51:59 +00:00
|
|
|
// Then add or update the children from `boxDef` to the model.
|
|
|
|
const newChildren: BoxModel[] = [];
|
|
|
|
const modelChildrenById = new Map(this.children.get().map(c => [c.id, c]));
|
|
|
|
for (const boxDefChild of boxDefChildren) {
|
|
|
|
if (!boxDefChild.id || !modelChildrenById.has(boxDefChild.id)) {
|
|
|
|
newChildren.push(BoxModel.new(boxDefChild, this));
|
|
|
|
} else {
|
|
|
|
const existingChild = modelChildrenById.get(boxDefChild.id)!;
|
|
|
|
existingChild.update(boxDefChild);
|
|
|
|
newChildren.push(existingChild);
|
|
|
|
}
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
2024-03-20 14:51:59 +00:00
|
|
|
this.children.set(newChildren);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Serialize this box to JSON.
|
|
|
|
*/
|
2024-02-21 19:22:01 +00:00
|
|
|
public toJSON(): FormLayoutNode {
|
2023-12-12 09:58:20 +00:00
|
|
|
return {
|
|
|
|
id: this.id,
|
2024-01-18 17:23:50 +00:00
|
|
|
type: this.type,
|
2023-12-12 09:58:20 +00:00
|
|
|
children: this.children.get().map(child => child?.toJSON() || null),
|
2024-01-23 20:52:57 +00:00
|
|
|
...(Object.fromEntries(Object.entries(this._props).map(([key, val]) => [key, val.get()]))),
|
2023-12-12 09:58:20 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-01-24 16:14:34 +00:00
|
|
|
public * traverse(): IterableIterator<BoxModel> {
|
2023-12-12 09:58:20 +00:00
|
|
|
for (const child of this.kids()) {
|
|
|
|
yield child;
|
2024-01-24 16:14:34 +00:00
|
|
|
yield* child.traverse();
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-20 14:51:59 +00:00
|
|
|
public canRemove() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-12-12 09:58:20 +00:00
|
|
|
protected onCreate() {
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class LayoutModel extends BoxModel {
|
2024-03-20 14:51:59 +00:00
|
|
|
public disableDeleteSection: Computed<boolean>;
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
constructor(
|
2024-02-21 19:22:01 +00:00
|
|
|
box: FormLayoutNode,
|
2024-01-18 17:23:50 +00:00
|
|
|
public parent: BoxModel | null,
|
|
|
|
public _save: (clb?: Callback) => Promise<void>,
|
|
|
|
public view: FormView
|
|
|
|
) {
|
2023-12-12 09:58:20 +00:00
|
|
|
super(box, parent, view);
|
2024-03-20 14:51:59 +00:00
|
|
|
this.disableDeleteSection = Computed.create(this, use => {
|
|
|
|
return use(this.children).filter(c => c.type === 'Section').length === 1;
|
|
|
|
});
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
public async save(clb?: Callback) {
|
|
|
|
return await this._save(clb);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
public override render(): HTMLElement {
|
2023-12-12 09:58:20 +00:00
|
|
|
throw new Error('Method not implemented.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class DefaultBoxModel extends BoxModel {
|
|
|
|
public render(): HTMLElement {
|
2024-01-18 17:23:50 +00:00
|
|
|
return dom('div', `Unknown box type ${this.type}`);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const ignoreClick = dom.on('click', (ev) => {
|
|
|
|
ev.stopPropagation();
|
|
|
|
ev.preventDefault();
|
|
|
|
});
|
|
|
|
|
|
|
|
export function unwrap<T>(val: T | Computed<T>): T {
|
|
|
|
return val instanceof Computed ? val.get() : val;
|
|
|
|
}
|
|
|
|
|
2024-02-21 19:22:01 +00:00
|
|
|
export function parseBox(text: string): FormLayoutNode|null {
|
2023-12-12 09:58:20 +00:00
|
|
|
try {
|
|
|
|
const json = JSON.parse(text);
|
|
|
|
return json && typeof json === 'object' && json.type ? json : null;
|
|
|
|
} catch (e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|