mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Forms improvements: mouse selection in firefox, focus, and styling
Summary: - Remove unused Form file (Label.ts) - Fix Firefox-specific bug in Forms, where mouse selection wasn't working in textarea. - Focus and set cursor in textarea on click. - Save on blur but only when focus stays within the Grist app, as for editing cells. - Make paragraph margins of rendered form match those in the form editor. Test Plan: Tested manually on Firefox and Chrome; relying on existing tests that nothing broke. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4281
This commit is contained in:
parent
2868ee2fa1
commit
063df75204
@ -22,10 +22,6 @@ interface Props {
|
||||
* Actual element to put into the editor. This is the main content of the editor.
|
||||
*/
|
||||
content: DomContents,
|
||||
/**
|
||||
* Click handler. If not provided, then clicking on the editor will select it.
|
||||
*/
|
||||
click?: (ev: MouseEvent, box: BoxModel) => void,
|
||||
/**
|
||||
* Whether to show the remove button. Defaults to true.
|
||||
*/
|
||||
@ -75,22 +71,6 @@ export function buildEditor(props: Props, ...args: IDomArgs<HTMLElement>) {
|
||||
style.cssRemoveButton.cls('-right', props.removePosition === 'right'),
|
||||
);
|
||||
|
||||
const onClick = (ev: MouseEvent) => {
|
||||
// Only if the click was in this element.
|
||||
const target = ev.target as HTMLElement;
|
||||
if (!target.closest) { return; }
|
||||
// Make sure that the closest editor is this one.
|
||||
const closest = target.closest(`.${style.cssFieldEditor.className}`);
|
||||
if (closest !== element) { return; }
|
||||
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
props.click?.(ev, props.box);
|
||||
|
||||
// Mark this box as selected.
|
||||
box.view.selectedBox.set(box);
|
||||
};
|
||||
|
||||
const dragAbove = Observable.create(owner, false);
|
||||
const dragBelow = Observable.create(owner, false);
|
||||
const dragging = Observable.create(owner, false);
|
||||
@ -111,7 +91,10 @@ export function buildEditor(props: Props, ...args: IDomArgs<HTMLElement>) {
|
||||
testId('field-editor-selected', box.selected),
|
||||
|
||||
// Select on click.
|
||||
dom.on('click', onClick),
|
||||
dom.on('click', (ev) => {
|
||||
stopEvent(ev);
|
||||
box.view.selectedBox.set(box);
|
||||
}),
|
||||
|
||||
// Attach context menu.
|
||||
buildMenu({
|
||||
@ -122,6 +105,15 @@ export function buildEditor(props: Props, ...args: IDomArgs<HTMLElement>) {
|
||||
// And now drag and drop support.
|
||||
{draggable: "true"},
|
||||
|
||||
// In Firefox, 'draggable' interferes with mouse selection in child input elements. Workaround
|
||||
// is to turn off 'draggable' temporarily (see https://stackoverflow.com/q/21680363/328565).
|
||||
dom.on('mousedown', (ev, elem) => {
|
||||
const isInput = ["INPUT", "TEXTAREA"].includes((ev.target as Element)?.tagName);
|
||||
// Turn off 'draggable' for inputs only, to support selection there; keep it on elsewhere.
|
||||
elem.draggable = !isInput;
|
||||
}),
|
||||
dom.on('mouseup', (ev, elem) => { elem.draggable = true; }),
|
||||
|
||||
// When started, we just put the box into the dataTransfer as a plain text.
|
||||
// TODO: this might be very sofisticated in the future.
|
||||
dom.on('dragstart', (ev) => {
|
||||
|
@ -1,85 +0,0 @@
|
||||
import * as css from './styles';
|
||||
import {buildEditor} from 'app/client/components/Forms/Editor';
|
||||
import {BoxModel} from 'app/client/components/Forms/Model';
|
||||
import {stopEvent} from 'app/client/lib/domUtils';
|
||||
import {not} from 'app/common/gutil';
|
||||
import {Computed, dom, Observable} from 'grainjs';
|
||||
|
||||
export class LabelModel extends BoxModel {
|
||||
public edit = Observable.create(this, false);
|
||||
|
||||
protected defaultValue = '';
|
||||
|
||||
public render(): HTMLElement {
|
||||
let element: HTMLTextAreaElement;
|
||||
const text = this.prop('text', this.defaultValue) as Observable<string|undefined>;
|
||||
const cssClass = this.prop('cssClass', '') as Observable<string>;
|
||||
const editableText = Observable.create(this, text.get() || '');
|
||||
const overlay = Computed.create(this, use => !use(this.edit));
|
||||
|
||||
this.autoDispose(text.addListener((v) => editableText.set(v || '')));
|
||||
|
||||
const save = (ok: boolean) => {
|
||||
if (ok) {
|
||||
text.set(editableText.get());
|
||||
void this.parent?.save().catch(reportError);
|
||||
} else {
|
||||
editableText.set(text.get() || '');
|
||||
}
|
||||
};
|
||||
|
||||
const mode = (edit: boolean) => {
|
||||
if (this.isDisposed() || this.edit.isDisposed()) { return; }
|
||||
if (this.edit.get() === edit) { return; }
|
||||
this.edit.set(edit);
|
||||
};
|
||||
|
||||
return buildEditor(
|
||||
{
|
||||
box: this,
|
||||
editMode: this.edit,
|
||||
overlay,
|
||||
click: (ev) => {
|
||||
stopEvent(ev);
|
||||
// If selected, then edit.
|
||||
if (!this.selected.get()) { return; }
|
||||
if (document.activeElement === element) { return; }
|
||||
editableText.set(text.get() || '');
|
||||
this.edit.set(true);
|
||||
setTimeout(() => {
|
||||
element.focus();
|
||||
element.select();
|
||||
}, 10);
|
||||
},
|
||||
content: element = css.cssEditableLabel(
|
||||
editableText,
|
||||
{onInput: true, autoGrow: true},
|
||||
{placeholder: `Empty label`},
|
||||
dom.on('click', ev => {
|
||||
stopEvent(ev);
|
||||
}),
|
||||
// Styles saved (for titles and such)
|
||||
css.cssEditableLabel.cls(use => `-${use(cssClass)}`),
|
||||
// Disable editing if not in edit mode.
|
||||
dom.boolAttr('readonly', not(this.edit)),
|
||||
// Pass edit to css.
|
||||
css.cssEditableLabel.cls('-edit', this.edit),
|
||||
// Attach default save controls (Enter, Esc) and so on.
|
||||
css.saveControls(this.edit, save),
|
||||
// Turn off resizable for textarea.
|
||||
dom.style('resize', 'none'),
|
||||
),
|
||||
},
|
||||
dom.onKeyDown({Enter$: (ev) => {
|
||||
// If no in edit mode, change it.
|
||||
if (!this.edit.get()) {
|
||||
mode(true);
|
||||
ev.stopPropagation();
|
||||
ev.stopImmediatePropagation();
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
}})
|
||||
);
|
||||
}
|
||||
}
|
@ -19,7 +19,6 @@ export class ParagraphModel extends BoxModel {
|
||||
public override render(): HTMLElement {
|
||||
const box = this;
|
||||
const editMode = box.edit;
|
||||
let element: HTMLElement;
|
||||
const text = this.prop('text', this.defaultValue) as Observable<string|undefined>;
|
||||
|
||||
// There is a spacial hack here. We might be created as a separator component, but the rendering
|
||||
@ -44,18 +43,21 @@ export class ParagraphModel extends BoxModel {
|
||||
this.cssClass ? dom.cls(this.cssClass, not(editMode)) : null,
|
||||
dom.maybe(editMode, () => {
|
||||
const draft = Observable.create(null, text.get() || '');
|
||||
setTimeout(() => element?.focus(), 10);
|
||||
return [
|
||||
element = cssTextArea(draft, {autoGrow: true, onInput: true},
|
||||
cssTextArea.cls('-edit', editMode),
|
||||
css.saveControls(editMode, (ok) => {
|
||||
if (ok && editMode.get()) {
|
||||
text.set(draft.get());
|
||||
this.save().catch(reportError);
|
||||
}
|
||||
})
|
||||
),
|
||||
];
|
||||
return cssTextArea(draft, {autoGrow: true, onInput: true},
|
||||
cssTextArea.cls('-edit', editMode),
|
||||
(elem) => {
|
||||
setTimeout(() => {
|
||||
elem.focus();
|
||||
elem.setSelectionRange(elem.value.length, elem.value.length);
|
||||
}, 10);
|
||||
},
|
||||
css.saveControls(editMode, (ok) => {
|
||||
if (ok && editMode.get()) {
|
||||
text.set(draft.get());
|
||||
this.save().catch(reportError);
|
||||
}
|
||||
})
|
||||
);
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
@ -13,7 +13,6 @@ export * from "./Section";
|
||||
export * from './Field';
|
||||
export * from './Columns';
|
||||
export * from './Submit';
|
||||
export * from './Label';
|
||||
|
||||
export function defaultElement(type: FormLayoutNodeType): FormLayoutNode {
|
||||
switch(type) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type {App} from 'app/client/ui/App';
|
||||
import {textarea} from 'app/client/ui/inputs';
|
||||
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
|
||||
import {basicButton, basicButtonLink, primaryButtonLink, textButton} from 'app/client/ui2018/buttons';
|
||||
@ -759,11 +760,17 @@ export function saveControls(editMode: Observable<boolean>, save: (ok: boolean)
|
||||
}
|
||||
}
|
||||
}),
|
||||
dom.on('blur', (ev) => {
|
||||
if (!editMode.isDisposed() && editMode.get()) {
|
||||
save(true);
|
||||
editMode.set(false);
|
||||
dom.create((owner) => {
|
||||
// Whenever focus returns to the Clipboard component, close the editor by saving the value.
|
||||
function saveEdit() {
|
||||
if (!editMode.isDisposed() && editMode.get()) {
|
||||
save(true);
|
||||
editMode.set(false);
|
||||
}
|
||||
}
|
||||
const app = (window as any).gristApp as App;
|
||||
app.on('clipboard_focus', saveEdit);
|
||||
owner.onDispose(() => app.off('clipboard_focus', saveEdit));
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
@ -165,7 +165,7 @@ const cssFormContent = styled('form', `
|
||||
font-size: 10px;
|
||||
}
|
||||
& p {
|
||||
margin: 0px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
& strong {
|
||||
font-weight: 600;
|
||||
|
Loading…
Reference in New Issue
Block a user