mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-20 09:04:08 +00:00
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
|
/**
|
||
|
* 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;
|
||
|
}
|
||
|
`);
|