sprotty
Version:
A next-gen framework for graphical views
233 lines (216 loc) • 10.6 kB
text/typescript
/********************************************************************************
* Copyright (c) 2017-2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { inject } from 'inversify';
import { Viewport } from 'sprotty-protocol/lib/model';
import { Action, CenterAction, SetViewportAction } from 'sprotty-protocol/lib/actions';
import { almostEquals, Point } from 'sprotty-protocol/lib/utils/geometry';
import { SModelElementImpl, SModelRootImpl } from '../../base/model/smodel';
import { MouseListener } from '../../base/views/mouse-tool';
import { findParentByFeature } from '../../base/model/smodel-utils';
import { isViewport } from './model';
import { isMoveable } from '../move/model';
import { SRoutingHandleImpl } from '../routing/model';
import { getModelBounds } from '../projection/model';
import { hitsMouseEvent } from '../../utils/browser';
import { TYPES } from '../../base/types';
import { ViewerOptions } from '../../base/views/viewer-options';
export class ScrollMouseListener extends MouseListener {
protected viewerOptions: ViewerOptions;
protected lastScrollPosition: Point |undefined;
protected scrollbar: HTMLElement | undefined;
protected scrollbarMouseDownTimeout: number | undefined;
protected scrollbarMouseDownDelay = 200;
override mouseDown(target: SModelElementImpl, event: MouseEvent): (Action | Promise<Action>)[] {
const moveable = findParentByFeature(target, isMoveable);
if (moveable === undefined && !(target instanceof SRoutingHandleImpl)) {
const viewport = findParentByFeature(target, isViewport);
if (viewport) {
this.lastScrollPosition = { x: event.pageX, y: event.pageY };
this.scrollbar = this.getScrollbar(event);
if (this.scrollbar) {
window.clearTimeout(this.scrollbarMouseDownTimeout);
return this.moveScrollBar(viewport, event, this.scrollbar, true)
.map(action => new Promise(resolve => {
this.scrollbarMouseDownTimeout = window.setTimeout(() => resolve(action), this.scrollbarMouseDownDelay);
}));
}
} else {
this.lastScrollPosition = undefined;
this.scrollbar = undefined;
}
}
return [];
}
override mouseMove(target: SModelElementImpl, event: MouseEvent): Action[] {
if (event.buttons === 0) {
return this.mouseUp(target, event);
}
if (this.scrollbar) {
window.clearTimeout(this.scrollbarMouseDownTimeout);
const viewport = findParentByFeature(target, isViewport);
if (viewport) {
return this.moveScrollBar(viewport, event, this.scrollbar);
}
}
if (this.lastScrollPosition) {
const viewport = findParentByFeature(target, isViewport);
if (viewport) {
return this.dragCanvas(viewport, event, this.lastScrollPosition);
}
}
return [];
}
override mouseEnter(target: SModelElementImpl, event: MouseEvent): Action[] {
if (target instanceof SModelRootImpl && event.buttons === 0) {
this.mouseUp(target, event);
}
return [];
}
override mouseUp(target: SModelElementImpl, event: MouseEvent): Action[] {
this.lastScrollPosition = undefined;
this.scrollbar = undefined;
return [];
}
override doubleClick(target: SModelElementImpl, event: MouseEvent): Action[] {
const viewport = findParentByFeature(target, isViewport);
if (viewport) {
const scrollbar = this.getScrollbar(event);
if (scrollbar) {
window.clearTimeout(this.scrollbarMouseDownTimeout);
const targetElement = this.findClickTarget(scrollbar, event);
let elementId: string | undefined;
if (targetElement && targetElement.id.startsWith('horizontal-projection:')) {
elementId = targetElement.id.substring('horizontal-projection:'.length);
} else if (targetElement && targetElement.id.startsWith('vertical-projection:')) {
elementId = targetElement.id.substring('vertical-projection:'.length);
}
if (elementId) {
return [CenterAction.create([elementId], { animate: true, retainZoom: true })];
}
}
}
return [];
}
protected dragCanvas(model: SModelRootImpl & Viewport, event: MouseEvent, lastScrollPosition: Point): Action[] {
let dx = (event.pageX - lastScrollPosition.x) / model.zoom;
if (dx > 0 && almostEquals(model.scroll.x, this.viewerOptions.horizontalScrollLimits.min)
|| dx < 0 && almostEquals(model.scroll.x, this.viewerOptions.horizontalScrollLimits.max - model.canvasBounds.width / model.zoom)) {
dx = 0;
}
let dy = (event.pageY - lastScrollPosition.y) / model.zoom;
if (dy > 0 && almostEquals(model.scroll.y, this.viewerOptions.verticalScrollLimits.min)
|| dy < 0 && almostEquals(model.scroll.y, this.viewerOptions.verticalScrollLimits.max - model.canvasBounds.height / model.zoom)) {
dy = 0;
}
if (dx === 0 && dy === 0) {
return [];
}
const newViewport: Viewport = {
scroll: {
x: model.scroll.x - dx,
y: model.scroll.y - dy,
},
zoom: model.zoom
};
this.lastScrollPosition = { x: event.pageX, y: event.pageY };
return [SetViewportAction.create(model.id, newViewport, { animate: false })];
}
protected moveScrollBar(model: SModelRootImpl & Viewport, event: MouseEvent, scrollbar: HTMLElement, animate: boolean = false): Action[] {
const modelBounds = getModelBounds(model);
if (!modelBounds || model.zoom <= 0) {
return [];
}
const scrollbarRect = scrollbar.getBoundingClientRect();
let newScroll: { x: number, y: number };
if (this.getScrollbarOrientation(scrollbar) === 'horizontal') {
if (scrollbarRect.width <= 0) {
return [];
}
const viewportSize = (model.canvasBounds.width / (model.zoom * modelBounds.width)) * scrollbarRect.width;
let position = event.clientX - scrollbarRect.x - viewportSize / 2;
if (position < 0) {
position = 0;
} else if (position > scrollbarRect.width - viewportSize) {
position = scrollbarRect.width - viewportSize;
}
newScroll = {
x: modelBounds.x + (position / scrollbarRect.width) * modelBounds.width,
y: model.scroll.y
};
if (newScroll.x < this.viewerOptions.horizontalScrollLimits.min) {
newScroll.x = this.viewerOptions.horizontalScrollLimits.min;
} else if (newScroll.x > this.viewerOptions.horizontalScrollLimits.max - model.canvasBounds.width / model.zoom) {
newScroll.x = this.viewerOptions.horizontalScrollLimits.max - model.canvasBounds.width / model.zoom;
}
if (almostEquals(newScroll.x, model.scroll.x)) {
return [];
}
} else {
if (scrollbarRect.height <= 0) {
return [];
}
const viewportSize = (model.canvasBounds.height / (model.zoom * modelBounds.height)) * scrollbarRect.height;
let position = event.clientY - scrollbarRect.y - viewportSize / 2;
if (position < 0) {
position = 0;
} else if (position > scrollbarRect.height - viewportSize) {
position = scrollbarRect.height - viewportSize;
}
newScroll = {
x: model.scroll.x,
y: modelBounds.y + (position / scrollbarRect.height) * modelBounds.height
};
if (newScroll.y < this.viewerOptions.verticalScrollLimits.min) {
newScroll.y = this.viewerOptions.verticalScrollLimits.min;
} else if (newScroll.y > this.viewerOptions.verticalScrollLimits.max - model.canvasBounds.height / model.zoom) {
newScroll.y = this.viewerOptions.verticalScrollLimits.max - model.canvasBounds.height / model.zoom;
}
if (almostEquals(newScroll.y, model.scroll.y)) {
return [];
}
}
return [SetViewportAction.create(model.id, { scroll: newScroll, zoom: model.zoom }, { animate })];
}
protected getScrollbar(event: MouseEvent): HTMLElement | undefined {
return findViewportScrollbar(event);
}
protected getScrollbarOrientation(scrollbar: HTMLElement): 'horizontal' | 'vertical' {
if (scrollbar.classList.contains('horizontal')) {
return 'horizontal';
} else {
return 'vertical';
}
}
protected findClickTarget(scrollbar: HTMLElement, event: MouseEvent): HTMLElement | undefined {
const matching = Array.from(scrollbar.children).filter(child =>
child.id && child.classList.contains('sprotty-projection') && hitsMouseEvent(child, event)
) as HTMLElement[];
if (matching.length > 0) {
return matching[matching.length - 1];
}
return undefined;
}
}
export function findViewportScrollbar(event: MouseEvent): HTMLElement | undefined {
let element = event.target as HTMLElement | null;
while (element) {
if (element.classList && element.classList.contains('sprotty-projection-bar')) {
return element;
}
element = element.parentElement;
}
return undefined;
}