mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
3e49fe9a50
Summary: A green line indicating the insertion point is now shown in the ChoiceListEntry component when dragging and dropping choices, similar to the one shown in the choice list cell editor. Test Plan: Tested manually. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3529
159 lines
4.8 KiB
TypeScript
159 lines
4.8 KiB
TypeScript
/**
|
|
* editableLabel uses grainjs's input widget and adds UI and behavioral extensions:
|
|
* - Label width grows/shrinks with content (using a hidden sizer element)
|
|
* - On Escape, cancel editing and revert to original value
|
|
* - Clicking away or hitting Enter on empty value cancels editing too
|
|
*
|
|
* The structure is a wrapper diver with an input child: div > input. Supports passing in
|
|
* DomElementArgs, which get passed to the underlying <input> element.
|
|
*
|
|
* TODO: Consider merging this into grainjs's input widget.
|
|
*/
|
|
import { colors } from 'app/client/ui2018/cssVars';
|
|
import { dom, DomArg, styled } from 'grainjs';
|
|
import { Observable } from 'grainjs';
|
|
import noop = require('lodash/noop');
|
|
|
|
const cssWrapper = styled('div', `
|
|
position: relative;
|
|
display: inline-block;
|
|
`);
|
|
|
|
export const cssLabelText = styled(rawTextInput, `
|
|
/* Reset appearance */
|
|
-webkit-appearance: none;
|
|
-moz-appearance: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
border: none;
|
|
outline: none;
|
|
|
|
/* Size is determined by the hidden sizer, so take up 100% of width */
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
|
|
line-height: inherit;
|
|
font-size: inherit;
|
|
font-family: inherit;
|
|
font-weight: inherit;
|
|
background-color: inherit;
|
|
color: inherit;
|
|
`);
|
|
|
|
export const cssTextInput = styled('input', `
|
|
outline: none;
|
|
height: 28px;
|
|
border: 1px solid ${colors.darkGrey};
|
|
border-radius: 3px;
|
|
padding: 0 6px;
|
|
`);
|
|
|
|
const cssSizer = styled('div', `
|
|
visibility: hidden;
|
|
overflow: visible;
|
|
white-space: pre;
|
|
|
|
&:empty:before {
|
|
content: ' '; /* Don't collapse */
|
|
}
|
|
`);
|
|
|
|
enum Status { NORMAL, EDITING, SAVING }
|
|
|
|
type SaveFunc = (value: string) => Promise<void>;
|
|
|
|
export interface EditableLabelOptions {
|
|
save: SaveFunc;
|
|
args?: Array<DomArg<HTMLDivElement>>;
|
|
inputArgs?: Array<DomArg<HTMLInputElement>>;
|
|
}
|
|
|
|
/**
|
|
* Provides a label that takes in an observable that is set on Enter or loss of focus. Escape
|
|
* cancels editing. Label grows in size with typed input. Validation logic (if any) should happen in
|
|
* the save function, to reject a value simply throw an error, this will revert to the saved one .
|
|
*/
|
|
export function editableLabel(label: Observable<string>, options: EditableLabelOptions) {
|
|
const {save, args, inputArgs} = options;
|
|
|
|
let input: HTMLInputElement;
|
|
let sizer: HTMLSpanElement;
|
|
|
|
function updateSizer() {
|
|
sizer.textContent = input.value;
|
|
}
|
|
|
|
return cssWrapper(
|
|
sizer = cssSizer(label.get()),
|
|
input = rawTextInput(label, save, updateSizer, dom.cls(cssLabelText.className),
|
|
dom.on('focus', () => input.select()),
|
|
...inputArgs ?? [],
|
|
),
|
|
...args ?? [],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Provides a text input element that pretty much behaves like the editableLabel only it shows as a
|
|
* regular input within a rigid static frame. It takes in an observable that is setf on Enter or loss
|
|
* of focus. Escape cancels editing. Validation logic (if any) should happen in the save function,
|
|
* to reject a value simply throw an error, this will revert to the the saved one.
|
|
*/
|
|
export function textInput(label: Observable<string>, save: SaveFunc, ...args: Array<DomArg<HTMLInputElement>>) {
|
|
return rawTextInput(label, save, noop, dom.cls(cssTextInput.className), ...args);
|
|
}
|
|
|
|
/**
|
|
* A helper that implements all the saving logic for both editableLabel and textInput.
|
|
*/
|
|
export function rawTextInput(value: Observable<string>, save: SaveFunc, onChange: () => void,
|
|
...args: Array<DomArg<HTMLInputElement>>) {
|
|
let status: Status = Status.NORMAL;
|
|
let inputEl: HTMLInputElement;
|
|
|
|
// When label changes updates the input, unless in the middle of editing.
|
|
const lis = value.addListener((val) => { if (status !== Status.EDITING) { setValue(val); } });
|
|
|
|
function setValue(val: string) {
|
|
inputEl.value = val;
|
|
onChange();
|
|
}
|
|
|
|
function revertToSaved() {
|
|
setValue(value.get());
|
|
status = Status.NORMAL;
|
|
inputEl.blur();
|
|
}
|
|
|
|
async function saveEdit() {
|
|
if (status === Status.EDITING) {
|
|
status = Status.SAVING;
|
|
inputEl.disabled = true;
|
|
// Ignore errors; save() callback is expected to handle their reporting.
|
|
try { await save(inputEl.value); } catch (e) { /* ignore */ }
|
|
inputEl.disabled = false;
|
|
revertToSaved();
|
|
} else if (status === Status.NORMAL) {
|
|
// If we are not editing, nothing to save, but lets end in the expected blurred state.
|
|
inputEl.blur();
|
|
}
|
|
}
|
|
|
|
return inputEl = dom('input',
|
|
dom.autoDispose(lis),
|
|
{type: 'text'},
|
|
dom.on('input', () => { status = Status.EDITING; onChange(); }),
|
|
dom.on('blur', saveEdit),
|
|
// we set the attribute to the initial value and keep it updated for the convenience of usage
|
|
// with selenium webdriver
|
|
dom.attr('value', value),
|
|
dom.onKeyDown({
|
|
Escape: revertToSaved,
|
|
Enter: saveEdit,
|
|
}),
|
|
...args
|
|
);
|
|
}
|