UNPKG

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
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); } }