You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/app/client/ui/resizeHandle.ts

156 lines
5.5 KiB

/**
* 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;
}
`);