blob: ec5f094cde5f9f96cae80f8c40adf12aa1eb5107 [file]
// Copyright (C) 2025 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import './resize_handle.scss';
import m from 'mithril';
import type {HTMLAttrs} from './common';
import type {MithrilEvent} from '../base/mithril_utils';
export interface ResizeHandleAttrs extends HTMLAttrs {
// Called with delta (relative change)
onResize?(deltaPx: number): void;
// Called with absolute position relative to offsetParent
onResizeAbsolute?(positionPx: number): void;
onResizeStart?(): void;
onResizeEnd?(): void;
// Direction of the resize handle:
// - 'vertical' (default): horizontal bar that can be dragged up/down
// - 'horizontal': vertical bar that can be dragged left/right
readonly direction?: 'vertical' | 'horizontal';
}
export class ResizeHandle implements m.ClassComponent<ResizeHandleAttrs> {
private handleElement?: HTMLElement;
private previousY: number | undefined;
private previousX: number | undefined;
oncreate(vnode: m.VnodeDOM<ResizeHandleAttrs, this>) {
this.handleElement = vnode.dom as HTMLElement;
}
private endDrag(attrs: ResizeHandleAttrs, pointerId: number) {
if (this.previousY !== undefined || this.previousX !== undefined) {
this.previousY = undefined;
this.previousX = undefined;
this.handleElement!.releasePointerCapture(pointerId);
attrs.onResizeEnd?.();
}
}
view({attrs}: m.CVnode<ResizeHandleAttrs>): m.Children {
const {
onResize: _onResize,
onResizeAbsolute: _onResizeAbsolute,
onResizeStart: _onResizeStart,
onResizeEnd: _onResizeEnd,
direction = 'vertical',
...rest
} = attrs;
const isHorizontal = direction === 'horizontal';
return m('.pf-resize-handle', {
class: isHorizontal
? 'pf-resize-handle--horizontal'
: 'pf-resize-handle--vertical',
oncontextmenu: (e: Event) => {
e.preventDefault();
},
onpointerdown: (e: PointerEvent) => {
const offsetParent = this.handleElement?.offsetParent as HTMLElement;
if (isHorizontal) {
const offsetLeft = offsetParent?.getBoundingClientRect().left ?? 0;
const mouseOffsetX = e.clientX - offsetLeft;
this.previousX = mouseOffsetX;
} else {
const offsetTop = offsetParent?.getBoundingClientRect().top ?? 0;
const mouseOffsetY = e.clientY - offsetTop;
this.previousY = mouseOffsetY;
}
this.handleElement!.setPointerCapture(e.pointerId);
attrs.onResizeStart?.();
e.stopPropagation();
},
onpointermove: (e: MithrilEvent<PointerEvent>) => {
const offsetParent = this.handleElement?.offsetParent as HTMLElement;
// We typically just resize some element when dragging the handle, so we
// tell Mithril not to redraw after this event.
e.redraw = false;
// Note: We don't check hasPointerCapture() here because pointer capture
// already ensures we only receive move events during an active drag.
// The previousX/previousY check is sufficient to determine drag state.
if (isHorizontal) {
const offsetLeft = offsetParent?.getBoundingClientRect().left ?? 0;
const mouseOffsetX = e.clientX - offsetLeft;
if (this.previousX !== undefined) {
attrs.onResize?.(mouseOffsetX - this.previousX);
attrs.onResizeAbsolute?.(mouseOffsetX);
this.previousX = mouseOffsetX;
}
} else {
const offsetTop = offsetParent?.getBoundingClientRect().top ?? 0;
const mouseOffsetY = e.clientY - offsetTop;
if (this.previousY !== undefined) {
attrs.onResize?.(mouseOffsetY - this.previousY);
attrs.onResizeAbsolute?.(mouseOffsetY);
this.previousY = mouseOffsetY;
}
}
},
onpointerup: (e: PointerEvent) => {
this.endDrag(attrs, e.pointerId);
},
onpointercancel: (e: PointerEvent) => {
this.endDrag(attrs, e.pointerId);
},
onpointercapturelost: (e: PointerEvent) => {
this.endDrag(attrs, e.pointerId);
},
...rest,
});
}
}