mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move client code to core
Summary: This moves all client code to core, and makes minimal fix-ups to get grist and grist-core to compile correctly. The client works in core, but I'm leaving clean-up around the build and bundles to follow-up. Test Plan: existing tests pass; server-dev bundle looks sane Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2627
This commit is contained in:
149
app/client/ui2018/editableLabel.ts
Normal file
149
app/client/ui2018/editableLabel.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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, DomElementArg, 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 apperance */
|
||||
-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>;
|
||||
|
||||
/**
|
||||
* 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>, save: SaveFunc, ...args: DomElementArg[]) {
|
||||
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()),
|
||||
...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: DomElementArg[]) {
|
||||
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: DomElementArg[]) {
|
||||
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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user