2024-03-20 14:51:59 +00:00
|
|
|
import {allCommands} from 'app/client/components/commands';
|
2024-02-21 19:22:01 +00:00
|
|
|
import {FormLayoutNode} from 'app/client/components/FormRenderer';
|
2024-01-18 17:23:50 +00:00
|
|
|
import {buildEditor} from 'app/client/components/Forms/Editor';
|
2024-02-14 21:18:09 +00:00
|
|
|
import {FieldModel} from 'app/client/components/Forms/Field';
|
2024-03-20 14:51:59 +00:00
|
|
|
import {FormView} from 'app/client/components/Forms/FormView';
|
2024-01-18 17:23:50 +00:00
|
|
|
import {buildMenu} from 'app/client/components/Forms/Menu';
|
2024-03-20 14:51:59 +00:00
|
|
|
import {BoxModel, LayoutModel} from 'app/client/components/Forms/Model';
|
|
|
|
import {Paragraph} from 'app/client/components/Forms/Paragraph';
|
2024-02-21 19:22:01 +00:00
|
|
|
import * as style from 'app/client/components/Forms/styles';
|
2024-01-18 17:23:50 +00:00
|
|
|
import {makeTestId} from 'app/client/lib/domUtils';
|
2024-03-20 14:51:59 +00:00
|
|
|
import {makeT} from 'app/client/lib/localization';
|
|
|
|
import * as menus from 'app/client/ui2018/menus';
|
2024-01-23 20:52:57 +00:00
|
|
|
import {dom, styled} from 'grainjs';
|
2024-03-20 14:51:59 +00:00
|
|
|
import {v4 as uuidv4} from 'uuid';
|
|
|
|
|
|
|
|
const t = makeT('FormView');
|
2024-01-18 17:23:50 +00:00
|
|
|
|
|
|
|
const testId = makeTestId('test-forms-');
|
2023-12-12 09:58:20 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that renders a section of the form.
|
|
|
|
*/
|
|
|
|
export class SectionModel extends BoxModel {
|
2024-03-20 14:51:59 +00:00
|
|
|
constructor(box: FormLayoutNode, parent: BoxModel | null, view: FormView) {
|
|
|
|
super(box, parent, view);
|
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
public override render(): HTMLElement {
|
2023-12-12 09:58:20 +00:00
|
|
|
const children = this.children;
|
2024-01-18 17:23:50 +00:00
|
|
|
return buildEditor({
|
|
|
|
box: this,
|
|
|
|
// Custom drag element that is little bigger and at the top of the section.
|
|
|
|
drag: style.cssDragWrapper(style.cssDrag('DragDrop', style.cssDrag.cls('-top'))),
|
2024-03-20 14:51:59 +00:00
|
|
|
showRemoveButton: use => !use((this.root() as LayoutModel).disableDeleteSection),
|
2024-01-18 17:23:50 +00:00
|
|
|
// Content is just a list of children.
|
|
|
|
content: style.cssSection(
|
|
|
|
// Wrap them in a div that mutes hover events.
|
|
|
|
cssSectionItems(
|
|
|
|
dom.forEach(children, (child) => child.render()),
|
|
|
|
),
|
|
|
|
// Plus icon
|
|
|
|
style.cssPlusButton(
|
|
|
|
testId('plus'),
|
|
|
|
style.cssDrop(),
|
|
|
|
style.cssCircle(
|
|
|
|
style.cssPlusIcon('Plus'),
|
|
|
|
buildMenu({
|
|
|
|
box: this,
|
2024-03-20 14:51:59 +00:00
|
|
|
customItems: [
|
|
|
|
menus.menuItem(
|
|
|
|
() => allCommands.insertFieldBefore.run({structure: 'Section'}),
|
|
|
|
menus.menuIcon('Section'),
|
|
|
|
t('Insert section above'),
|
|
|
|
),
|
|
|
|
menus.menuItem(
|
|
|
|
() => allCommands.insertFieldAfter.run({structure: 'Section'}),
|
|
|
|
menus.menuIcon('Section'),
|
|
|
|
t('Insert section below'),
|
|
|
|
),
|
|
|
|
],
|
2024-01-18 17:23:50 +00:00
|
|
|
})
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)},
|
|
|
|
style.cssSectionEditor.cls(''),
|
2023-12-12 09:58:20 +00:00
|
|
|
);
|
2024-01-18 17:23:50 +00:00
|
|
|
}
|
|
|
|
|
2024-01-24 16:14:34 +00:00
|
|
|
public override willAccept(): 'sibling' | 'child' | null {
|
|
|
|
return 'child';
|
|
|
|
}
|
2023-12-12 09:58:20 +00:00
|
|
|
|
2024-01-18 17:23:50 +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 override accept(dropped: FormLayoutNode) {
|
2024-01-18 17:23:50 +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.
|
2024-01-24 16:14:34 +00:00
|
|
|
const droppedRef = dropped.id ? this.root().find(dropped.id) : null;
|
2024-01-18 17:23:50 +00:00
|
|
|
if (droppedRef) {
|
|
|
|
droppedRef.removeSelf();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Depending of the type of dropped box we need to insert it in different places.
|
|
|
|
// By default we insert it before this box.
|
|
|
|
let place = this.placeBeforeMe();
|
|
|
|
if (dropped.type === 'Field') {
|
|
|
|
// Fields are inserted after last child.
|
|
|
|
place = this.placeAfterListChild();
|
|
|
|
}
|
|
|
|
|
|
|
|
return place(dropped);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
2024-02-14 21:18:09 +00:00
|
|
|
|
|
|
|
public async deleteSelf(): Promise<void> {
|
|
|
|
// Prepare all the fields that are children of this section for removal.
|
|
|
|
const fieldsToRemove = Array.from(this.filter(b => b instanceof FieldModel)) as FieldModel[];
|
|
|
|
const fieldIdsToRemove = fieldsToRemove.map(f => f.leaf.get());
|
|
|
|
|
|
|
|
await this.parent?.save(async () => {
|
2024-03-20 14:51:59 +00:00
|
|
|
// Remove the fields.
|
2024-02-14 21:18:09 +00:00
|
|
|
if (fieldIdsToRemove.length > 0) {
|
|
|
|
await this.view.viewSection.removeField(fieldIdsToRemove);
|
|
|
|
}
|
2024-03-20 14:51:59 +00:00
|
|
|
|
|
|
|
// Remove each child of this section from the layout.
|
|
|
|
this.children.get().forEach(child => { child.removeSelf(); });
|
|
|
|
|
|
|
|
// Remove this section from the layout.
|
|
|
|
this.removeSelf();
|
2024-02-14 21:18:09 +00:00
|
|
|
});
|
|
|
|
}
|
2024-03-20 14:51:59 +00:00
|
|
|
|
|
|
|
public canRemove() {
|
|
|
|
return !((this.parent as LayoutModel).disableDeleteSection.get());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function Section(...children: FormLayoutNode[]): FormLayoutNode {
|
|
|
|
return {
|
|
|
|
id: uuidv4(),
|
|
|
|
type: 'Section',
|
|
|
|
children: [
|
|
|
|
Paragraph('### **Header**'),
|
|
|
|
Paragraph('Description'),
|
|
|
|
...children,
|
|
|
|
],
|
|
|
|
};
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
2024-01-18 17:23:50 +00:00
|
|
|
|
|
|
|
const cssSectionItems = styled('div.hover_border', `
|
|
|
|
`);
|