gristlabs_grist-core/app/client/components/Forms/Columns.ts

177 lines
5.3 KiB
TypeScript
Raw Normal View History

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;
`);