mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-20 01:02:22 +00:00
a424450cbe
Summary: A new widget type Forms. For now hidden behind GRIST_EXPERIMENTAL_PLUGINS(). This diff contains all the core moving parts as a serves as a base to extend this functionality further. Test Plan: New test added Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4130
177 lines
5.3 KiB
TypeScript
177 lines
5.3 KiB
TypeScript
import * as style from 'app/client/components/Forms/styles';
|
|
import {Box, BoxModel, RenderContext} from 'app/client/components/Forms/Model';
|
|
import {makeTestId} from 'app/client/lib/domUtils';
|
|
import {icon} from 'app/client/ui2018/icons';
|
|
import * as menus from 'app/client/ui2018/menus';
|
|
import {inlineStyle, not} from 'app/common/gutil';
|
|
import {bundleChanges, Computed, dom, MultiHolder, Observable, styled} from 'grainjs';
|
|
|
|
const testId = makeTestId('test-forms-');
|
|
|
|
export class ColumnsModel extends BoxModel {
|
|
private _columnCount = Computed.create(this, use => use(this.children).length);
|
|
|
|
public removeChild(box: BoxModel) {
|
|
if (box.type.get() === 'Placeholder') {
|
|
// Make sure we have at least one rendered.
|
|
if (this.children.get().length <= 1) {
|
|
return;
|
|
}
|
|
return super.removeChild(box);
|
|
}
|
|
// We will replace this box with a placeholder.
|
|
this.replace(box, Placeholder());
|
|
}
|
|
|
|
// Dropping a box on a column will replace it.
|
|
public drop(dropped: Box): BoxModel {
|
|
if (!this.parent) { throw new Error('No parent'); }
|
|
|
|
// We need to remove it from the parent, so find it first.
|
|
const droppedId = dropped.id;
|
|
const droppedRef = this.root().find(droppedId);
|
|
|
|
// Now we simply insert it after this box.
|
|
droppedRef?.removeSelf();
|
|
|
|
return this.parent.replace(this, dropped);
|
|
}
|
|
public render(context: RenderContext): HTMLElement {
|
|
context.overlay.set(false);
|
|
|
|
// Now render the dom.
|
|
const renderedDom = style.cssColumns(
|
|
// Pass column count as a css variable (to style the grid).
|
|
inlineStyle(`--css-columns-count`, this._columnCount),
|
|
|
|
// Render placeholders as children.
|
|
dom.forEach(this.children, (child) => {
|
|
return this.view.renderBox(
|
|
this.children,
|
|
child || BoxModel.new(Placeholder(), this),
|
|
testId('column')
|
|
);
|
|
}),
|
|
|
|
// Append + button at the end.
|
|
dom('div',
|
|
testId('add'),
|
|
icon('Plus'),
|
|
dom.on('click', () => this.placeAfterListChild()(Placeholder())),
|
|
style.cssColumn.cls('-add-button')
|
|
),
|
|
);
|
|
return renderedDom;
|
|
}
|
|
}
|
|
|
|
export class PlaceholderModel extends BoxModel {
|
|
|
|
public render(context: RenderContext): HTMLElement {
|
|
const [box, view, overlay] = [this, this.view, context.overlay];
|
|
const scope = new MultiHolder();
|
|
overlay.set(false);
|
|
|
|
const liveIndex = Computed.create(scope, (use) => {
|
|
if (!box.parent) { return -1; }
|
|
const parentChildren = use(box.parent.children);
|
|
return parentChildren.indexOf(box);
|
|
});
|
|
|
|
const boxModelAt = Computed.create(scope, (use) => {
|
|
const index = use(liveIndex);
|
|
if (index === null) { return null; }
|
|
const childBox = use(box.children)[index];
|
|
if (!childBox) {
|
|
return null;
|
|
}
|
|
return childBox;
|
|
});
|
|
|
|
const dragHover = Observable.create(scope, false);
|
|
|
|
return cssPlaceholder(
|
|
style.cssDrag(),
|
|
testId('placeholder'),
|
|
dom.autoDispose(scope),
|
|
|
|
style.cssColumn.cls('-drag-over', dragHover),
|
|
style.cssColumn.cls('-empty', not(boxModelAt)),
|
|
style.cssColumn.cls('-selected', use => use(view.selectedBox) === box),
|
|
|
|
view.buildAddMenu(insertBox, {
|
|
customItems: [menus.menuItem(removeColumn, menus.menuIcon('Remove'), 'Remove Column')],
|
|
}),
|
|
|
|
dom.on('contextmenu', (ev) => {
|
|
ev.stopPropagation();
|
|
}),
|
|
|
|
dom.on('dragleave', (ev) => {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
// Just remove the style and stop propagation.
|
|
dragHover.set(false);
|
|
}),
|
|
|
|
dom.on('dragover', (ev) => {
|
|
// As usual, prevent propagation.
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
// Here we just change the style of the element.
|
|
ev.dataTransfer!.dropEffect = "move";
|
|
dragHover.set(true);
|
|
}),
|
|
|
|
dom.on('drop', (ev) => {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
dragHover.set(false);
|
|
|
|
// Get the box that was dropped.
|
|
const dropped = JSON.parse(ev.dataTransfer!.getData('text/plain'));
|
|
|
|
// We need to remove it from the parent, so find it first.
|
|
const droppedId = dropped.id;
|
|
const droppedRef = box.root().find(droppedId);
|
|
if (!droppedRef) { return; }
|
|
|
|
// Now we simply insert it after this box.
|
|
bundleChanges(() => {
|
|
droppedRef.removeSelf();
|
|
const parent = box.parent!;
|
|
parent.replace(box, dropped);
|
|
parent.save().catch(reportError);
|
|
});
|
|
}),
|
|
|
|
dom.maybeOwned(boxModelAt, (mscope, child) => view.renderBox(mscope, child)),
|
|
dom.maybe(use => !use(boxModelAt) && use(view.isEdit), () => {
|
|
return dom('span', `Column `, dom.text(use => String(use(liveIndex) + 1)));
|
|
}),
|
|
);
|
|
|
|
function insertBox(childBox: Box) {
|
|
// Make sure we have at least as many columns as the index we are inserting at.
|
|
if (!box.parent) { throw new Error('No parent'); }
|
|
return box.parent.replace(box, childBox);
|
|
}
|
|
|
|
function removeColumn() {
|
|
box.removeSelf();
|
|
}
|
|
}
|
|
}
|
|
|
|
export function Placeholder(): Box {
|
|
return {type: 'Placeholder'};
|
|
}
|
|
|
|
export function Columns(): Box {
|
|
return {type: 'Columns', children: [Placeholder(), Placeholder()]};
|
|
}
|
|
|
|
const cssPlaceholder = styled('div', `
|
|
position: relative;
|
|
`);
|