import { $, addDisposableListener, append, EventHelper, EventLike, getWindow, isHTMLElement } from '../../dom.js';
import { createStyleSheet } from '../../domStylesheets.js';
import { DomEmitter } from '../../event.js';
import { EventType, Gesture } from '../../touch.js';
import { Delayer } from '../../../common/async.js';
import { memoize } from '../../../common/decorators.js';
import { Emitter, Event } from '../../../common/event.js';
import { Disposable, DisposableStore, toDisposable } from '../../../common/lifecycle.js';
import { isMacintosh } from '../../../common/platform.js';
import './sash.css';
const DEBUG = false;
export interface IVerticalSashLayoutProvider {
getVerticalSashLeft(sash: Sash): number;
getVerticalSashTop?(sash: Sash): number;
getVerticalSashHeight?(sash: Sash): number;
}
export interface IHorizontalSashLayoutProvider {
getHorizontalSashTop(sash: Sash): number;
getHorizontalSashLeft?(sash: Sash): number;
getHorizontalSashWidth?(sash: Sash): number;
}
type ISashLayoutProvider = IVerticalSashLayoutProvider | IHorizontalSashLayoutProvider;
export interface ISashEvent {
readonly startX: number;
readonly currentX: number;
readonly startY: number;
readonly currentY: number;
readonly altKey: boolean;
}
export enum OrthogonalEdge {
North = 'north',
South = 'south',
East = 'east',
West = 'west'
}
export interface IBoundarySashes {
readonly top?: Sash;
readonly right?: Sash;
readonly bottom?: Sash;
readonly left?: Sash;
}
export interface ISashOptions {
readonly orientation: Orientation;
readonly size?: number;
readonly orthogonalStartSash?: Sash;
readonly orthogonalEndSash?: Sash;
readonly orthogonalEdge?: OrthogonalEdge;
}
export interface IVerticalSashOptions extends ISashOptions {
readonly orientation: Orientation.VERTICAL;
}
export interface IHorizontalSashOptions extends ISashOptions {
readonly orientation: Orientation.HORIZONTAL;
}
export const enum Orientation {
VERTICAL,
HORIZONTAL
}
export const enum SashState {
Disabled,
AtMinimum,
AtMaximum,
Enabled
}
let globalSize = 4;
const onDidChangeGlobalSize = new Emitter<number>();
export function setGlobalSashSize(size: number): void {
globalSize = size;
onDidChangeGlobalSize.fire(size);
}
let globalHoverDelay = 300;
const onDidChangeHoverDelay = new Emitter<number>();
export function setGlobalHoverDelay(size: number): void {
globalHoverDelay = size;
onDidChangeHoverDelay.fire(size);
}
interface PointerEvent extends EventLike {
readonly pageX: number;
readonly pageY: number;
readonly altKey: boolean;
readonly target: EventTarget | null;
readonly initialTarget?: EventTarget | undefined;
}
interface IPointerEventFactory {
readonly onPointerMove: Event<PointerEvent>;
readonly onPointerUp: Event<PointerEvent>;
dispose(): void;
}
class MouseEventFactory implements IPointerEventFactory {
private readonly disposables = new DisposableStore();
constructor(private el: HTMLElement) { }
@memoize
get onPointerMove(): Event<PointerEvent> {
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mousemove')).event;
}
@memoize
get onPointerUp(): Event<PointerEvent> {
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mouseup')).event;
}
dispose(): void {
this.disposables.dispose();
}
}
class GestureEventFactory implements IPointerEventFactory {
private readonly disposables = new DisposableStore();
@memoize
get onPointerMove(): Event<PointerEvent> {
return this.disposables.add(new DomEmitter(this.el, EventType.Change)).event;
}
@memoize
get onPointerUp(): Event<PointerEvent> {
return this.disposables.add(new DomEmitter(this.el, EventType.End)).event;
}
constructor(private el: HTMLElement) { }
dispose(): void {
this.disposables.dispose();
}
}
class OrthogonalPointerEventFactory implements IPointerEventFactory {
@memoize
get onPointerMove(): Event<PointerEvent> {
return this.factory.onPointerMove;
}
@memoize
get onPointerUp(): Event<PointerEvent> {
return this.factory.onPointerUp;
}
constructor(private factory: IPointerEventFactory) { }
dispose(): void {
}
}
const PointerEventsDisabledCssClass = 'pointer-events-disabled';
export class Sash extends Disposable {
private el: HTMLElement;
private layoutProvider: ISashLayoutProvider;
private orientation: Orientation;
private size: number;
private hoverDelay = globalHoverDelay;
private hoverDelayer = this._register(new Delayer(this.hoverDelay));
private _state: SashState = SashState.Enabled;
private readonly onDidEnablementChange = this._register(new Emitter<SashState>());
private readonly _onDidStart = this._register(new Emitter<ISashEvent>());
private readonly _onDidChange = this._register(new Emitter<ISashEvent>());
private readonly _onDidReset = this._register(new Emitter<void>());
private readonly _onDidEnd = this._register(new Emitter<void>());
private readonly orthogonalStartSashDisposables = this._register(new DisposableStore());
private _orthogonalStartSash: Sash | undefined;
private readonly orthogonalStartDragHandleDisposables = this._register(new DisposableStore());
private _orthogonalStartDragHandle: HTMLElement | undefined;
private readonly orthogonalEndSashDisposables = this._register(new DisposableStore());
private _orthogonalEndSash: Sash | undefined;
private readonly orthogonalEndDragHandleDisposables = this._register(new DisposableStore());
private _orthogonalEndDragHandle: HTMLElement | undefined;
get state(): SashState { return this._state; }
get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; }
get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; }
set state(state: SashState) {
if (this._state === state) {
return;
}
this.el.classList.toggle('disabled', state === SashState.Disabled);
this.el.classList.toggle('minimum', state === SashState.AtMinimum);
this.el.classList.toggle('maximum', state === SashState.AtMaximum);
this._state = state;
this.onDidEnablementChange.fire(state);
}
get onDidStart() { return this._onDidStart.event; }
get onDidChange() { return this._onDidChange.event; }
get onDidReset() { return this._onDidReset.event; }
get onDidEnd() { return this._onDidEnd.event; }
linkedSash: Sash | undefined = undefined;
set orthogonalStartSash(sash: Sash | undefined) {
if (this._orthogonalStartSash === sash) {
return;
}
this.orthogonalStartDragHandleDisposables.clear();
this.orthogonalStartSashDisposables.clear();
if (sash) {
const onChange = (state: SashState) => {
this.orthogonalStartDragHandleDisposables.clear();
if (state !== SashState.Disabled) {
this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start'));
this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove()));
this.orthogonalStartDragHandleDisposables.add(addDisposableListener(this._orthogonalStartDragHandle, 'mouseenter', () => Sash.onMouseEnter(sash)));
this.orthogonalStartDragHandleDisposables.add(addDisposableListener(this._orthogonalStartDragHandle, 'mouseleave', () => Sash.onMouseLeave(sash)));
}
};
this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
onChange(sash.state);
}
this._orthogonalStartSash = sash;
}
set orthogonalEndSash(sash: Sash | undefined) {
if (this._orthogonalEndSash === sash) {
return;
}
this.orthogonalEndDragHandleDisposables.clear();
this.orthogonalEndSashDisposables.clear();
if (sash) {
const onChange = (state: SashState) => {
this.orthogonalEndDragHandleDisposables.clear();
if (state !== SashState.Disabled) {
this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end'));
this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove()));
this.orthogonalEndDragHandleDisposables.add(addDisposableListener(this._orthogonalEndDragHandle, 'mouseenter', () => Sash.onMouseEnter(sash)));
this.orthogonalEndDragHandleDisposables.add(addDisposableListener(this._orthogonalEndDragHandle, 'mouseleave', () => Sash.onMouseLeave(sash)));
}
};
this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
onChange(sash.state);
}
this._orthogonalEndSash = sash;
}
constructor(container: HTMLElement, verticalLayoutProvider: IVerticalSashLayoutProvider, options: IVerticalSashOptions);
constructor(container: HTMLElement, horizontalLayoutProvider: IHorizontalSashLayoutProvider, options: IHorizontalSashOptions);
constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions) {
super();
this.el = append(container, $('.monaco-sash'));
if (options.orthogonalEdge) {
this.el.classList.add(`orthogonal-edge-${options.orthogonalEdge}`);
}
if (isMacintosh) {
this.el.classList.add('mac');
}
this._register(addDisposableListener(this.el, 'mousedown', e => this.onPointerStart(e, new MouseEventFactory(container))));
this._register(addDisposableListener(this.el, 'dblclick', e => this.onPointerDoublePress(e)));
this._register(addDisposableListener(this.el, 'mouseenter', () => Sash.onMouseEnter(this)));
this._register(addDisposableListener(this.el, 'mouseleave', () => Sash.onMouseLeave(this)));
this._register(Gesture.addTarget(this.el));
this._register(addDisposableListener(this.el, EventType.Start, e => this.onPointerStart(e, new GestureEventFactory(this.el))));
let doubleTapTimeout: Timeout | undefined = undefined;
this._register(addDisposableListener(this.el, EventType.Tap, event => {
if (doubleTapTimeout) {
clearTimeout(doubleTapTimeout);
doubleTapTimeout = undefined;
this.onPointerDoublePress(event);
return;
}
clearTimeout(doubleTapTimeout);
doubleTapTimeout = setTimeout(() => doubleTapTimeout = undefined, 250);
}));
if (typeof options.size === 'number') {
this.size = options.size;
if (options.orientation === Orientation.VERTICAL) {
this.el.style.width = `${this.size}px`;
} else {
this.el.style.height = `${this.size}px`;
}
} else {
this.size = globalSize;
this._register(onDidChangeGlobalSize.event(size => {
this.size = size;
this.layout();
}));
}
this._register(onDidChangeHoverDelay.event(delay => this.hoverDelay = delay));
this.layoutProvider = layoutProvider;
this.orthogonalStartSash = options.orthogonalStartSash;
this.orthogonalEndSash = options.orthogonalEndSash;
this.orientation = options.orientation || Orientation.VERTICAL;
if (this.orientation === Orientation.HORIZONTAL) {
this.el.classList.add('horizontal');
this.el.classList.remove('vertical');
} else {
this.el.classList.remove('horizontal');
this.el.classList.add('vertical');
}
this.el.classList.toggle('debug', DEBUG);
this.layout();
}
private onPointerStart(event: PointerEvent, pointerEventFactory: IPointerEventFactory): void {
EventHelper.stop(event);
let isMultisashResize = false;
if (!(event as any).__orthogonalSashEvent) {
const orthogonalSash = this.getOrthogonalSash(event);
if (orthogonalSash) {
isMultisashResize = true;
(event as any).__orthogonalSashEvent = true;
orthogonalSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
}
}
if (this.linkedSash && !(event as any).__linkedSashEvent) {
(event as any).__linkedSashEvent = true;
this.linkedSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
}
if (!this.state) {
return;
}
const iframes = this.el.ownerDocument.getElementsByTagName('iframe');
for (const iframe of iframes) {
iframe.classList.add(PointerEventsDisabledCssClass);
}
const startX = event.pageX;
const startY = event.pageY;
const altKey = event.altKey;
const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
this.el.classList.add('active');
this._onDidStart.fire(startEvent);
const style = createStyleSheet(this.el);
const updateStyle = () => {
let cursor = '';
if (isMultisashResize) {
cursor = 'all-scroll';
} else if (this.orientation === Orientation.HORIZONTAL) {
if (this.state === SashState.AtMinimum) {
cursor = 's-resize';
} else if (this.state === SashState.AtMaximum) {
cursor = 'n-resize';
} else {
cursor = isMacintosh ? 'row-resize' : 'ns-resize';
}
} else {
if (this.state === SashState.AtMinimum) {
cursor = 'e-resize';
} else if (this.state === SashState.AtMaximum) {
cursor = 'w-resize';
} else {
cursor = isMacintosh ? 'col-resize' : 'ew-resize';
}
}
style.textContent = `* { cursor: ${cursor} !important; }`;
};
const disposables = new DisposableStore();
updateStyle();
if (!isMultisashResize) {
this.onDidEnablementChange.event(updateStyle, null, disposables);
}
const onPointerMove = (e: PointerEvent) => {
EventHelper.stop(e, false);
const event: ISashEvent = { startX, currentX: e.pageX, startY, currentY: e.pageY, altKey };
this._onDidChange.fire(event);
};
const onPointerUp = (e: PointerEvent) => {
EventHelper.stop(e, false);
style.remove();
this.el.classList.remove('active');
this._onDidEnd.fire();
disposables.dispose();
for (const iframe of iframes) {
iframe.classList.remove(PointerEventsDisabledCssClass);
}
};
pointerEventFactory.onPointerMove(onPointerMove, null, disposables);
pointerEventFactory.onPointerUp(onPointerUp, null, disposables);
disposables.add(pointerEventFactory);
}
private onPointerDoublePress(e: MouseEvent): void {
const orthogonalSash = this.getOrthogonalSash(e);
if (orthogonalSash) {
orthogonalSash._onDidReset.fire();
}
if (this.linkedSash) {
this.linkedSash._onDidReset.fire();
}
this._onDidReset.fire();
}
private static onMouseEnter(sash: Sash, fromLinkedSash: boolean = false): void {
if (sash.el.classList.contains('active')) {
sash.hoverDelayer.cancel();
sash.el.classList.add('hover');
} else {
sash.hoverDelayer.trigger(() => sash.el.classList.add('hover'), sash.hoverDelay).then(undefined, () => { });
}
if (!fromLinkedSash && sash.linkedSash) {
Sash.onMouseEnter(sash.linkedSash, true);
}
}
private static onMouseLeave(sash: Sash, fromLinkedSash: boolean = false): void {
sash.hoverDelayer.cancel();
sash.el.classList.remove('hover');
if (!fromLinkedSash && sash.linkedSash) {
Sash.onMouseLeave(sash.linkedSash, true);
}
}
clearSashHoverState(): void {
Sash.onMouseLeave(this);
}
layout(): void {
if (this.orientation === Orientation.VERTICAL) {
const verticalProvider = (<IVerticalSashLayoutProvider>this.layoutProvider);
this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px';
if (verticalProvider.getVerticalSashTop) {
this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
}
if (verticalProvider.getVerticalSashHeight) {
this.el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px';
}
} else {
const horizontalProvider = (<IHorizontalSashLayoutProvider>this.layoutProvider);
this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px';
if (horizontalProvider.getHorizontalSashLeft) {
this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px';
}
if (horizontalProvider.getHorizontalSashWidth) {
this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px';
}
}
}
private getOrthogonalSash(e: PointerEvent): Sash | undefined {
const target = e.initialTarget ?? e.target;
if (!target || !(isHTMLElement(target))) {
return undefined;
}
if (target.classList.contains('orthogonal-drag-handle')) {
return target.classList.contains('start') ? this.orthogonalStartSash : this.orthogonalEndSash;
}
return undefined;
}
override dispose(): void {
super.dispose();
this.el.remove();
}
}