js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
200 lines (199 loc) • 8.99 kB
JavaScript
import { Mat33 } from '@js-draw/math';
import PanZoom, { PanZoomMode } from '../../tools/PanZoom.mjs';
import { EditorEventType } from '../../types.mjs';
import Viewport from '../../Viewport.mjs';
import { toolbarCSSPrefix } from '../constants.mjs';
import BaseToolWidget from './BaseToolWidget.mjs';
import BaseWidget from './BaseWidget.mjs';
import makeSeparator from './components/makeSeparator.mjs';
const makeZoomControl = (localizationTable, editor, helpDisplay) => {
const zoomLevelRow = document.createElement('div');
const increaseButton = document.createElement('button');
const decreaseButton = document.createElement('button');
const resetViewButton = document.createElement('button');
const zoomLevelDisplay = document.createElement('span');
increaseButton.innerText = '+';
decreaseButton.innerText = '-';
resetViewButton.innerText = localizationTable.resetView;
zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton, resetViewButton);
zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
zoomLevelDisplay.classList.add('zoomDisplay');
let lastZoom;
const updateZoomDisplay = () => {
let zoomLevel = editor.viewport.getScaleFactor() * 100;
if (zoomLevel > 0.1) {
zoomLevel = Math.round(zoomLevel * 10) / 10;
}
else {
zoomLevel = Math.round(zoomLevel * 1000) / 1000;
}
if (zoomLevel !== lastZoom) {
zoomLevelDisplay.textContent = localizationTable.zoomLevel(zoomLevel);
lastZoom = zoomLevel;
}
};
updateZoomDisplay();
editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
if (event.kind === EditorEventType.ViewportChanged) {
updateZoomDisplay();
// Can't reset if already reset.
resetViewButton.disabled = event.newTransform.eq(Mat33.identity);
}
});
const zoomBy = (factor) => {
const screenCenter = editor.viewport.visibleRect.center;
const transformUpdate = Mat33.scaling2D(factor, screenCenter);
editor.dispatch(Viewport.transformBy(transformUpdate), false);
};
increaseButton.onclick = () => {
zoomBy(5.0 / 4);
};
decreaseButton.onclick = () => {
zoomBy(4.0 / 5);
};
resetViewButton.onclick = () => {
const addToHistory = false;
editor.dispatch(Viewport.transformBy(editor.viewport.canvasToScreenTransform.inverse()), addToHistory);
};
helpDisplay?.registerTextHelpForElement(increaseButton, localizationTable.handDropdown__zoomInHelpText);
helpDisplay?.registerTextHelpForElement(decreaseButton, localizationTable.handDropdown__zoomOutHelpText);
helpDisplay?.registerTextHelpForElement(resetViewButton, localizationTable.handDropdown__resetViewHelpText);
helpDisplay?.registerTextHelpForElement(zoomLevelDisplay, localizationTable.handDropdown__zoomDisplayHelpText);
return zoomLevelRow;
};
class HandModeWidget extends BaseWidget {
constructor(editor, tool, flag, makeIcon, title, helpText, localizationTable) {
super(editor, `pan-mode-${flag}`, localizationTable);
this.tool = tool;
this.flag = flag;
this.makeIcon = makeIcon;
this.title = title;
this.helpText = helpText;
editor.notifier.on(EditorEventType.ToolUpdated, (toolEvt) => {
if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === tool) {
const allEnabled = !!(tool.getMode() & PanZoomMode.SinglePointerGestures);
this.setSelected(!!(tool.getMode() & flag) || allEnabled);
// Unless this widget toggles all single pointer gestures, toggling while
// single pointer gestures are enabled should have no effect
this.setDisabled(allEnabled && flag !== PanZoomMode.SinglePointerGestures);
}
});
this.setSelected(false);
}
shouldAutoDisableInReadOnlyEditor() {
return false;
}
setModeFlag(enabled) {
this.tool.setModeEnabled(this.flag, enabled);
}
handleClick() {
this.setModeFlag(!this.isSelected());
}
getTitle() {
return this.title;
}
createIcon() {
return this.makeIcon();
}
fillDropdown(_dropdown) {
return false;
}
getHelpText() {
return this.helpText;
}
}
/** This toolbar widget allows controlling the editor's {@link PanZoom} tool(s). */
export default class HandToolWidget extends BaseToolWidget {
constructor(editor,
// Can either be the primary pan/zoom tool (in the primary tools list) or
// the override pan/zoom tool.
// If the override pan/zoom tool, the primary will be gotten from the editor's
// tool controller.
// If the primary, the override will be gotten from the editor's tool controller.
tool, localizationTable) {
const isGivenToolPrimary = editor.toolController.getPrimaryTools().includes(tool);
const primaryTool = (isGivenToolPrimary ? tool : HandToolWidget.getPrimaryHandTool(editor.toolController)) ??
tool;
super(editor, primaryTool, 'hand-tool-widget', localizationTable);
this.overridePanZoomTool =
(isGivenToolPrimary ? HandToolWidget.getOverrideHandTool(editor.toolController) : tool) ??
tool;
// Only allow toggling a hand tool if we're using the primary hand tool and not the override
// hand tool for this button.
this.allowTogglingBaseTool = primaryTool !== null;
// Allow showing/hiding the dropdown, even if `overridePanZoomTool` isn't enabled.
if (!this.allowTogglingBaseTool) {
this.container.classList.add('dropdownShowable');
}
// Controls for the overriding hand tool.
const touchPanningWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable.handDropdown__touchPanningHelpText, localizationTable);
const rotationLockWidget = new HandModeWidget(editor, this.overridePanZoomTool, PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable.handDropdown__lockRotationHelpText, localizationTable);
this.addSubWidget(touchPanningWidget);
this.addSubWidget(rotationLockWidget);
}
static getPrimaryHandTool(toolController) {
const primaryPanZoomToolList = toolController
.getPrimaryTools()
.filter((tool) => tool instanceof PanZoom);
const primaryPanZoomTool = primaryPanZoomToolList[0];
return primaryPanZoomTool;
}
static getOverrideHandTool(toolController) {
const panZoomToolList = toolController.getMatchingTools(PanZoom);
const panZoomTool = panZoomToolList[0];
return panZoomTool;
}
shouldAutoDisableInReadOnlyEditor() {
return false;
}
getTitle() {
return this.localizationTable.handTool;
}
createIcon() {
return this.editor.icons.makeHandToolIcon();
}
handleClick() {
if (this.allowTogglingBaseTool) {
super.handleClick();
}
else {
this.setDropdownVisible(!this.isDropdownVisible());
}
}
getHelpText() {
return this.localizationTable.handDropdown__baseHelpText;
}
fillDropdown(dropdown, helpDisplay) {
super.fillDropdown(dropdown, helpDisplay);
// The container for all actions that come after the toolbar buttons.
const nonbuttonActionContainer = document.createElement('div');
nonbuttonActionContainer.classList.add(`${toolbarCSSPrefix}nonbutton-controls-main-list`);
makeSeparator().addTo(nonbuttonActionContainer);
const zoomControl = makeZoomControl(this.localizationTable, this.editor, helpDisplay);
nonbuttonActionContainer.appendChild(zoomControl);
dropdown.appendChild(nonbuttonActionContainer);
return true;
}
setSelected(selected) {
if (this.allowTogglingBaseTool) {
super.setSelected(selected);
}
}
serializeState() {
const toolMode = this.overridePanZoomTool.getMode();
return {
...super.serializeState(),
touchPanning: toolMode & PanZoomMode.OneFingerTouchGestures,
rotationLocked: toolMode & PanZoomMode.RotationLocked,
};
}
deserializeFrom(state) {
if (state.touchPanning !== undefined) {
this.overridePanZoomTool.setModeEnabled(PanZoomMode.OneFingerTouchGestures, !!state.touchPanning);
}
if (state.rotationLocked !== undefined) {
this.overridePanZoomTool.setModeEnabled(PanZoomMode.RotationLocked, !!state.rotationLocked);
}
super.deserializeFrom(state);
}
}