mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
Initial config with a few files that build on client and server side.
This commit is contained in:
commit
ec182792be
68
.gitignore
vendored
Normal file
68
.gitignore
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
/node_modules/
|
||||
/build/
|
||||
/static/*.bundle.js
|
||||
/static/*.bundle.js.map
|
||||
|
||||
# Build helper files.
|
||||
/.build*
|
||||
|
||||
*.swp
|
||||
*.pyc
|
||||
*.bak
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
/node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
9
app/client/tsconfig.json
Normal file
9
app/client/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../buildtools/tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../build/app/client"
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../common" }
|
||||
]
|
||||
}
|
193
app/client/ui/PagePanels.ts
Normal file
193
app/client/ui/PagePanels.ts
Normal file
@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Note that it assumes the presence of cssVars.cssRootVars on <body>.
|
||||
*/
|
||||
import {resizeFlexVHandle} from 'app/client/ui/resizeHandle';
|
||||
import {transition} from 'app/client/ui/transitions';
|
||||
import {colors} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {dom, DomArg, noTestId, Observable, styled, TestId} from "grainjs";
|
||||
|
||||
export interface PageSidePanel {
|
||||
// Note that widths need to start out with a correct default in JS (having them in CSS is not
|
||||
// enough), needed for open/close transitions.
|
||||
panelWidth: Observable<number>;
|
||||
panelOpen: Observable<boolean>;
|
||||
hideOpener?: boolean; // If true, don't show the opener handle.
|
||||
header: DomArg;
|
||||
content: DomArg;
|
||||
}
|
||||
|
||||
export interface PageContents {
|
||||
leftPanel: PageSidePanel;
|
||||
rightPanel?: PageSidePanel; // If omitted, the right panel isn't shown at all.
|
||||
|
||||
headerMain: DomArg;
|
||||
contentMain: DomArg;
|
||||
|
||||
onResize?: () => void; // Callback for when either pane is opened, closed, or resized.
|
||||
testId?: TestId;
|
||||
}
|
||||
|
||||
export function pagePanels(page: PageContents) {
|
||||
const testId = page.testId || noTestId;
|
||||
const left = page.leftPanel;
|
||||
const right = page.rightPanel;
|
||||
const onResize = page.onResize || (() => null);
|
||||
|
||||
return cssPageContainer(
|
||||
cssLeftPane(
|
||||
testId('left-panel'),
|
||||
cssTopHeader(left.header),
|
||||
left.content,
|
||||
|
||||
dom.style('width', (use) => use(left.panelOpen) ? use(left.panelWidth) + 'px' : ''),
|
||||
|
||||
// Opening/closing the left pane, with transitions.
|
||||
cssLeftPane.cls('-open', left.panelOpen),
|
||||
transition(left.panelOpen, {
|
||||
prepare(elem, open) { elem.style.marginRight = (open ? -1 : 1) * (left.panelWidth.get() - 48) + 'px'; },
|
||||
run(elem, open) { elem.style.marginRight = ''; },
|
||||
finish: onResize,
|
||||
}),
|
||||
),
|
||||
|
||||
// Resizer for the left pane.
|
||||
// TODO: resizing to small size should collapse. possibly should allow expanding too
|
||||
cssResizeFlexVHandle(
|
||||
{target: 'left', onSave: (val) => { left.panelWidth.set(val); onResize(); }},
|
||||
testId('left-resizer'),
|
||||
dom.show(left.panelOpen)),
|
||||
|
||||
// Show plain border when the resize handle is hidden.
|
||||
cssResizeDisabledBorder(dom.hide(left.panelOpen)),
|
||||
|
||||
cssMainPane(
|
||||
cssTopHeader(
|
||||
(left.hideOpener ? null :
|
||||
cssPanelOpener('PanelRight', cssPanelOpener.cls('-open', left.panelOpen),
|
||||
testId('left-opener'),
|
||||
dom.on('click', () => toggleObs(left.panelOpen)))
|
||||
),
|
||||
|
||||
page.headerMain,
|
||||
|
||||
(!right || right.hideOpener ? null :
|
||||
cssPanelOpener('PanelLeft', cssPanelOpener.cls('-open', right.panelOpen),
|
||||
testId('right-opener'),
|
||||
dom.on('click', () => toggleObs(right.panelOpen)))
|
||||
),
|
||||
),
|
||||
page.contentMain,
|
||||
),
|
||||
(right ? [
|
||||
// Resizer for the right pane.
|
||||
cssResizeFlexVHandle(
|
||||
{target: 'right', onSave: (val) => { right.panelWidth.set(val); onResize(); }},
|
||||
testId('right-resizer'),
|
||||
dom.show(right.panelOpen)),
|
||||
|
||||
cssRightPane(
|
||||
testId('right-panel'),
|
||||
cssTopHeader(right.header),
|
||||
right.content,
|
||||
|
||||
dom.style('width', (use) => use(right.panelOpen) ? use(right.panelWidth) + 'px' : ''),
|
||||
|
||||
// Opening/closing the right pane, with transitions.
|
||||
cssRightPane.cls('-open', right.panelOpen),
|
||||
transition(right.panelOpen, {
|
||||
prepare(elem, open) { elem.style.marginLeft = (open ? -1 : 1) * right.panelWidth.get() + 'px'; },
|
||||
run(elem, open) { elem.style.marginLeft = ''; },
|
||||
finish: onResize,
|
||||
}),
|
||||
)] : null
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function toggleObs(boolObs: Observable<boolean>) {
|
||||
boolObs.set(!boolObs.get());
|
||||
}
|
||||
|
||||
const cssVBox = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`);
|
||||
const cssHBox = styled('div', `
|
||||
display: flex;
|
||||
`);
|
||||
const cssPageContainer = styled(cssHBox, `
|
||||
position: absolute;
|
||||
isolation: isolate; /* Create a new stacking context */
|
||||
z-index: 0; /* As of March 2019, isolation does not have Edge support, so force one with z-index */
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
background-color: ${colors.lightGrey};
|
||||
`);
|
||||
export const cssLeftPane = styled(cssVBox, `
|
||||
position: relative;
|
||||
background-color: ${colors.lightGrey};
|
||||
width: 48px;
|
||||
margin-right: 0px;
|
||||
overflow: hidden;
|
||||
transition: margin-right 0.4s;
|
||||
&-open {
|
||||
width: 240px;
|
||||
min-width: 160px;
|
||||
max-width: 320px;
|
||||
}
|
||||
`);
|
||||
const cssMainPane = styled(cssVBox, `
|
||||
position: relative;
|
||||
flex: 1 1 0px;
|
||||
min-width: 0px;
|
||||
background-color: white;
|
||||
z-index: 1;
|
||||
`);
|
||||
const cssRightPane = styled(cssVBox, `
|
||||
position: relative;
|
||||
background-color: ${colors.lightGrey};
|
||||
width: 0px;
|
||||
margin-left: 0px;
|
||||
overflow: hidden;
|
||||
transition: margin-left 0.4s;
|
||||
z-index: 0;
|
||||
&-open {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
max-width: 320px;
|
||||
}
|
||||
`);
|
||||
const cssTopHeader = styled('div', `
|
||||
height: 48px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid ${colors.mediumGrey};
|
||||
`);
|
||||
const cssResizeFlexVHandle = styled(resizeFlexVHandle, `
|
||||
--resize-handle-color: ${colors.mediumGrey};
|
||||
--resize-handle-highlight: ${colors.lightGreen};
|
||||
`);
|
||||
const cssResizeDisabledBorder = styled('div', `
|
||||
flex: none;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: ${colors.mediumGrey};
|
||||
`);
|
||||
const cssPanelOpener = styled(icon, `
|
||||
flex: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 8px 8px;
|
||||
cursor: pointer;
|
||||
-webkit-mask-size: 16px 16px;
|
||||
background-color: ${colors.lightGreen};
|
||||
transition: transform 0.4s;
|
||||
&:hover { background-color: ${colors.darkGreen}; }
|
||||
&-open { transform: rotateY(180deg); }
|
||||
`);
|
47
app/client/ui/mouseDrag.ts
Normal file
47
app/client/ui/mouseDrag.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Small utility to help with processing mouse-drag events. Usage is:
|
||||
* dom('div', mouseDrag((startEvent, elem) => ({
|
||||
* onMove(moveEvent) { ... },
|
||||
* onStop(stopEvent) { ... },
|
||||
* })));
|
||||
*
|
||||
* The passed-in callback is called on 'mousedown' events. It may return null to ignore the event.
|
||||
* Otherwise, it should return a handler for mousemove/mouseup: we will then subscribe to these
|
||||
* events, and clean up on mouseup.
|
||||
*/
|
||||
import {dom, DomElementMethod, IDisposable} from "grainjs";
|
||||
|
||||
|
||||
export interface MouseDragHandler {
|
||||
onMove(moveEv: MouseEvent): void;
|
||||
onStop(endEv: MouseEvent): void;
|
||||
}
|
||||
|
||||
export type MouseDragStart = (startEv: MouseEvent, elem: Element) => MouseDragHandler|null;
|
||||
|
||||
export function mouseDragElem(elem: HTMLElement, onStart: MouseDragStart): IDisposable {
|
||||
|
||||
// This prevents the default text-drag behavior when elem is part of a text selection.
|
||||
elem.style.userSelect = 'none';
|
||||
|
||||
return dom.onElem(elem, 'mousedown', (ev, el) => _startDragging(ev, el, onStart));
|
||||
}
|
||||
export function mouseDrag(onStart: MouseDragStart): DomElementMethod {
|
||||
return (elem) => { mouseDragElem(elem, onStart); };
|
||||
}
|
||||
|
||||
function _startDragging(startEv: MouseEvent, elem: Element, onStart: MouseDragStart) {
|
||||
const dragHandler = onStart(startEv, elem);
|
||||
if (dragHandler) {
|
||||
|
||||
const {onMove, onStop} = dragHandler;
|
||||
const upLis = dom.onElem(document, 'mouseup', stop, {useCapture: true});
|
||||
const moveLis = dom.onElem(document, 'mousemove', onMove, {useCapture: true});
|
||||
|
||||
function stop(stopEv: MouseEvent) {
|
||||
moveLis.dispose();
|
||||
upLis.dispose();
|
||||
onStop(stopEv);
|
||||
}
|
||||
}
|
||||
}
|
155
app/client/ui/resizeHandle.ts
Normal file
155
app/client/ui/resizeHandle.ts
Normal file
@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Exports resizeFlexVHandle() for resizing flex items. The returned handle should be attached to
|
||||
* a flexbox container next to the item that needs resizing. Usage:
|
||||
*
|
||||
* dom('div.flex',
|
||||
* dom('div.child-to-be-resized', ...),
|
||||
* resizeFlexVHandle({target: 'left', onSave: (width) => { ... })
|
||||
* dom('div.other-children', ...),
|
||||
* )
|
||||
*
|
||||
* The .target parameter determines whether to resize the left or right sibling of the handle. The
|
||||
* handle shows up as a 1px line of color --resize-handle-color. On hover and while resizing, it
|
||||
* changes to --resize-handle-highlight.
|
||||
*
|
||||
* The handle may be dragged to change the width of the target. It sets style.width while
|
||||
* dragging, and calls .onSave(width) at the end, and optionally .onDrag(width) while dragging.
|
||||
*
|
||||
* You may limit the width of the target with min-width, max-width properties as usual.
|
||||
*
|
||||
* At the moment, flexbox width resizing is the only need, but the same approach is intended to be
|
||||
* easily extended to non-flexbox situation, and to height-resizing.
|
||||
*/
|
||||
import {mouseDrag} from 'app/client/ui/mouseDrag';
|
||||
import {DomElementArg, styled} from "grainjs";
|
||||
|
||||
export type ChangeFunc = (value: number) => void;
|
||||
export type Edge = 'left' | 'right';
|
||||
|
||||
export interface IResizeFlexOptions {
|
||||
// Whether to change the width of the flex item to the left or to the right of this handle.
|
||||
target: 'left'|'right';
|
||||
onDrag?(value: number): void;
|
||||
onSave(value: number): void;
|
||||
}
|
||||
|
||||
export interface IResizeOptions {
|
||||
prop: 'width' | 'height';
|
||||
sign: 1 | -1;
|
||||
getTarget(handle: Element): Element|null;
|
||||
onDrag?(value: number): void;
|
||||
onSave(value: number): void;
|
||||
}
|
||||
|
||||
// See module documentation for usage.
|
||||
export function resizeFlexVHandle(options: IResizeFlexOptions, ...args: DomElementArg[]): Element {
|
||||
const resizeOptions: IResizeOptions = {
|
||||
prop: 'width',
|
||||
sign: options.target === 'left' ? 1 : -1,
|
||||
getTarget(handle: Element) {
|
||||
return options.target === 'left' ? handle.previousElementSibling : handle.nextElementSibling;
|
||||
},
|
||||
onDrag: options.onDrag,
|
||||
onSave: options.onSave,
|
||||
};
|
||||
return cssResizeFlexVHandle(
|
||||
mouseDrag((ev, handle) => onResizeStart(ev, handle, resizeOptions)),
|
||||
...args);
|
||||
}
|
||||
|
||||
// The core of the implementation. This is intended to be very general, so as to be adaptable to
|
||||
// other kinds of resizing (edge of parent, resizing height) by only attaching this to the
|
||||
// mouseDrag() event with suitable options. See resizeFlexVHandle().
|
||||
function onResizeStart(startEv: MouseEvent, handle: Element, options: IResizeOptions) {
|
||||
const target = options.getTarget(handle) as HTMLElement|null;
|
||||
if (!target) { return null; }
|
||||
|
||||
const {sign, prop, onDrag, onSave} = options;
|
||||
const startSize = getComputedSize(target, prop);
|
||||
|
||||
// Set the body cursor to that on the handle, so that it doesn't jump to different shapes as
|
||||
// the mouse moves temporarily outside of the handle. (Approach from jqueryui-resizable.)
|
||||
const startBodyCursor = document.body.style.cursor;
|
||||
document.body.style.cursor = window.getComputedStyle(handle).cursor;
|
||||
|
||||
handle.classList.add(cssResizeDragging.className);
|
||||
|
||||
return {
|
||||
// While moving, just adjust the size of the target, relying on min-width/max-width for
|
||||
// constraints.
|
||||
onMove(ev: MouseEvent) {
|
||||
target.style[prop] = (startSize + sign * (ev.pageX - startEv.pageX)) + 'px';
|
||||
if (onDrag) { onDrag(getComputedSize(target, prop)); }
|
||||
},
|
||||
|
||||
// At the end, undo temporary changes, and call onSave() with the actual size.
|
||||
onStop(ev: MouseEvent) {
|
||||
handle.classList.remove(cssResizeDragging.className);
|
||||
|
||||
// Restore the body cursor to what it was.
|
||||
document.body.style.cursor = startBodyCursor;
|
||||
|
||||
target.style[prop] = (startSize + sign * (ev.pageX - startEv.pageX)) + 'px';
|
||||
onSave(getComputedSize(target, prop));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Compute the CSS width or height of the element. If element.style[prop] is set to it, it should
|
||||
// be unchanged. (Note that when an element has borders or padding, the size from
|
||||
// getBoundingClientRect() would be different, and isn't suitable for style[prop].)
|
||||
function getComputedSize(elem: Element, prop: 'width'|'height'): number {
|
||||
const sizePx = window.getComputedStyle(elem)[prop];
|
||||
const sizeNum = sizePx && parseFloat(sizePx);
|
||||
// If we can't get the size, fall back to getBoundingClientRect().
|
||||
return Number.isFinite(sizeNum as number) ? sizeNum as number : elem.getBoundingClientRect()[prop];
|
||||
}
|
||||
|
||||
const cssResizeFlexVHandle = styled('div', `
|
||||
position: relative;
|
||||
flex: none;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
z-index: 10;
|
||||
|
||||
/* width with negative margins leaves exactly 1px line in the middle */
|
||||
width: 7px;
|
||||
margin: auto -3px;
|
||||
|
||||
/* two highlighted 1px lines are placed in the middle, normal and highlighted one */
|
||||
&::before, &::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
left: 3px;
|
||||
}
|
||||
&::before {
|
||||
background-color: var(--resize-handle-color, lightgrey);
|
||||
}
|
||||
/* the highlighted line is shown on hover with opacity transition */
|
||||
&::after {
|
||||
background-color: var(--resize-handle-highlight, black);
|
||||
opacity: 0;
|
||||
transition: opacity linear 0.2s;
|
||||
}
|
||||
&:hover::after {
|
||||
opacity: 1;
|
||||
transition: opacity linear 0.2s 0.2s;
|
||||
}
|
||||
/* the highlighted line is also always shown while dragging */
|
||||
&-dragging::after {
|
||||
opacity: 1;
|
||||
transition: none !important;
|
||||
}
|
||||
`);
|
||||
|
||||
// May be applied to Handle class to show the highlighted line while dragging.
|
||||
const cssResizeDragging = styled('div', `
|
||||
&::after {
|
||||
opacity: 1;
|
||||
transition: none !important;
|
||||
}
|
||||
`);
|
116
app/client/ui/transitions.ts
Normal file
116
app/client/ui/transitions.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* A helper for CSS transitions. Usage:
|
||||
*
|
||||
* dom(...,
|
||||
* transition(obs, {
|
||||
* prepare(elem, val) { SET STYLE WITH TRANSITIONS OFF },
|
||||
* run(elem, val) { SET STYLE WITH TRANSITIONS ON },
|
||||
* // finish(elem, val) { console.log("transition finished"); }
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Allows modifiying styles in response to changes in an observable. Any time the observable
|
||||
* changes, the prepare() callback allows preparing the styles, with transitions off. Then
|
||||
* the run() callback can set the styles that will be subject to transitions.
|
||||
*
|
||||
* The actual transition styles (e.g. 'transition: width 0.2s') should be set on elem elsewhere.
|
||||
*
|
||||
* The optional finish() callback is called when the transition ends. If CSS transitions are set
|
||||
* on multiple properties, only the first one is used to determine when the transition ends.
|
||||
*
|
||||
* All callbacks are called with the element this is attached to, and the value of the observable.
|
||||
*
|
||||
* The recommendation is to avoid setting styles at transition end, since it's not entirely
|
||||
* reliable; it's better to arrange CSS so that the desired final styles can be set in run(). The
|
||||
* finish() callback is intended to tell other code that the element is in its transitioned state.
|
||||
*
|
||||
* When the observable changes during a transition, the prepare() callback is skipped, the run()
|
||||
* callback is called, and the finish() callback delayed until the new transition ends.
|
||||
*
|
||||
* If other styles are changed (or css classes applied) when the observable changes, subscriptions
|
||||
* triggered BEFORE the transition() subscription are applied with transitions OFF (like
|
||||
* prepare()); those triggered AFTER are subject to transitions (like run()).
|
||||
*/
|
||||
|
||||
import {BindableValue, Disposable, dom, DomElementMethod, subscribeElem} from 'grainjs';
|
||||
|
||||
export interface ITransitionLogic<T = void> {
|
||||
prepare(elem: HTMLElement, value: T): void;
|
||||
run(elem: HTMLElement, value: T): void;
|
||||
finish?(elem: HTMLElement, value: T): void;
|
||||
}
|
||||
|
||||
export function transition<T>(obs: BindableValue<T>, trans: ITransitionLogic<T>): DomElementMethod {
|
||||
const {prepare, run, finish} = trans;
|
||||
let watcher: TransitionWatcher|null = null;
|
||||
let firstCall = true;
|
||||
return (elem) => subscribeElem<T>(elem, obs, (val) => {
|
||||
// First call is initialization, don't treat it as a transition
|
||||
if (firstCall) { firstCall = false; return; }
|
||||
if (watcher) {
|
||||
watcher.reschedule();
|
||||
} else {
|
||||
watcher = new TransitionWatcher(elem);
|
||||
watcher.onDispose(() => {
|
||||
watcher = null;
|
||||
if (finish) { finish(elem, val); }
|
||||
});
|
||||
|
||||
// Call prepare() with transitions turned off.
|
||||
const prior = elem.style.transitionProperty;
|
||||
elem.style.transitionProperty = 'none';
|
||||
prepare(elem, val);
|
||||
|
||||
// Recompute styles while transitions are off. See https://stackoverflow.com/a/16575811/328565
|
||||
// for explanation and https://stackoverflow.com/a/31862081/328565 for the recommendation used
|
||||
// here to trigger a style computation without a reflow.
|
||||
window.getComputedStyle(elem).opacity; // tslint:disable-line:no-unused-expression
|
||||
|
||||
// Restore transitions before run().
|
||||
elem.style.transitionProperty = prior;
|
||||
}
|
||||
run(elem, val);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for waiting for an active transition to end. Beyond listening to 'transitionend', it
|
||||
* does a few things:
|
||||
*
|
||||
* (1) if the transition lists multiple properties, only the first property and duration are used
|
||||
* ('transitionend' on additional properties is inconsistent across browsers).
|
||||
* (2) if 'transitionend' fails to fire, the transition is considered ended when duration elapses,
|
||||
* plus 10ms grace period (to let 'transitionend' fire first normally).
|
||||
* (3) reschedule() allows resetting the timer if a new transition is known to have started.
|
||||
*
|
||||
* When the transition ends, TransitionWatcher disposes itself. Its onDispose() method allows
|
||||
* registering callbacks.
|
||||
*/
|
||||
class TransitionWatcher extends Disposable {
|
||||
private _propertyName: string;
|
||||
private _durationMs: number;
|
||||
private _timer: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(elem: Element) {
|
||||
super();
|
||||
const style = window.getComputedStyle(elem);
|
||||
this._propertyName = style.transitionProperty.split(",")[0].trim();
|
||||
|
||||
// Gets the duration of the transition from the styles of the given element, in ms.
|
||||
// FF and Chrome both return transitionDuration in seconds (e.g. "0.150s") In case of multiple
|
||||
// values, e.g. "0.150s, 2s"; parseFloat will just parse the first one.
|
||||
const duration = style.transitionDuration;
|
||||
this._durationMs = ((duration && parseFloat(duration)) || 0) * 1000;
|
||||
|
||||
this.autoDispose(dom.onElem(elem, 'transitionend', (e) =>
|
||||
(e.propertyName === this._propertyName) && this.dispose()));
|
||||
|
||||
this._timer = setTimeout(() => this.dispose(), this._durationMs + 10);
|
||||
this.onDispose(() => clearTimeout(this._timer));
|
||||
}
|
||||
|
||||
public reschedule() {
|
||||
clearTimeout(this._timer);
|
||||
this._timer = setTimeout(() => this.dispose(), this._durationMs + 10);
|
||||
}
|
||||
}
|
3
app/client/ui2018/IconList.ts
Normal file
3
app/client/ui2018/IconList.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// tslint:disable:max-line-length
|
||||
export type IconName = "ChartArea" | "ChartBar" | "ChartKaplan" | "ChartLine" | "ChartPie" | "TypeCard" | "TypeCardList" | "TypeCell" | "TypeChart" | "TypeCustom" | "TypeDetails" | "TypeTable" | "FieldAny" | "FieldAttachment" | "FieldCheckbox" | "FieldChoice" | "FieldColumn" | "FieldDate" | "FieldDateTime" | "FieldFunction" | "FieldFunctionEqual" | "FieldInteger" | "FieldLink" | "FieldNumeric" | "FieldReference" | "FieldSpinner" | "FieldSwitcher" | "FieldTable" | "FieldText" | "FieldTextbox" | "FieldToggle" | "GristLogo" | "ThumbPreview" | "CenterAlign" | "Code" | "Collapse" | "Convert" | "CrossBig" | "CrossSmall" | "Dots" | "Download" | "DragDrop" | "Dropdown" | "DropdownUp" | "Expand" | "EyeHide" | "EyeShow" | "Feedback" | "Filter" | "FilterSimple" | "Folder" | "FunctionResult" | "Help" | "Home" | "Idea" | "Import" | "LeftAlign" | "Log" | "Mail" | "Minus" | "NewNotification" | "Notification" | "Offline" | "Page" | "PanelLeft" | "PanelRight" | "PinBig" | "PinSmall" | "Pivot" | "Plus" | "Redo" | "Remove" | "Repl" | "ResizePanel" | "RightAlign" | "Search" | "Settings" | "Share" | "Sort" | "Tick" | "Undo" | "Validation" | "Widget" | "Wrap" | "Zoom";
|
||||
export const IconList: IconName[] = ["ChartArea", "ChartBar", "ChartKaplan", "ChartLine", "ChartPie", "TypeCard", "TypeCardList", "TypeCell", "TypeChart", "TypeCustom", "TypeDetails", "TypeTable", "FieldAny", "FieldAttachment", "FieldCheckbox", "FieldChoice", "FieldColumn", "FieldDate", "FieldDateTime", "FieldFunction", "FieldFunctionEqual", "FieldInteger", "FieldLink", "FieldNumeric", "FieldReference", "FieldSpinner", "FieldSwitcher", "FieldTable", "FieldText", "FieldTextbox", "FieldToggle", "GristLogo", "ThumbPreview", "CenterAlign", "Code", "Collapse", "Convert", "CrossBig", "CrossSmall", "Dots", "Download", "DragDrop", "Dropdown", "DropdownUp", "Expand", "EyeHide", "EyeShow", "Feedback", "Filter", "FilterSimple", "Folder", "FunctionResult", "Help", "Home", "Idea", "Import", "LeftAlign", "Log", "Mail", "Minus", "NewNotification", "Notification", "Offline", "Page", "PanelLeft", "PanelRight", "PinBig", "PinSmall", "Pivot", "Plus", "Redo", "Remove", "Repl", "ResizePanel", "RightAlign", "Search", "Settings", "Share", "Sort", "Tick", "Undo", "Validation", "Widget", "Wrap", "Zoom"];
|
163
app/client/ui2018/cssVars.ts
Normal file
163
app/client/ui2018/cssVars.ts
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* CSS Variables. To use in your web appication, add `cssRootVars` to the class list for your app's
|
||||
* root node, typically `<body>`.
|
||||
*
|
||||
* The fonts used attempt to default to system fonts as described here:
|
||||
* https://css-tricks.com/snippets/css/system-font-stack/
|
||||
*
|
||||
*/
|
||||
import {ProductFlavor} from 'app/common/gristUrls';
|
||||
import {dom, makeTestId, styled, TestId} from 'grainjs';
|
||||
import values = require('lodash/values');
|
||||
|
||||
const VAR_PREFIX = 'grist';
|
||||
|
||||
class CustomProp {
|
||||
constructor(public name: string, public value: string) { }
|
||||
|
||||
public decl() {
|
||||
return `--${VAR_PREFIX}-${this.name}: ${this.value};`;
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `var(--${VAR_PREFIX}-${this.name})`;
|
||||
}
|
||||
}
|
||||
|
||||
export const colors = {
|
||||
lightGrey: new CustomProp('color-light-grey', '#F7F7F7'),
|
||||
mediumGrey: new CustomProp('color-medium-grey', 'rgba(217,217,217,0.6)'),
|
||||
mediumGreyOpaque: new CustomProp('color-medium-grey-opaque', '#E8E8E8'),
|
||||
darkGrey: new CustomProp('color-dark-grey', '#D9D9D9'),
|
||||
|
||||
light: new CustomProp('color-light', '#FFFFFF'),
|
||||
dark: new CustomProp('color-dark', '#262633'),
|
||||
darkBg: new CustomProp('color-dark-bg', '#262633'),
|
||||
slate: new CustomProp('color-slate', '#929299'),
|
||||
|
||||
lightGreen: new CustomProp('color-light-green', '#16B378'),
|
||||
darkGreen: new CustomProp('color-dark-green', '#009058'),
|
||||
darkerGreen: new CustomProp('color-darker-green', '#007548'),
|
||||
lighterGreen: new CustomProp('color-lighter-green', '#b1ffe2'),
|
||||
|
||||
cursor: new CustomProp('color-cursor', '#16B378'), // cursor is lightGreen
|
||||
selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'),
|
||||
inactiveCursor: new CustomProp('color-inactive-cursor', '#A2E1C9'),
|
||||
|
||||
hover: new CustomProp('color-hover', '#bfbfbf'),
|
||||
error: new CustomProp('color-error', '#D0021B'),
|
||||
backdrop: new CustomProp('color-backdrop', 'rgba(38,38,51,0.9)')
|
||||
|
||||
};
|
||||
|
||||
export const vars = {
|
||||
/* Fonts */
|
||||
fontFamily: new CustomProp('font-family', `-apple-system,BlinkMacSystemFont,Segoe UI,
|
||||
Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol`),
|
||||
|
||||
// This is more monospace and looks better for data that should often align (e.g. to have 00000
|
||||
// take similar space to 11111). This is the main font for user data.
|
||||
fontFamilyData: new CustomProp('font-family-data',
|
||||
`Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol`),
|
||||
|
||||
/* Font sizes */
|
||||
xxsmallFontSize: new CustomProp('xx-font-size', '8px'),
|
||||
xsmallFontSize: new CustomProp('x-small-font-size', '10px'),
|
||||
smallFontSize: new CustomProp('small-font-size', '11px'),
|
||||
mediumFontSize: new CustomProp('medium-font-size', '13px'),
|
||||
largeFontSize: new CustomProp('large-font-size', '16px'),
|
||||
xlargeFontSize: new CustomProp('x-large-font-size', '18px'),
|
||||
xxlargeFontSize: new CustomProp('xx-large-font-size', '20px'),
|
||||
xxxlargeFontSize: new CustomProp('xxx-large-font-size', '22px'),
|
||||
|
||||
/* Controls size and space */
|
||||
controlFontSize: new CustomProp('control-font-size', '12px'),
|
||||
smallControlFontSize: new CustomProp('small-control-font-size', '10px'),
|
||||
bigControlFontSize: new CustomProp('big-control-font-size', '13px'),
|
||||
headerControlFontSize: new CustomProp('header-control-font-size', '22px'),
|
||||
bigControlTextWeight: new CustomProp('big-text-weight', '500'),
|
||||
headerControlTextWeight: new CustomProp('header-text-weight', '600'),
|
||||
|
||||
/* Labels */
|
||||
labelTextSize: new CustomProp('label-text-size', 'medium'),
|
||||
labelTextBg: new CustomProp('label-text-bg', '#FFFFFF'),
|
||||
labelActiveBg: new CustomProp('label-active-bg', '#F0F0F0'),
|
||||
|
||||
controlMargin: new CustomProp('normal-margin', '2px'),
|
||||
controlPadding: new CustomProp('normal-padding', '3px 5px'),
|
||||
tightPadding: new CustomProp('tight-padding', '1px 2px'),
|
||||
loosePadding: new CustomProp('loose-padding', '5px 15px'),
|
||||
|
||||
/* Control colors and borders */
|
||||
primaryBg: new CustomProp('primary-fg', '#16B378'),
|
||||
primaryBgHover: new CustomProp('primary-fg-hover', '#009058'),
|
||||
primaryFg: new CustomProp('primary-bg', '#ffffff'),
|
||||
|
||||
controlBg: new CustomProp('control-bg', '#ffffff'),
|
||||
controlFg: new CustomProp('control-fg', '#16B378'),
|
||||
controlFgHover: new CustomProp('primary-fg-hover', '#009058'),
|
||||
|
||||
controlBorder: new CustomProp('control-border', '1px solid #11B683'),
|
||||
controlBorderRadius: new CustomProp('border-radius', '4px'),
|
||||
|
||||
logoBg: new CustomProp('logo-bg', '#040404'),
|
||||
toastBg: new CustomProp('toast-bg', '#040404'),
|
||||
};
|
||||
|
||||
const cssColors = values(colors).map(v => v.decl()).join('\n');
|
||||
const cssVars = values(vars).map(v => v.decl()).join('\n');
|
||||
const cssFontParams = `
|
||||
font-family: ${vars.fontFamily};
|
||||
font-size: ${vars.mediumFontSize};
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
`;
|
||||
|
||||
// We set box-sizing globally to match bootstrap's setting of border-box, since we are integrating
|
||||
// into an app which already has it set, and it's impossible to make things look consistently with
|
||||
// AND without it. This duplicates bootstrap's setting.
|
||||
const cssBorderBox = `
|
||||
*, *:before, *:after {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
// These styles duplicate bootstrap's global settings, which we rely on even on pages that don't
|
||||
// have bootstrap.
|
||||
const cssInputFonts = `
|
||||
button, input, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
const cssVarsOnly = styled('div', cssColors + cssVars);
|
||||
const cssBodyVars = styled('div', cssFontParams + cssColors + cssVars + cssBorderBox + cssInputFonts);
|
||||
|
||||
const cssBody = styled('body', `
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
`);
|
||||
|
||||
const cssRoot = styled('html', `
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`);
|
||||
|
||||
export const cssRootVars = cssBodyVars.className;
|
||||
|
||||
// Also make a globally available testId, with a simple "test-" prefix (i.e. in tests, query css
|
||||
// class ".test-{name}". Ideally, we'd use noTestId() instead in production.
|
||||
export const testId: TestId = makeTestId('test-');
|
||||
|
||||
/**
|
||||
* Attaches the global css properties to the document's root to them available in the page.
|
||||
*/
|
||||
export function attachCssRootVars(productFlavor: ProductFlavor, varsOnly: boolean = false) {
|
||||
dom.update(document.documentElement!, varsOnly ? dom.cls(cssVarsOnly.className) : dom.cls(cssRootVars));
|
||||
document.documentElement!.classList.add(cssRoot.className);
|
||||
document.body.classList.add(cssBody.className);
|
||||
}
|
75
app/client/ui2018/icons.ts
Normal file
75
app/client/ui2018/icons.ts
Normal file
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Exports a single `icon` function which returns a DOM Element for the given icon name.
|
||||
* Names are of type `IconName` imported from `IconList.ts`, which is auto-generated during the
|
||||
* build process.
|
||||
*
|
||||
* In order to use the icons, you must first include the generated CSS file:
|
||||
*
|
||||
* <link rel="stylesheet" href="icons.css">
|
||||
*
|
||||
* The CSS file encodes each icon as a `url()` with base64 DataURI of the icon SVG and saves it
|
||||
* as a CSS :root var of the form --icon-${name}. It also includes a class for each icon that
|
||||
* uses the variable to set the mask-image (vs background-image, allowing you to change colors
|
||||
* using background-color):
|
||||
*
|
||||
* .icon.Search_icon { -webkit-mask-image: var(--icon-Search); }
|
||||
*
|
||||
* This approach is more performant than inlining SVGs or using <symbol> with <use>.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* // Display icon with default color and size
|
||||
* dom('div',
|
||||
* icon('Search')
|
||||
* );
|
||||
*
|
||||
* // Display bigger icon in blue
|
||||
* const bigBlueIcon = styled(icon, `
|
||||
* background-color: blue;
|
||||
* width: 32px;
|
||||
* height: 32px;
|
||||
* `);
|
||||
* dom('div',
|
||||
* bigBlueIcon('Search')
|
||||
* )
|
||||
*
|
||||
* // Use icon image directly in css to style a checkbox
|
||||
* // Refer to https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Advanced_styling_for_HTML_forms
|
||||
* const checkbox = styled('input#checkbox', `
|
||||
* -webkit-appearance: none;
|
||||
* -moz-appearance: none;
|
||||
* width: 1rem;
|
||||
* height: 1rem;
|
||||
* border: 1px solid blue;
|
||||
*
|
||||
* &:checked::before {
|
||||
* position: absolute;
|
||||
* content: var(--icon-Select);
|
||||
* }
|
||||
* `);
|
||||
*/
|
||||
|
||||
import { dom, DomElementArg, styled } from 'grainjs';
|
||||
import { IconName } from './IconList';
|
||||
|
||||
/**
|
||||
* Defaults for all icons.
|
||||
*/
|
||||
const iconDiv = styled('div', `
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--icon-color, black);
|
||||
`);
|
||||
|
||||
export function icon(name: IconName, ...domArgs: DomElementArg[]): HTMLElement {
|
||||
return iconDiv(
|
||||
dom.style('-webkit-mask-image', `var(--icon-${name})`),
|
||||
...domArgs
|
||||
);
|
||||
}
|
1
app/common/gristUrls.ts
Normal file
1
app/common/gristUrls.ts
Normal file
@ -0,0 +1 @@
|
||||
export type ProductFlavor = 'grist';
|
6
app/common/tsconfig.json
Normal file
6
app/common/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../buildtools/tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../build/app/common",
|
||||
}
|
||||
}
|
24
app/server/server.ts
Normal file
24
app/server/server.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import {AddressInfo} from 'net';
|
||||
|
||||
const G = {
|
||||
port: parseInt(process.env.PORT!, 10) || 8484,
|
||||
host: process.env.HOST || 'localhost',
|
||||
};
|
||||
|
||||
export async function main() {
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
app.use(express.static('static'));
|
||||
|
||||
// Start listening.
|
||||
await new Promise((resolve, reject) => server.listen(G.port, G.host, resolve).on('error', reject));
|
||||
const address = server.address() as AddressInfo;
|
||||
console.warn(`Server listening at http://${address.address}:${address.port}`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((err) => console.error(err));
|
||||
}
|
9
app/server/tsconfig.json
Normal file
9
app/server/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../buildtools/tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../build/app/server",
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../common" }
|
||||
]
|
||||
}
|
9
app/tsconfig.json
Normal file
9
app/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./client" },
|
||||
{ "path": "./server" },
|
||||
{ "path": "./common" },
|
||||
]
|
||||
}
|
20
buildtools/tsconfig-base.json
Normal file
20
buildtools/tsconfig-base.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": "..",
|
||||
"composite": true,
|
||||
"plugins": [{
|
||||
"name": "typescript-tslint-plugin"
|
||||
}],
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
56
buildtools/webpack.config.js
Normal file
56
buildtools/webpack.config.js
Normal file
@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
const StatsPlugin = require('stats-webpack-plugin');
|
||||
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
entry: {
|
||||
main: "./build/app/client/ui/PagePanels.js",
|
||||
},
|
||||
output: {
|
||||
filename: "[name].bundle.js",
|
||||
sourceMapFilename: "[file].map",
|
||||
path: path.resolve("./static"),
|
||||
// Workaround for a known issue with webpack + onerror under chrome, see:
|
||||
// https://github.com/webpack/webpack/issues/5681
|
||||
// "We use a source map plugin here with this special configuration
|
||||
// because if we do not - the window.onerror function does not work properly in chrome
|
||||
// and it swallows the errors because normally source maps have begin with webpack:///
|
||||
// here we are changing how the module file names are created
|
||||
// See this bug
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=765909
|
||||
// See this for syntax
|
||||
// https://webpack.js.org/configuration/output/#output-devtoolmodulefilenametemplate
|
||||
// "
|
||||
devtoolModuleFilenameTemplate: "[resourcePath]?[loaders]",
|
||||
crossOriginLoading: "anonymous",
|
||||
},
|
||||
// This creates .map files, and takes webpack a couple of seconds to rebuild while developing,
|
||||
// but provides correct mapping back to typescript, and allows breakpoints to be set in
|
||||
// typescript ("cheap-module-eval-source-map" is faster, but breakpoints are largely broken).
|
||||
devtool: "source-map",
|
||||
resolve: {
|
||||
modules: [
|
||||
path.resolve('./build'),
|
||||
path.resolve('./node_modules')
|
||||
],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre"
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new StatsPlugin(
|
||||
'../.build_stats_js_bundle', // relative to output folder
|
||||
{source: false}, // Omit sources, which unnecessarily make the stats file huge.
|
||||
),
|
||||
// To strip all locales except “en”
|
||||
new MomentLocalesPlugin()
|
||||
],
|
||||
};
|
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "grist-core",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "tsc --build app -w --preserveWatchOutput & webpack --config buildtools/webpack.config.js --mode development --watch --hide-modules & nodemon -w build/app/server -w build/app/common build/app/server/server & wait",
|
||||
"build:prod": "tsc --build app && webpack --config buildtools/webpack.config.js --mode production",
|
||||
"start:prod": "node build/app/server/server"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"@types/node": "10",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"nodemon": "^2.0.4",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"stats-webpack-plugin": "^0.7.0",
|
||||
"typescript": "^3.9.2",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"grainjs": "^1.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.25.3"
|
||||
}
|
||||
}
|
9
static/index.html
Normal file
9
static/index.html
Normal file
@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
</head>
|
||||
<body>
|
||||
<script src='/main.bundle.js'></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user