2024-02-21 19:22:01 +00:00
|
|
|
import {CHOOSE_TEXT, FormLayoutNode} from 'app/client/components/FormRenderer';
|
2024-01-18 17:23:50 +00:00
|
|
|
import {buildEditor} from 'app/client/components/Forms/Editor';
|
2023-12-12 09:58:20 +00:00
|
|
|
import {FormView} from 'app/client/components/Forms/FormView';
|
2024-01-23 20:52:57 +00:00
|
|
|
import {BoxModel, ignoreClick} from 'app/client/components/Forms/Model';
|
2024-01-18 17:23:50 +00:00
|
|
|
import * as css from 'app/client/components/Forms/styles';
|
|
|
|
import {stopEvent} from 'app/client/lib/domUtils';
|
|
|
|
import {refRecord} from 'app/client/models/DocModel';
|
|
|
|
import {autoGrow} from 'app/client/ui/forms';
|
|
|
|
import {squareCheckbox} from 'app/client/ui2018/checkbox';
|
|
|
|
import {colors} from 'app/client/ui2018/cssVars';
|
2024-01-24 16:14:34 +00:00
|
|
|
import {Constructor, not} from 'app/common/gutil';
|
2024-01-18 17:23:50 +00:00
|
|
|
import {
|
|
|
|
BindableValue,
|
|
|
|
Computed,
|
|
|
|
Disposable,
|
|
|
|
dom,
|
|
|
|
DomContents,
|
|
|
|
DomElementArg,
|
|
|
|
IDomArgs,
|
|
|
|
makeTestId,
|
|
|
|
MultiHolder,
|
|
|
|
observable,
|
|
|
|
Observable,
|
|
|
|
styled,
|
|
|
|
toKo
|
|
|
|
} from 'grainjs';
|
2023-12-12 09:58:20 +00:00
|
|
|
import * as ko from 'knockout';
|
|
|
|
|
|
|
|
const testId = makeTestId('test-forms-');
|
|
|
|
|
|
|
|
/**
|
2024-01-18 17:23:50 +00:00
|
|
|
* Container class for all fields.
|
2023-12-12 09:58:20 +00:00
|
|
|
*/
|
|
|
|
export class FieldModel extends BoxModel {
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
/**
|
|
|
|
* Edit mode, (only one element can be in edit mode in the form editor).
|
|
|
|
*/
|
|
|
|
public edit = Observable.create(this, false);
|
2023-12-12 09:58:20 +00:00
|
|
|
public fieldRef = this.autoDispose(ko.pureComputed(() => toKo(ko, this.leaf)()));
|
2024-01-18 17:23:50 +00:00
|
|
|
public field = refRecord(this.view.gristDoc.docModel.viewFields, this.fieldRef);
|
|
|
|
public colId = Computed.create(this, (use) => use(use(this.field).colId));
|
|
|
|
public column = Computed.create(this, (use) => use(use(this.field).column));
|
2024-01-24 16:14:34 +00:00
|
|
|
public required: Computed<boolean>;
|
2023-12-12 09:58:20 +00:00
|
|
|
public question = Computed.create(this, (use) => {
|
2024-01-18 17:23:50 +00:00
|
|
|
const field = use(this.field);
|
|
|
|
if (field.isDisposed() || use(field.id) === 0) { return ''; }
|
|
|
|
return use(field.question) || use(field.origLabel);
|
2023-12-12 09:58:20 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
public description = Computed.create(this, (use) => {
|
2024-01-18 17:23:50 +00:00
|
|
|
const field = use(this.field);
|
|
|
|
return use(field.description);
|
2023-12-12 09:58:20 +00:00
|
|
|
});
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
/**
|
|
|
|
* Column type of the field.
|
|
|
|
*/
|
2023-12-12 09:58:20 +00:00
|
|
|
public colType = Computed.create(this, (use) => {
|
2024-01-18 17:23:50 +00:00
|
|
|
const field = use(this.field);
|
|
|
|
return use(use(field.column).pureType);
|
2023-12-12 09:58:20 +00:00
|
|
|
});
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
/**
|
|
|
|
* Field row id.
|
|
|
|
*/
|
2023-12-12 09:58:20 +00:00
|
|
|
public get leaf() {
|
2024-01-23 20:52:57 +00:00
|
|
|
return this.prop('leaf') as Observable<number>;
|
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
/**
|
|
|
|
* A renderer of question instance.
|
|
|
|
*/
|
2023-12-12 09:58:20 +00:00
|
|
|
public renderer = Computed.create(this, (use) => {
|
|
|
|
const ctor = fieldConstructor(use(this.colType));
|
2024-01-18 17:23:50 +00:00
|
|
|
const instance = new ctor(this);
|
2023-12-12 09:58:20 +00:00
|
|
|
use.owner.autoDispose(instance);
|
|
|
|
return instance;
|
|
|
|
});
|
|
|
|
|
2024-02-21 19:22:01 +00:00
|
|
|
constructor(box: FormLayoutNode, parent: BoxModel | null, view: FormView) {
|
2023-12-12 09:58:20 +00:00
|
|
|
super(box, parent, view);
|
2024-01-18 17:23:50 +00:00
|
|
|
|
2024-01-24 16:14:34 +00:00
|
|
|
this.required = Computed.create(this, (use) => {
|
|
|
|
const field = use(this.field);
|
|
|
|
return Boolean(use(field.widgetOptionsJson.prop('formRequired')));
|
|
|
|
});
|
|
|
|
this.required.onWrite(value => {
|
|
|
|
this.field.peek().widgetOptionsJson.prop('formRequired').setAndSave(value).catch(reportError);
|
|
|
|
});
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
this.question.onWrite(value => {
|
|
|
|
this.field.peek().question.setAndSave(value).catch(reportError);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.autoDispose(
|
|
|
|
this.selected.addListener((now, then) => {
|
|
|
|
if (!now && then) {
|
|
|
|
setImmediate(() => !this.edit.isDisposed() && this.edit.set(false));
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
public async afterDrop() {
|
|
|
|
// Base class does good job of handling drop.
|
|
|
|
await super.afterDrop();
|
|
|
|
if (this.isDisposed()) { return; }
|
|
|
|
|
|
|
|
// Except when a field is dragged from the creator panel, which stores colId instead of fieldRef (as there is no
|
|
|
|
// field yet). In this case, we need to create a field.
|
2023-12-12 09:58:20 +00:00
|
|
|
if (typeof this.leaf.get() === 'string') {
|
|
|
|
this.leaf.set(await this.view.showColumn(this.leaf.get()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
public override render(...args: IDomArgs<HTMLElement>): HTMLElement {
|
|
|
|
// Updated question is used for editing, we don't save on every key press, but only on blur (or enter, etc).
|
|
|
|
const save = (value: string) => {
|
|
|
|
value = value?.trim();
|
|
|
|
// If question is empty or same as original, don't save.
|
|
|
|
if (!value || value === this.field.peek().question()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.field.peek().question.setAndSave(value).catch(reportError);
|
|
|
|
};
|
|
|
|
const overlay = Observable.create(null, true);
|
|
|
|
|
|
|
|
const content = dom.domComputed(this.renderer, (r) => r.buildDom({
|
|
|
|
edit: this.edit,
|
|
|
|
overlay,
|
|
|
|
onSave: save,
|
2024-01-24 16:14:34 +00:00
|
|
|
}));
|
2024-01-18 17:23:50 +00:00
|
|
|
|
|
|
|
return buildEditor({
|
|
|
|
box: this,
|
|
|
|
overlay,
|
|
|
|
removeIcon: 'CrossBig',
|
|
|
|
removeTooltip: 'Hide',
|
|
|
|
editMode: this.edit,
|
|
|
|
content,
|
|
|
|
},
|
|
|
|
dom.on('dblclick', () => this.selected.get() && this.edit.set(true)),
|
2024-01-24 16:14:34 +00:00
|
|
|
...args
|
2023-12-12 09:58:20 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async deleteSelf() {
|
2024-01-18 17:23:50 +00:00
|
|
|
const rowId = this.field.peek().id.peek();
|
2023-12-12 09:58:20 +00:00
|
|
|
const view = this.view;
|
2024-01-18 17:23:50 +00:00
|
|
|
const root = this.root();
|
2023-12-12 09:58:20 +00:00
|
|
|
this.removeSelf();
|
|
|
|
// The order here matters for undo.
|
2024-01-18 17:23:50 +00:00
|
|
|
await root.save(async () => {
|
|
|
|
// Make sure to save first layout without this field, otherwise the undo won't work properly.
|
|
|
|
await root.save();
|
|
|
|
// We are disposed at this point, be still can access the view.
|
|
|
|
if (rowId) {
|
|
|
|
await view.viewSection.removeField(rowId);
|
|
|
|
}
|
|
|
|
});
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
export abstract class Question extends Disposable {
|
|
|
|
constructor(public model: FieldModel) {
|
2023-12-12 09:58:20 +00:00
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
public buildDom(props: {
|
|
|
|
edit: Observable<boolean>,
|
|
|
|
overlay: Observable<boolean>,
|
|
|
|
onSave: (value: string) => void,
|
|
|
|
}, ...args: IDomArgs<HTMLElement>) {
|
2024-01-24 16:14:34 +00:00
|
|
|
return css.cssQuestion(
|
2024-01-18 17:23:50 +00:00
|
|
|
testId('question'),
|
|
|
|
testType(this.model.colType),
|
|
|
|
this.renderLabel(props, dom.style('margin-bottom', '5px')),
|
|
|
|
this.renderInput(),
|
2024-01-24 16:14:34 +00:00
|
|
|
css.cssQuestion.cls('-required', this.model.required),
|
2024-01-18 17:23:50 +00:00
|
|
|
...args
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public abstract renderInput(): DomContents;
|
|
|
|
|
|
|
|
protected renderLabel(props: {
|
|
|
|
edit: Observable<boolean>,
|
|
|
|
onSave: (value: string) => void,
|
|
|
|
}, ...args: DomElementArg[]) {
|
|
|
|
const {edit, onSave} = props;
|
|
|
|
|
|
|
|
const scope = new MultiHolder();
|
|
|
|
|
|
|
|
// When in edit, we will update a copy of the question.
|
|
|
|
const draft = Observable.create(scope, this.model.question.get());
|
|
|
|
scope.autoDispose(
|
|
|
|
this.model.question.addListener(q => draft.set(q)),
|
|
|
|
);
|
|
|
|
const controller = Computed.create(scope, (use) => use(draft));
|
|
|
|
controller.onWrite(value => {
|
|
|
|
if (this.isDisposed() || draft.isDisposed()) { return; }
|
|
|
|
if (!edit.get()) { return; }
|
|
|
|
draft.set(value);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Wire up save method.
|
|
|
|
const saveDraft = (ok: boolean) => {
|
|
|
|
if (this.isDisposed() || draft.isDisposed()) { return; }
|
|
|
|
if (!ok || !edit.get() || !controller.get()) {
|
|
|
|
controller.set(this.model.question.get());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
onSave(controller.get());
|
|
|
|
};
|
|
|
|
let element: HTMLTextAreaElement;
|
|
|
|
|
|
|
|
scope.autoDispose(
|
|
|
|
props.edit.addListener((now, then) => {
|
|
|
|
if (now && !then) {
|
|
|
|
// When we go into edit mode, we copy the question into draft.
|
|
|
|
draft.set(this.model.question.get());
|
|
|
|
// And focus on the element.
|
|
|
|
setTimeout(() => {
|
|
|
|
element?.focus();
|
|
|
|
element?.select();
|
|
|
|
}, 10);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
return [
|
|
|
|
dom.autoDispose(scope),
|
2024-01-24 16:14:34 +00:00
|
|
|
css.cssRequiredWrapper(
|
2024-01-18 17:23:50 +00:00
|
|
|
testId('label'),
|
2024-01-24 16:14:34 +00:00
|
|
|
// When in edit - hide * and change display from grid to display
|
|
|
|
css.cssRequiredWrapper.cls('-required', use => Boolean(use(this.model.required) && !use(this.model.edit))),
|
|
|
|
dom.maybe(props.edit, () => [
|
|
|
|
element = css.cssEditableLabel(
|
|
|
|
controller,
|
|
|
|
{onInput: true},
|
|
|
|
// Attach common Enter,Escape, blur handlers.
|
|
|
|
css.saveControls(edit, saveDraft),
|
|
|
|
// Autoselect whole text when mounted.
|
|
|
|
// Auto grow for textarea.
|
|
|
|
autoGrow(controller),
|
|
|
|
// Enable normal menu.
|
|
|
|
dom.on('contextmenu', stopEvent),
|
|
|
|
dom.style('resize', 'none'),
|
|
|
|
css.cssEditableLabel.cls('-edit'),
|
|
|
|
testId('label-editor'),
|
|
|
|
),
|
|
|
|
]),
|
|
|
|
dom.maybe(not(props.edit), () => [
|
|
|
|
css.cssRenderedLabel(
|
|
|
|
dom.text(controller),
|
|
|
|
testId('label-rendered'),
|
|
|
|
),
|
|
|
|
]),
|
2024-01-18 17:23:50 +00:00
|
|
|
// When selected, we want to be able to edit the label by clicking it
|
|
|
|
// so we need to make it relative and z-indexed.
|
|
|
|
dom.style('position', u => u(this.model.selected) ? 'relative' : 'static'),
|
|
|
|
dom.style('z-index', '2'),
|
|
|
|
dom.on('click', (ev) => {
|
|
|
|
if (this.model.selected.get() && !props.edit.get()) {
|
|
|
|
props.edit.set(true);
|
|
|
|
ev.stopPropagation();
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
...args,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
}
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class TextModel extends Question {
|
2024-01-18 17:23:50 +00:00
|
|
|
public renderInput() {
|
|
|
|
return css.cssInput(
|
|
|
|
dom.prop('name', u => u(u(this.model.field).colId)),
|
|
|
|
{disabled: true},
|
2023-12-12 09:58:20 +00:00
|
|
|
{type: 'text', tabIndex: "-1"},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ChoiceModel extends Question {
|
2024-01-23 20:52:57 +00:00
|
|
|
protected choices: Computed<string[]> = Computed.create(this, use => {
|
|
|
|
// Read choices from field.
|
|
|
|
const list = use(use(use(this.model.field).origCol).widgetOptionsJson.prop('choices')) || [];
|
|
|
|
|
|
|
|
// Make sure it is array of strings.
|
|
|
|
if (!Array.isArray(list) || list.some((v) => typeof v !== 'string')) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
return list;
|
|
|
|
});
|
|
|
|
|
|
|
|
protected choicesWithEmpty = Computed.create(this, use => {
|
2024-01-24 16:14:34 +00:00
|
|
|
const list: Array<string|null> = Array.from(use(this.choices));
|
2024-01-23 20:52:57 +00:00
|
|
|
// Add empty choice if not present.
|
2024-01-24 16:14:34 +00:00
|
|
|
list.unshift(null);
|
2024-01-23 20:52:57 +00:00
|
|
|
return list;
|
|
|
|
});
|
|
|
|
|
|
|
|
public renderInput(): HTMLElement {
|
2024-01-18 17:23:50 +00:00
|
|
|
const field = this.model.field;
|
|
|
|
return css.cssSelect(
|
2023-12-12 09:58:20 +00:00
|
|
|
{tabIndex: "-1"},
|
|
|
|
ignoreClick,
|
2024-01-18 17:23:50 +00:00
|
|
|
dom.prop('name', use => use(use(field).colId)),
|
2024-01-24 16:14:34 +00:00
|
|
|
dom.forEach(this.choicesWithEmpty, (choice) => dom('option', choice ?? CHOOSE_TEXT, {value: choice ?? ''})),
|
2023-12-12 09:58:20 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-23 20:52:57 +00:00
|
|
|
class ChoiceListModel extends ChoiceModel {
|
2024-01-18 17:23:50 +00:00
|
|
|
public renderInput() {
|
|
|
|
const field = this.model.field;
|
2023-12-12 09:58:20 +00:00
|
|
|
return dom('div',
|
2024-01-18 17:23:50 +00:00
|
|
|
dom.prop('name', use => use(use(field).colId)),
|
2024-01-23 20:52:57 +00:00
|
|
|
dom.forEach(this.choices, (choice) => css.cssCheckboxLabel(
|
2024-01-18 17:23:50 +00:00
|
|
|
squareCheckbox(observable(false)),
|
2023-12-12 09:58:20 +00:00
|
|
|
choice
|
|
|
|
)),
|
2024-01-23 20:52:57 +00:00
|
|
|
dom.maybe(use => use(this.choices).length === 0, () => [
|
2023-12-12 09:58:20 +00:00
|
|
|
dom('div', 'No choices defined'),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class BoolModel extends Question {
|
2024-01-18 17:23:50 +00:00
|
|
|
public override buildDom(props: {
|
|
|
|
edit: Observable<boolean>,
|
|
|
|
overlay: Observable<boolean>,
|
|
|
|
question: Observable<string>,
|
|
|
|
onSave: () => void,
|
|
|
|
}) {
|
2024-01-24 16:14:34 +00:00
|
|
|
return css.cssQuestion(
|
2024-01-18 17:23:50 +00:00
|
|
|
testId('question'),
|
|
|
|
testType(this.model.colType),
|
|
|
|
cssToggle(
|
|
|
|
this.renderInput(),
|
2024-01-24 16:14:34 +00:00
|
|
|
this.renderLabel(props, css.cssLabelInline.cls('')),
|
2023-12-12 09:58:20 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2024-01-18 17:23:50 +00:00
|
|
|
public override renderInput() {
|
|
|
|
const value = Observable.create(this, true);
|
|
|
|
return dom('div.widget_switch',
|
|
|
|
dom.style('--grist-actual-cell-color', colors.lightGreen.toString()),
|
|
|
|
dom.cls('switch_on', value),
|
|
|
|
dom.cls('switch_transition', true),
|
|
|
|
dom('div.switch_slider'),
|
|
|
|
dom('div.switch_circle'),
|
|
|
|
);
|
|
|
|
}
|
2023-12-12 09:58:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class DateModel extends Question {
|
2024-01-18 17:23:50 +00:00
|
|
|
public renderInput() {
|
2023-12-12 09:58:20 +00:00
|
|
|
return dom('div',
|
2024-01-18 17:23:50 +00:00
|
|
|
css.cssInput(
|
|
|
|
dom.prop('name', this.model.colId),
|
2023-12-12 09:58:20 +00:00
|
|
|
{type: 'date', style: 'margin-right: 5px; width: 100%;'
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class DateTimeModel extends Question {
|
2024-01-18 17:23:50 +00:00
|
|
|
public renderInput() {
|
2023-12-12 09:58:20 +00:00
|
|
|
return dom('div',
|
2024-01-18 17:23:50 +00:00
|
|
|
css.cssInput(
|
|
|
|
dom.prop('name', this.model.colId),
|
2023-12-12 09:58:20 +00:00
|
|
|
{type: 'datetime-local', style: 'margin-right: 5px; width: 100%;'}
|
|
|
|
),
|
|
|
|
dom.style('width', '100%'),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
class RefListModel extends Question {
|
|
|
|
protected choices = this._subscribeForChoices();
|
|
|
|
|
|
|
|
public renderInput() {
|
|
|
|
return dom('div',
|
|
|
|
dom.prop('name', this.model.colId),
|
2024-01-24 09:58:19 +00:00
|
|
|
dom.forEach(this.choices, (choice) => css.cssCheckboxLabel(
|
|
|
|
squareCheckbox(observable(false)),
|
2024-01-18 17:23:50 +00:00
|
|
|
String(choice[1] ?? '')
|
|
|
|
)),
|
|
|
|
dom.maybe(use => use(this.choices).length === 0, () => [
|
|
|
|
dom('div', 'No choices defined'),
|
|
|
|
]),
|
|
|
|
) as HTMLElement;
|
|
|
|
}
|
|
|
|
|
|
|
|
private _subscribeForChoices() {
|
|
|
|
const tableId = Computed.create(this, use => {
|
|
|
|
const refTable = use(use(this.model.column).refTable);
|
|
|
|
return refTable ? use(refTable.tableId) : '';
|
|
|
|
});
|
|
|
|
|
|
|
|
const colId = Computed.create(this, use => {
|
|
|
|
const dispColumnIdObs = use(use(this.model.column).visibleColModel);
|
|
|
|
return use(dispColumnIdObs.colId);
|
|
|
|
});
|
|
|
|
|
|
|
|
const observer = this.model.view.gristDoc.columnObserver(this, tableId, colId);
|
|
|
|
|
|
|
|
return Computed.create(this, use => {
|
|
|
|
const unsorted = use(observer);
|
|
|
|
unsorted.sort((a, b) => String(a[1]).localeCompare(String(b[1])));
|
|
|
|
return unsorted.slice(0, 50); // TODO: pagination or a waning
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class RefModel extends RefListModel {
|
2024-01-23 20:52:57 +00:00
|
|
|
protected withEmpty = Computed.create(this, use => {
|
|
|
|
const list = Array.from(use(this.choices));
|
|
|
|
// Add empty choice if not present.
|
2024-01-24 16:14:34 +00:00
|
|
|
list.unshift(['', CHOOSE_TEXT]);
|
2024-01-23 20:52:57 +00:00
|
|
|
return list;
|
|
|
|
});
|
|
|
|
|
2024-01-18 17:23:50 +00:00
|
|
|
public renderInput() {
|
|
|
|
return css.cssSelect(
|
|
|
|
{tabIndex: "-1"},
|
|
|
|
ignoreClick,
|
|
|
|
dom.prop('name', this.model.colId),
|
2024-01-23 20:52:57 +00:00
|
|
|
dom.forEach(this.withEmpty, (choice) => dom('option', String(choice[1] ?? ''), {value: String(choice[0])})),
|
2024-01-18 17:23:50 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2023-12-12 09:58:20 +00:00
|
|
|
|
|
|
|
// TODO: decide which one we need and implement rest.
|
|
|
|
const AnyModel = TextModel;
|
|
|
|
const NumericModel = TextModel;
|
|
|
|
const IntModel = TextModel;
|
|
|
|
const AttachmentsModel = TextModel;
|
|
|
|
|
|
|
|
|
|
|
|
function fieldConstructor(type: string): Constructor<Question> {
|
|
|
|
switch (type) {
|
|
|
|
case 'Any': return AnyModel;
|
|
|
|
case 'Bool': return BoolModel;
|
|
|
|
case 'Choice': return ChoiceModel;
|
|
|
|
case 'ChoiceList': return ChoiceListModel;
|
|
|
|
case 'Date': return DateModel;
|
|
|
|
case 'DateTime': return DateTimeModel;
|
|
|
|
case 'Int': return IntModel;
|
|
|
|
case 'Numeric': return NumericModel;
|
|
|
|
case 'Ref': return RefModel;
|
|
|
|
case 'RefList': return RefListModel;
|
|
|
|
case 'Attachments': return AttachmentsModel;
|
|
|
|
default: return TextModel;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a hidden input element with element type. Used in tests.
|
|
|
|
*/
|
|
|
|
function testType(value: BindableValue<string>) {
|
|
|
|
return dom('input', {type: 'hidden'}, dom.prop('value', value), testId('type'));
|
|
|
|
}
|
2024-01-18 17:23:50 +00:00
|
|
|
|
|
|
|
const cssToggle = styled('div', `
|
2024-01-24 16:14:34 +00:00
|
|
|
display: grid;
|
2024-01-18 17:23:50 +00:00
|
|
|
align-items: center;
|
2024-01-24 16:14:34 +00:00
|
|
|
grid-template-columns: auto 1fr;
|
2024-01-18 17:23:50 +00:00
|
|
|
gap: 8px;
|
2024-01-24 16:14:34 +00:00
|
|
|
padding: 4px 0px;
|
2024-01-18 17:23:50 +00:00
|
|
|
--grist-actual-cell-color: ${colors.lightGreen};
|
|
|
|
`);
|