Comparing version 0.5.0 to 0.6.0
@@ -0,1 +1,12 @@ | ||
# 0.6.0 | ||
* Selection tool: | ||
* Shift+click extends a selection | ||
* `ctrl+d` duplicates selected objects | ||
* `ctrl+r` resizes the image to the selected region | ||
* `ctrl+a` selects everything (when the selection tool is enabled) | ||
* Panning tool: Toggle all device panning by clicking on the hand button. | ||
* `HandToolWidget` now expects, but does not require, a primary hand tool to work properly. See `ToolController#addPrimaryTool`. | ||
* **Breaiking changes:** | ||
* Icons are no longer accessible through `import {makeFooIcon} from '...'`. Use `editor.icons.makeFooIcon` instead. | ||
# 0.5.0 | ||
@@ -2,0 +13,0 @@ * Increase contrast between selection box/background |
@@ -31,2 +31,3 @@ import SerializableCommand from '../commands/SerializableCommand'; | ||
transformBy(affineTransfm: Mat33): SerializableCommand; | ||
isSelectable(): boolean; | ||
private static transformElementCommandId; | ||
@@ -33,0 +34,0 @@ private static UnresolvedTransformElementCommand; |
@@ -51,2 +51,6 @@ var _a; | ||
} | ||
// @returns true iff this component can be selected (e.g. by the selection tool.) | ||
isSelectable() { | ||
return true; | ||
} | ||
// Returns a copy of this component. | ||
@@ -53,0 +57,0 @@ clone() { |
@@ -315,4 +315,4 @@ import { Bezier } from 'bezier-js'; | ||
if (!enteringVec) { | ||
let sampleIdx = Math.ceil(this.buffer.length / 3); | ||
if (sampleIdx === 0) { | ||
let sampleIdx = Math.ceil(this.buffer.length / 2); | ||
if (sampleIdx === 0 || sampleIdx >= this.buffer.length) { | ||
sampleIdx = this.buffer.length - 1; | ||
@@ -319,0 +319,0 @@ } |
@@ -15,2 +15,3 @@ import LineSegment2 from '../math/LineSegment2'; | ||
protected applyTransformation(_affineTransfm: Mat33): void; | ||
isSelectable(): boolean; | ||
protected createClone(): SVGGlobalAttributesObject; | ||
@@ -17,0 +18,0 @@ description(localization: ImageComponentLocalization): string; |
@@ -32,2 +32,5 @@ // | ||
} | ||
isSelectable() { | ||
return false; | ||
} | ||
createClone() { | ||
@@ -34,0 +37,0 @@ return new SVGGlobalAttributesObject(this.attrs); |
@@ -15,3 +15,2 @@ import LineSegment2 from '../math/LineSegment2'; | ||
} | ||
declare type GetTextDimensCallback = (text: string, style: TextStyle) => Rect2; | ||
export default class Text extends AbstractComponent { | ||
@@ -21,7 +20,7 @@ protected readonly textObjects: Array<string | Text>; | ||
private readonly style; | ||
private readonly getTextDimens; | ||
protected contentBBox: Rect2; | ||
constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle, getTextDimens?: GetTextDimensCallback); | ||
constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle); | ||
static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void; | ||
private static textMeasuringCtx; | ||
private static estimateTextDimens; | ||
private static getTextDimens; | ||
@@ -37,4 +36,3 @@ private computeBBoxOfPart; | ||
protected serializeToJSON(): Record<string, any>; | ||
static deserializeFromString(json: any, getTextDimens?: GetTextDimensCallback): Text; | ||
static deserializeFromString(json: any): Text; | ||
} | ||
export {}; |
@@ -8,6 +8,3 @@ import LineSegment2 from '../math/LineSegment2'; | ||
export default class Text extends AbstractComponent { | ||
constructor(textObjects, transform, style, | ||
// If not given, an HtmlCanvasElement is used to determine text boundaries. | ||
// @internal | ||
getTextDimens = Text.getTextDimens) { | ||
constructor(textObjects, transform, style) { | ||
super(componentTypeId); | ||
@@ -17,3 +14,2 @@ this.textObjects = textObjects; | ||
this.style = style; | ||
this.getTextDimens = getTextDimens; | ||
this.recomputeBBox(); | ||
@@ -33,5 +29,17 @@ } | ||
} | ||
// Roughly estimate the bounding box of `text`. Use if no CanvasRenderingContext2D is available. | ||
static estimateTextDimens(text, style) { | ||
const widthEst = text.length * style.size; | ||
const heightEst = style.size; | ||
// Text is drawn with (0, 0) as its baseline. As such, the majority of the text's height should | ||
// be above (0, 0). | ||
return new Rect2(0, -heightEst * 2 / 3, widthEst, heightEst); | ||
} | ||
// Returns the bounding box of `text`. This is approximate if no Canvas is available. | ||
static getTextDimens(text, style) { | ||
var _a; | ||
(_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = document.createElement('canvas').getContext('2d')); | ||
var _a, _b; | ||
(_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = (_b = document.createElement('canvas').getContext('2d')) !== null && _b !== void 0 ? _b : null); | ||
if (!Text.textMeasuringCtx) { | ||
return this.estimateTextDimens(text, style); | ||
} | ||
const ctx = Text.textMeasuringCtx; | ||
@@ -47,3 +55,3 @@ Text.applyTextStyles(ctx, style); | ||
if (typeof part === 'string') { | ||
const textBBox = this.getTextDimens(part, this.style); | ||
const textBBox = Text.getTextDimens(part, this.style); | ||
return textBBox.transformedBoundingBox(this.transform); | ||
@@ -145,3 +153,3 @@ } | ||
} | ||
static deserializeFromString(json, getTextDimens = Text.getTextDimens) { | ||
static deserializeFromString(json) { | ||
const style = { | ||
@@ -167,5 +175,6 @@ renderingStyle: styleFromJSON(json.style.renderingStyle), | ||
const transform = new Mat33(...transformData); | ||
return new Text(textObjects, transform, style, getTextDimens); | ||
return new Text(textObjects, transform, style); | ||
} | ||
} | ||
Text.textMeasuringCtx = null; | ||
AbstractComponent.registerComponent(componentTypeId, (data) => Text.deserializeFromString(data)); |
@@ -14,2 +14,3 @@ import LineSegment2 from '../math/LineSegment2'; | ||
protected applyTransformation(_affineTransfm: Mat33): void; | ||
isSelectable(): boolean; | ||
protected createClone(): AbstractComponent; | ||
@@ -16,0 +17,0 @@ description(localization: ImageComponentLocalization): string; |
@@ -28,2 +28,5 @@ // | ||
} | ||
isSelectable() { | ||
return false; | ||
} | ||
createClone() { | ||
@@ -30,0 +33,0 @@ return new UnknownSVGObject(this.svgObject.cloneNode(true)); |
@@ -31,2 +31,3 @@ /** | ||
import { EditorLocalization } from './localization'; | ||
import IconProvider from './toolbar/IconProvider'; | ||
declare type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel'; | ||
@@ -48,2 +49,3 @@ declare type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean; | ||
maxZoom: number; | ||
iconProvider: IconProvider; | ||
} | ||
@@ -86,9 +88,10 @@ export declare class Editor { | ||
*/ | ||
image: EditorImage; | ||
readonly image: EditorImage; | ||
/** Viewport for the exported/imported image. */ | ||
private importExportViewport; | ||
/** @internal */ | ||
localization: EditorLocalization; | ||
viewport: Viewport; | ||
toolController: ToolController; | ||
readonly localization: EditorLocalization; | ||
readonly icons: IconProvider; | ||
readonly viewport: Viewport; | ||
readonly toolController: ToolController; | ||
/** | ||
@@ -98,3 +101,3 @@ * Global event dispatcher/subscriber. | ||
*/ | ||
notifier: EditorNotifier; | ||
readonly notifier: EditorNotifier; | ||
private loadingWarning; | ||
@@ -101,0 +104,0 @@ private accessibilityAnnounceArea; |
@@ -44,2 +44,3 @@ /** | ||
import getLocalizationTable from './localizations/getLocalizationTable'; | ||
import IconProvider from './toolbar/IconProvider'; | ||
// { @inheritDoc Editor! } | ||
@@ -71,3 +72,3 @@ export class Editor { | ||
constructor(parent, settings = {}) { | ||
var _a, _b, _c, _d; | ||
var _a, _b, _c, _d, _e; | ||
this.eventListenerTargets = []; | ||
@@ -91,3 +92,5 @@ this.previousAccessibilityAnnouncement = ''; | ||
maxZoom: (_d = settings.maxZoom) !== null && _d !== void 0 ? _d : 1e12, | ||
iconProvider: (_e = settings.iconProvider) !== null && _e !== void 0 ? _e : new IconProvider(), | ||
}; | ||
this.icons = this.settings.iconProvider; | ||
this.container = document.createElement('div'); | ||
@@ -94,0 +97,0 @@ this.renderingRegion = document.createElement('div'); |
@@ -19,2 +19,5 @@ import AbstractRenderer from './rendering/renderers/AbstractRenderer'; | ||
renderAll(renderer: AbstractRenderer): void; | ||
/** @returns all elements in the image, sorted by z-index. This can be slow for large images. */ | ||
getAllElements(): AbstractComponent[]; | ||
/** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */ | ||
getElementsIntersectingRegion(region: Rect2): AbstractComponent[]; | ||
@@ -21,0 +24,0 @@ /** @internal */ |
@@ -42,2 +42,9 @@ var _a; | ||
} | ||
/** @returns all elements in the image, sorted by z-index. This can be slow for large images. */ | ||
getAllElements() { | ||
const leaves = this.root.getLeaves(); | ||
sortLeavesByZIndex(leaves); | ||
return leaves.map(leaf => leaf.getContent()); | ||
} | ||
/** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */ | ||
getElementsIntersectingRegion(region) { | ||
@@ -44,0 +51,0 @@ const leaves = this.root.getLeavesIntersectingRegion(region); |
@@ -102,3 +102,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
} | ||
if (supportedStyleAttrs) { | ||
if (supportedStyleAttrs && node.style) { | ||
for (const attr of node.style) { | ||
@@ -161,5 +161,5 @@ if (attr === '' || !attr) { | ||
const elemY = elem.getAttribute('y'); | ||
if (elemX && elemY) { | ||
const x = parseFloat(elemX); | ||
const y = parseFloat(elemY); | ||
if (elemX || elemY) { | ||
const x = parseFloat(elemX !== null && elemX !== void 0 ? elemX : '0'); | ||
const y = parseFloat(elemY !== null && elemY !== void 0 ? elemY : '0'); | ||
if (!isNaN(x) && !isNaN(y)) { | ||
@@ -209,3 +209,3 @@ supportedAttrs === null || supportedAttrs === void 0 ? void 0 : supportedAttrs.push('x', 'y'); | ||
renderingStyle: { | ||
fill: Color4.fromString(computedStyles.fill) | ||
fill: Color4.fromString(computedStyles.fill || elem.style.fill || '#000') | ||
}, | ||
@@ -345,3 +345,2 @@ }; | ||
} | ||
// TODO: Handling unsafe data! Tripple-check that this is secure! | ||
// @param sanitize - if `true`, don't store unknown attributes. | ||
@@ -348,0 +347,0 @@ static fromString(text, sanitize = false) { |
import loadExpectExtensions from './loadExpectExtensions'; | ||
loadExpectExtensions(); | ||
jest.useFakeTimers(); | ||
// jsdom doesn't support HTMLCanvasElement#getContext — it logs an error | ||
// to the console. Make it return null so we can handle a non-existent Canvas | ||
// at runtime (e.g. use something else, if available). | ||
HTMLCanvasElement.prototype.getContext = () => null; |
@@ -5,3 +5,2 @@ import { EditorEventType } from '../types'; | ||
import { defaultToolbarLocalization } from './localization'; | ||
import { makeRedoIcon, makeUndoIcon } from './icons'; | ||
import SelectionTool from '../tools/SelectionTool/SelectionTool'; | ||
@@ -127,3 +126,3 @@ import PanZoomTool from '../tools/PanZoom'; | ||
label: this.localizationTable.undo, | ||
icon: makeUndoIcon() | ||
icon: this.editor.icons.makeUndoIcon() | ||
}, () => { | ||
@@ -134,3 +133,3 @@ this.editor.history.undo(); | ||
label: this.localizationTable.redo, | ||
icon: makeRedoIcon(), | ||
icon: this.editor.icons.makeRedoIcon(), | ||
}, () => { | ||
@@ -137,0 +136,0 @@ this.editor.history.redo(); |
export * from './widgets/lib'; | ||
export * as icons from './icons'; | ||
export * from './makeColorInput'; | ||
export { default as IconProvider } from './IconProvider'; |
export * from './widgets/lib'; | ||
import * as icons_1 from './icons'; | ||
export { icons_1 as icons }; | ||
export * from './makeColorInput'; | ||
export { default as IconProvider } from './IconProvider'; |
export interface ToolbarLocalization { | ||
fontLabel: string; | ||
anyDevicePanning: string; | ||
touchPanning: string; | ||
@@ -5,0 +4,0 @@ outlinedRectanglePen: string; |
@@ -21,3 +21,2 @@ export const defaultToolbarLocalization = { | ||
touchPanning: 'Touchscreen panning', | ||
anyDevicePanning: 'Any device panning', | ||
freehandPen: 'Freehand', | ||
@@ -24,0 +23,0 @@ arrowPen: 'Arrow', |
import Color4 from '../Color4'; | ||
import PipetteTool from '../tools/PipetteTool'; | ||
import { EditorEventType } from '../types'; | ||
import { makePipetteIcon } from './icons'; | ||
// Returns [ color input, input container ]. | ||
@@ -59,3 +58,3 @@ export const makeColorInput = (editor, onColorChange) => { | ||
const updatePipetteIcon = (color) => { | ||
pipetteButton.replaceChildren(makePipetteIcon(color)); | ||
pipetteButton.replaceChildren(editor.icons.makePipetteIcon(color)); | ||
}; | ||
@@ -62,0 +61,0 @@ updatePipetteIcon(); |
@@ -16,3 +16,2 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
import { makeDropdownIcon } from '../icons'; | ||
export default class BaseWidget { | ||
@@ -210,3 +209,3 @@ constructor(editor, localizationTable) { | ||
createDropdownIcon() { | ||
const icon = makeDropdownIcon(); | ||
const icon = this.editor.icons.makeDropdownIcon(); | ||
icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`); | ||
@@ -213,0 +212,0 @@ return icon; |
@@ -1,2 +0,1 @@ | ||
import { makeEraserIcon } from '../icons'; | ||
import BaseToolWidget from './BaseToolWidget'; | ||
@@ -8,3 +7,3 @@ export default class EraserToolWidget extends BaseToolWidget { | ||
createIcon() { | ||
return makeEraserIcon(); | ||
return this.editor.icons.makeEraserIcon(); | ||
} | ||
@@ -11,0 +10,0 @@ fillDropdown(_dropdown) { |
@@ -6,9 +6,11 @@ import Editor from '../../Editor'; | ||
export default class HandToolWidget extends BaseToolWidget { | ||
protected tool: PanZoom; | ||
protected overridePanZoomTool: PanZoom; | ||
private touchPanningWidget; | ||
constructor(editor: Editor, tool: PanZoom, localizationTable: ToolbarLocalization); | ||
private allowTogglingBaseTool; | ||
constructor(editor: Editor, overridePanZoomTool: PanZoom, localizationTable: ToolbarLocalization); | ||
private static getPrimaryHandTool; | ||
protected getTitle(): string; | ||
protected createIcon(): Element; | ||
setSelected(_selected: boolean): void; | ||
protected handleClick(): void; | ||
setSelected(selected: boolean): void; | ||
} |
import Mat33 from '../../math/Mat33'; | ||
import { PanZoomMode } from '../../tools/PanZoom'; | ||
import PanZoom, { PanZoomMode } from '../../tools/PanZoom'; | ||
import { EditorEventType } from '../../types'; | ||
import Viewport from '../../Viewport'; | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
import { makeAllDevicePanningIcon, makeHandToolIcon, makeTouchPanningIcon, makeZoomIcon } from '../icons'; | ||
import BaseToolWidget from './BaseToolWidget'; | ||
@@ -69,3 +68,3 @@ import BaseWidget from './BaseWidget'; | ||
createIcon() { | ||
return makeZoomIcon(); | ||
return this.editor.icons.makeZoomIcon(); | ||
} | ||
@@ -121,11 +120,27 @@ handleClick() { | ||
export default class HandToolWidget extends BaseToolWidget { | ||
constructor(editor, tool, localizationTable) { | ||
constructor(editor, | ||
// Pan zoom tool that overrides all other tools (enabling this tool for a device | ||
// causes that device to pan/zoom instead of interact with the primary tools) | ||
overridePanZoomTool, localizationTable) { | ||
const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController); | ||
const tool = primaryHandTool !== null && primaryHandTool !== void 0 ? primaryHandTool : overridePanZoomTool; | ||
super(editor, tool, localizationTable); | ||
this.tool = tool; | ||
this.container.classList.add('dropdownShowable'); | ||
this.touchPanningWidget = new HandModeWidget(editor, localizationTable, tool, PanZoomMode.OneFingerTouchGestures, makeTouchPanningIcon, localizationTable.touchPanning); | ||
this.overridePanZoomTool = overridePanZoomTool; | ||
// 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 = primaryHandTool !== 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. | ||
this.touchPanningWidget = new HandModeWidget(editor, localizationTable, overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning); | ||
this.addSubWidget(this.touchPanningWidget); | ||
this.addSubWidget(new HandModeWidget(editor, localizationTable, tool, PanZoomMode.SinglePointerGestures, makeAllDevicePanningIcon, localizationTable.anyDevicePanning)); | ||
this.addSubWidget(new ZoomWidget(editor, localizationTable)); | ||
} | ||
static getPrimaryHandTool(toolController) { | ||
const primaryPanZoomToolList = toolController.getPrimaryTools().filter(tool => tool instanceof PanZoom); | ||
const primaryPanZoomTool = primaryPanZoomToolList[0]; | ||
return primaryPanZoomTool; | ||
} | ||
getTitle() { | ||
@@ -135,9 +150,17 @@ return this.localizationTable.handTool; | ||
createIcon() { | ||
return makeHandToolIcon(); | ||
return this.editor.icons.makeHandToolIcon(); | ||
} | ||
setSelected(_selected) { | ||
} | ||
handleClick() { | ||
this.setDropdownVisible(!this.isDropdownVisible()); | ||
if (this.allowTogglingBaseTool) { | ||
super.handleClick(); | ||
} | ||
else { | ||
this.setDropdownVisible(!this.isDropdownVisible()); | ||
} | ||
} | ||
setSelected(selected) { | ||
if (this.allowTogglingBaseTool) { | ||
super.setSelected(selected); | ||
} | ||
} | ||
} |
@@ -7,3 +7,2 @@ import { makeArrowBuilder } from '../../components/builders/ArrowBuilder'; | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
import { makeIconFromFactory, makePenIcon } from '../icons'; | ||
import makeColorInput from '../makeColorInput'; | ||
@@ -59,7 +58,7 @@ import BaseToolWidget from './BaseToolWidget'; | ||
const color = this.tool.getColor(); | ||
return makePenIcon(scale, color.toHexString()); | ||
return this.editor.icons.makePenIcon(scale, color.toHexString()); | ||
} | ||
else { | ||
const strokeFactory = this.tool.getStrokeFactory(); | ||
return makeIconFromFactory(this.tool, strokeFactory); | ||
return this.editor.icons.makeIconFromFactory(this.tool, strokeFactory); | ||
} | ||
@@ -66,0 +65,0 @@ } |
import Editor from '../../Editor'; | ||
import SelectionTool from '../../tools/SelectionTool/SelectionTool'; | ||
import { KeyPressEvent } from '../../types'; | ||
import { ToolbarLocalization } from '../localization'; | ||
@@ -8,4 +9,6 @@ import BaseToolWidget from './BaseToolWidget'; | ||
constructor(editor: Editor, tool: SelectionTool, localization: ToolbarLocalization); | ||
private resizeImageToSelection; | ||
protected onKeyPress(event: KeyPressEvent): boolean; | ||
protected getTitle(): string; | ||
protected createIcon(): Element; | ||
} |
import { EditorEventType } from '../../types'; | ||
import { makeDeleteSelectionIcon, makeDuplicateSelectionIcon, makeResizeViewportIcon, makeSelectionIcon } from '../icons'; | ||
import ActionButtonWidget from './ActionButtonWidget'; | ||
@@ -9,7 +8,6 @@ import BaseToolWidget from './BaseToolWidget'; | ||
this.tool = tool; | ||
const resizeButton = new ActionButtonWidget(editor, localization, makeResizeViewportIcon, this.localizationTable.resizeImageToSelection, () => { | ||
const selection = this.tool.getSelection(); | ||
this.editor.dispatch(this.editor.setImportExportRect(selection.region)); | ||
const resizeButton = new ActionButtonWidget(editor, localization, () => editor.icons.makeResizeViewportIcon(), this.localizationTable.resizeImageToSelection, () => { | ||
this.resizeImageToSelection(); | ||
}); | ||
const deleteButton = new ActionButtonWidget(editor, localization, makeDeleteSelectionIcon, this.localizationTable.deleteSelection, () => { | ||
const deleteButton = new ActionButtonWidget(editor, localization, () => editor.icons.makeDeleteSelectionIcon(), this.localizationTable.deleteSelection, () => { | ||
const selection = this.tool.getSelection(); | ||
@@ -19,3 +17,3 @@ this.editor.dispatch(selection.deleteSelectedObjects()); | ||
}); | ||
const duplicateButton = new ActionButtonWidget(editor, localization, makeDuplicateSelectionIcon, this.localizationTable.duplicateSelection, () => { | ||
const duplicateButton = new ActionButtonWidget(editor, localization, () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, () => { | ||
const selection = this.tool.getSelection(); | ||
@@ -45,2 +43,17 @@ this.editor.dispatch(selection.duplicateSelectedObjects()); | ||
} | ||
resizeImageToSelection() { | ||
const selection = this.tool.getSelection(); | ||
if (selection) { | ||
this.editor.dispatch(this.editor.setImportExportRect(selection.region)); | ||
} | ||
} | ||
onKeyPress(event) { | ||
// Resize image to selection: | ||
// Other keys are handled directly by the selection tool. | ||
if (event.ctrlKey && event.key === 'r') { | ||
this.resizeImageToSelection(); | ||
return true; | ||
} | ||
return false; | ||
} | ||
getTitle() { | ||
@@ -50,4 +63,4 @@ return this.localizationTable.select; | ||
createIcon() { | ||
return makeSelectionIcon(); | ||
return this.editor.icons.makeSelectionIcon(); | ||
} | ||
} |
import { EditorEventType } from '../../types'; | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
import { makeTextIcon } from '../icons'; | ||
import makeColorInput from '../makeColorInput'; | ||
@@ -24,3 +23,3 @@ import BaseToolWidget from './BaseToolWidget'; | ||
const textStyle = this.tool.getTextStyle(); | ||
return makeTextIcon(textStyle); | ||
return this.editor.icons.makeTextIcon(textStyle); | ||
} | ||
@@ -27,0 +26,0 @@ fillDropdown(dropdown) { |
@@ -15,2 +15,3 @@ export interface ToolLocalization { | ||
pasteHandler: string; | ||
anyDevicePanning: string; | ||
copied: (count: number, description: string) => string; | ||
@@ -17,0 +18,0 @@ pasted: (count: number, description: string) => string; |
@@ -15,2 +15,3 @@ export const defaultToolLocalization = { | ||
pasteHandler: 'Copy paste handler', | ||
anyDevicePanning: 'Any device panning', | ||
copied: (count, description) => `Copied ${count} ${description}`, | ||
@@ -17,0 +18,0 @@ pasted: (count, description) => `Pasted ${count} ${description}`, |
@@ -38,3 +38,3 @@ import { Editor } from '../Editor'; | ||
onWheel({ delta, screenPos }: WheelEvt): boolean; | ||
onKeyPress({ key }: KeyPressEvent): boolean; | ||
onKeyPress({ key, ctrlKey, altKey }: KeyPressEvent): boolean; | ||
setMode(mode: PanZoomMode): void; | ||
@@ -41,0 +41,0 @@ getMode(): PanZoomMode; |
@@ -137,6 +137,9 @@ import Mat33 from '../math/Mat33'; | ||
} | ||
onKeyPress({ key }) { | ||
onKeyPress({ key, ctrlKey, altKey }) { | ||
if (!(this.mode & PanZoomMode.Keyboard)) { | ||
return false; | ||
} | ||
if (ctrlKey || altKey) { | ||
return false; | ||
} | ||
// No need to keep the same the transform for keyboard events. | ||
@@ -143,0 +146,0 @@ this.transform = Viewport.transformBy(Mat33.identity); |
@@ -13,2 +13,4 @@ import AbstractComponent from '../../components/AbstractComponent'; | ||
private lastEvtTarget; | ||
private expandingSelectionBox; | ||
private shiftKeyPressed; | ||
constructor(editor: Editor, description: string); | ||
@@ -30,4 +32,5 @@ private makeSelectionBox; | ||
getSelection(): Selection | null; | ||
getSelectedObjects(): AbstractComponent[]; | ||
setSelection(objects: AbstractComponent[]): void; | ||
clearSelection(): void; | ||
} |
@@ -19,2 +19,4 @@ // Allows users to select/transform portions of the `EditorImage`. | ||
this.lastEvtTarget = null; | ||
this.expandingSelectionBox = false; | ||
this.shiftKeyPressed = false; | ||
this.selectionBoxHandlingEvt = false; | ||
@@ -38,6 +40,9 @@ this.handleOverlay = document.createElement('div'); | ||
makeSelectionBox(selectionStartPos) { | ||
var _a; | ||
this.prevSelectionBox = this.selectionBox; | ||
this.selectionBox = new Selection(selectionStartPos, this.editor); | ||
// Remove any previous selection rects | ||
this.handleOverlay.replaceChildren(); | ||
if (!this.expandingSelectionBox) { | ||
// Remove any previous selection rects | ||
(_a = this.prevSelectionBox) === null || _a === void 0 ? void 0 : _a.cancelSelection(); | ||
} | ||
this.selectionBox.addTo(this.handleOverlay); | ||
@@ -50,4 +55,7 @@ } | ||
this.selectionBoxHandlingEvt = true; | ||
this.expandingSelectionBox = false; | ||
} | ||
else { | ||
// Shift key: Combine the new and old selection boxes at the end of the gesture. | ||
this.expandingSelectionBox = this.shiftKeyPressed; | ||
this.makeSelectionBox(event.current.canvasPos); | ||
@@ -87,2 +95,3 @@ } | ||
} | ||
// Called after a gestureCancel and a pointerUp | ||
onGestureEnd() { | ||
@@ -112,3 +121,15 @@ this.lastEvtTarget = null; | ||
this.selectionBox.setToPoint(event.current.canvasPos); | ||
this.onGestureEnd(); | ||
// Were we expanding the previous selection? | ||
if (this.expandingSelectionBox && this.prevSelectionBox) { | ||
// If so, finish expanding. | ||
this.expandingSelectionBox = false; | ||
this.selectionBox.resolveToObjects(); | ||
this.setSelection([ | ||
...this.selectionBox.getSelectedObjects(), | ||
...this.prevSelectionBox.getSelectedObjects(), | ||
]); | ||
} | ||
else { | ||
this.onGestureEnd(); | ||
} | ||
} | ||
@@ -126,4 +147,24 @@ onGestureCancel() { | ||
} | ||
this.expandingSelectionBox = false; | ||
} | ||
onKeyPress(event) { | ||
if (this.selectionBox && event.ctrlKey && event.key === 'd') { | ||
// Handle duplication on key up — we don't want to accidentally duplicate | ||
// many times. | ||
return true; | ||
} | ||
else if (event.key === 'a' && event.ctrlKey) { | ||
// Handle ctrl+A on key up. | ||
// Return early to prevent 'a' from moving the selection/view. | ||
return true; | ||
} | ||
else if (event.ctrlKey) { | ||
// Don't transform the selection with, for example, ctrl+i. | ||
// Pass it to another tool, if apliccable. | ||
return false; | ||
} | ||
else if (event.key === 'Shift') { | ||
this.shiftKeyPressed = true; | ||
return true; | ||
} | ||
let rotationSteps = 0; | ||
@@ -203,2 +244,16 @@ let xTranslateSteps = 0; | ||
onKeyUp(evt) { | ||
if (evt.key === 'Shift') { | ||
this.shiftKeyPressed = false; | ||
return true; | ||
} | ||
else if (evt.ctrlKey) { | ||
if (this.selectionBox && evt.key === 'd') { | ||
this.editor.dispatch(this.selectionBox.duplicateSelectedObjects()); | ||
return true; | ||
} | ||
else if (evt.key === 'a') { | ||
this.setSelection(this.editor.image.getAllElements()); | ||
return true; | ||
} | ||
} | ||
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) { | ||
@@ -254,6 +309,14 @@ this.selectionBox.finalizeTransform(); | ||
// Get the object responsible for displaying this' selection. | ||
// @internal | ||
getSelection() { | ||
return this.selectionBox; | ||
} | ||
getSelectedObjects() { | ||
var _a, _b; | ||
return (_b = (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.getSelectedObjects()) !== null && _b !== void 0 ? _b : []; | ||
} | ||
// Select the given `objects`. Any non-selectable objects in `objects` are ignored. | ||
setSelection(objects) { | ||
// Only select selectable objects. | ||
objects = objects.filter(obj => obj.isSelectable()); | ||
let bbox = null; | ||
@@ -260,0 +323,0 @@ for (const object of objects) { |
@@ -32,2 +32,3 @@ import { InputEvtType, EditorEventType } from '../types'; | ||
new TextTool(editor, localization.textTool, localization), | ||
new PanZoom(editor, PanZoomMode.SinglePointerGestures, localization.anyDevicePanning) | ||
]; | ||
@@ -34,0 +35,0 @@ this.tools = [ |
{ | ||
"name": "js-draw", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ", | ||
@@ -5,0 +5,0 @@ "main": "./dist/src/lib.d.ts", |
@@ -92,2 +92,7 @@ import SerializableCommand from '../commands/SerializableCommand'; | ||
// @returns true iff this component can be selected (e.g. by the selection tool.) | ||
public isSelectable(): boolean { | ||
return true; | ||
} | ||
private static transformElementCommandId = 'transform-element'; | ||
@@ -94,0 +99,0 @@ |
@@ -418,4 +418,4 @@ import { Bezier } from 'bezier-js'; | ||
if (!enteringVec) { | ||
let sampleIdx = Math.ceil(this.buffer.length / 3); | ||
if (sampleIdx === 0) { | ||
let sampleIdx = Math.ceil(this.buffer.length / 2); | ||
if (sampleIdx === 0 || sampleIdx >= this.buffer.length) { | ||
sampleIdx = this.buffer.length - 1; | ||
@@ -422,0 +422,0 @@ } |
@@ -46,2 +46,6 @@ // | ||
public isSelectable() { | ||
return false; | ||
} | ||
protected createClone() { | ||
@@ -48,0 +52,0 @@ return new SVGGlobalAttributesObject(this.attrs); |
import Color4 from '../Color4'; | ||
import Mat33 from '../math/Mat33'; | ||
import Rect2 from '../math/Rect2'; | ||
import AbstractComponent from './AbstractComponent'; | ||
import Text, { TextStyle } from './Text'; | ||
const estimateTextBounds = (text: string, style: TextStyle): Rect2 => { | ||
const widthEst = text.length * style.size; | ||
const heightEst = style.size; | ||
// Text is drawn with (0, 0) as its baseline. As such, the majority of the text's height should | ||
// be above (0, 0). | ||
return new Rect2(0, -heightEst * 2/3, widthEst, heightEst); | ||
}; | ||
// Don't use the default Canvas-based text bounding code. The canvas-based code may not work | ||
// with jsdom. | ||
AbstractComponent.registerComponent('text', (data: string) => Text.deserializeFromString(data, estimateTextBounds)); | ||
describe('Text', () => { | ||
@@ -27,5 +14,3 @@ it('should be serializable', () => { | ||
}; | ||
const text = new Text( | ||
[ 'Foo' ], Mat33.identity, style, estimateTextBounds | ||
); | ||
const text = new Text([ 'Foo' ], Mat33.identity, style); | ||
const serialized = text.serialize(); | ||
@@ -32,0 +17,0 @@ const deserialized = AbstractComponent.deserialize(serialized) as Text; |
@@ -17,4 +17,2 @@ import LineSegment2 from '../math/LineSegment2'; | ||
type GetTextDimensCallback = (text: string, style: TextStyle) => Rect2; | ||
const componentTypeId = 'text'; | ||
@@ -28,6 +26,2 @@ export default class Text extends AbstractComponent { | ||
private readonly style: TextStyle, | ||
// If not given, an HtmlCanvasElement is used to determine text boundaries. | ||
// @internal | ||
private readonly getTextDimens: GetTextDimensCallback = Text.getTextDimens, | ||
) { | ||
@@ -52,5 +46,21 @@ super(componentTypeId); | ||
private static textMeasuringCtx: CanvasRenderingContext2D; | ||
private static textMeasuringCtx: CanvasRenderingContext2D|null = null; | ||
// Roughly estimate the bounding box of `text`. Use if no CanvasRenderingContext2D is available. | ||
private static estimateTextDimens(text: string, style: TextStyle): Rect2 { | ||
const widthEst = text.length * style.size; | ||
const heightEst = style.size; | ||
// Text is drawn with (0, 0) as its baseline. As such, the majority of the text's height should | ||
// be above (0, 0). | ||
return new Rect2(0, -heightEst * 2/3, widthEst, heightEst); | ||
} | ||
// Returns the bounding box of `text`. This is approximate if no Canvas is available. | ||
private static getTextDimens(text: string, style: TextStyle): Rect2 { | ||
Text.textMeasuringCtx ??= document.createElement('canvas').getContext('2d')!; | ||
Text.textMeasuringCtx ??= document.createElement('canvas').getContext('2d') ?? null; | ||
if (!Text.textMeasuringCtx) { | ||
return this.estimateTextDimens(text, style); | ||
} | ||
const ctx = Text.textMeasuringCtx; | ||
@@ -69,3 +79,3 @@ Text.applyTextStyles(ctx, style); | ||
if (typeof part === 'string') { | ||
const textBBox = this.getTextDimens(part, this.style); | ||
const textBBox = Text.getTextDimens(part, this.style); | ||
return textBBox.transformedBoundingBox(this.transform); | ||
@@ -185,3 +195,3 @@ } else { | ||
public static deserializeFromString(json: any, getTextDimens: GetTextDimensCallback = Text.getTextDimens): Text { | ||
public static deserializeFromString(json: any): Text { | ||
const style: TextStyle = { | ||
@@ -211,3 +221,3 @@ renderingStyle: styleFromJSON(json.style.renderingStyle), | ||
return new Text(textObjects, transform, style, getTextDimens); | ||
return new Text(textObjects, transform, style); | ||
} | ||
@@ -214,0 +224,0 @@ } |
@@ -40,2 +40,6 @@ // | ||
public isSelectable() { | ||
return false; | ||
} | ||
protected createClone(): AbstractComponent { | ||
@@ -42,0 +46,0 @@ return new UnknownSVGObject(this.svgObject.cloneNode(true) as SVGElement); |
@@ -40,2 +40,3 @@ /** | ||
import getLocalizationTable from './localizations/getLocalizationTable'; | ||
import IconProvider from './toolbar/IconProvider'; | ||
@@ -62,2 +63,4 @@ type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel'; | ||
maxZoom: number, | ||
iconProvider: IconProvider, | ||
} | ||
@@ -106,3 +109,3 @@ | ||
*/ | ||
public image: EditorImage; | ||
public readonly image: EditorImage; | ||
@@ -113,6 +116,7 @@ /** Viewport for the exported/imported image. */ | ||
/** @internal */ | ||
public localization: EditorLocalization; | ||
public readonly localization: EditorLocalization; | ||
public viewport: Viewport; | ||
public toolController: ToolController; | ||
public readonly icons: IconProvider; | ||
public readonly viewport: Viewport; | ||
public readonly toolController: ToolController; | ||
@@ -123,3 +127,3 @@ /** | ||
*/ | ||
public notifier: EditorNotifier; | ||
public readonly notifier: EditorNotifier; | ||
@@ -172,3 +176,5 @@ private loadingWarning: HTMLElement; | ||
maxZoom: settings.maxZoom ?? 1e12, | ||
iconProvider: settings.iconProvider ?? new IconProvider(), | ||
}; | ||
this.icons = this.settings.iconProvider; | ||
@@ -175,0 +181,0 @@ this.container = document.createElement('div'); |
@@ -57,2 +57,11 @@ import Editor from './Editor'; | ||
/** @returns all elements in the image, sorted by z-index. This can be slow for large images. */ | ||
public getAllElements() { | ||
const leaves = this.root.getLeaves(); | ||
sortLeavesByZIndex(leaves); | ||
return leaves.map(leaf => leaf.getContent()!); | ||
} | ||
/** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */ | ||
public getElementsIntersectingRegion(region: Rect2): AbstractComponent[] { | ||
@@ -59,0 +68,0 @@ const leaves = this.root.getLeavesIntersectingRegion(region); |
@@ -127,3 +127,3 @@ import Color4 from './Color4'; | ||
if (supportedStyleAttrs) { | ||
if (supportedStyleAttrs && node.style) { | ||
for (const attr of node.style) { | ||
@@ -202,5 +202,5 @@ if (attr === '' || !attr) { | ||
const elemY = elem.getAttribute('y'); | ||
if (elemX && elemY) { | ||
const x = parseFloat(elemX); | ||
const y = parseFloat(elemY); | ||
if (elemX || elemY) { | ||
const x = parseFloat(elemX ?? '0'); | ||
const y = parseFloat(elemY ?? '0'); | ||
if (!isNaN(x) && !isNaN(y)) { | ||
@@ -250,3 +250,3 @@ supportedAttrs?.push('x', 'y'); | ||
renderingStyle: { | ||
fill: Color4.fromString(computedStyles.fill) | ||
fill: Color4.fromString(computedStyles.fill || elem.style.fill || '#000') | ||
}, | ||
@@ -412,3 +412,2 @@ }; | ||
// TODO: Handling unsafe data! Tripple-check that this is secure! | ||
// @param sanitize - if `true`, don't store unknown attributes. | ||
@@ -415,0 +414,0 @@ public static fromString(text: string, sanitize: boolean = false): SVGLoader { |
import loadExpectExtensions from './loadExpectExtensions'; | ||
loadExpectExtensions(); | ||
jest.useFakeTimers(); | ||
jest.useFakeTimers(); | ||
// jsdom doesn't support HTMLCanvasElement#getContext — it logs an error | ||
// to the console. Make it return null so we can handle a non-existent Canvas | ||
// at runtime (e.g. use something else, if available). | ||
HTMLCanvasElement.prototype.getContext = () => null; |
@@ -8,3 +8,2 @@ import Editor from '../Editor'; | ||
import { ActionButtonIcon } from './types'; | ||
import { makeRedoIcon, makeUndoIcon } from './icons'; | ||
import SelectionTool from '../tools/SelectionTool/SelectionTool'; | ||
@@ -160,3 +159,3 @@ import PanZoomTool from '../tools/PanZoom'; | ||
label: this.localizationTable.undo, | ||
icon: makeUndoIcon() | ||
icon: this.editor.icons.makeUndoIcon() | ||
}, () => { | ||
@@ -167,3 +166,3 @@ this.editor.history.undo(); | ||
label: this.localizationTable.redo, | ||
icon: makeRedoIcon(), | ||
icon: this.editor.icons.makeRedoIcon(), | ||
}, () => { | ||
@@ -170,0 +169,0 @@ this.editor.history.redo(); |
export * from './widgets/lib'; | ||
export * as icons from './icons'; | ||
export * from './makeColorInput'; | ||
export { default as IconProvider } from './IconProvider'; |
@@ -5,3 +5,2 @@ | ||
fontLabel: string; | ||
anyDevicePanning: string; | ||
touchPanning: string; | ||
@@ -58,3 +57,2 @@ outlinedRectanglePen: string; | ||
touchPanning: 'Touchscreen panning', | ||
anyDevicePanning: 'Any device panning', | ||
@@ -61,0 +59,0 @@ freehandPen: 'Freehand', |
@@ -5,3 +5,2 @@ import Color4 from '../Color4'; | ||
import { EditorEventType } from '../types'; | ||
import { makePipetteIcon } from './icons'; | ||
@@ -76,3 +75,3 @@ type OnColorChangeListener = (color: Color4)=>void; | ||
const updatePipetteIcon = (color?: Color4) => { | ||
pipetteButton.replaceChildren(makePipetteIcon(color)); | ||
pipetteButton.replaceChildren(editor.icons.makePipetteIcon(color)); | ||
}; | ||
@@ -79,0 +78,0 @@ updatePipetteIcon(); |
@@ -5,3 +5,2 @@ import Editor from '../../Editor'; | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
import { makeDropdownIcon } from '../icons'; | ||
import { ToolbarLocalization } from '../localization'; | ||
@@ -252,3 +251,3 @@ | ||
private createDropdownIcon(): Element { | ||
const icon = makeDropdownIcon(); | ||
const icon = this.editor.icons.makeDropdownIcon(); | ||
icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`); | ||
@@ -255,0 +254,0 @@ return icon; |
@@ -1,2 +0,1 @@ | ||
import { makeEraserIcon } from '../icons'; | ||
import BaseToolWidget from './BaseToolWidget'; | ||
@@ -9,3 +8,3 @@ | ||
protected createIcon(): Element { | ||
return makeEraserIcon(); | ||
return this.editor.icons.makeEraserIcon(); | ||
} | ||
@@ -12,0 +11,0 @@ |
import Editor from '../../Editor'; | ||
import Mat33 from '../../math/Mat33'; | ||
import PanZoom, { PanZoomMode } from '../../tools/PanZoom'; | ||
import ToolController from '../../tools/ToolController'; | ||
import { EditorEventType } from '../../types'; | ||
import Viewport from '../../Viewport'; | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
import { makeAllDevicePanningIcon, makeHandToolIcon, makeTouchPanningIcon, makeZoomIcon } from '../icons'; | ||
import { ToolbarLocalization } from '../localization'; | ||
@@ -89,3 +89,3 @@ import BaseToolWidget from './BaseToolWidget'; | ||
protected createIcon(): Element { | ||
return makeZoomIcon(); | ||
return this.editor.icons.makeZoomIcon(); | ||
} | ||
@@ -153,13 +153,32 @@ | ||
private touchPanningWidget: HandModeWidget; | ||
private allowTogglingBaseTool: boolean; | ||
public constructor( | ||
editor: Editor, protected tool: PanZoom, localizationTable: ToolbarLocalization | ||
editor: Editor, | ||
// Pan zoom tool that overrides all other tools (enabling this tool for a device | ||
// causes that device to pan/zoom instead of interact with the primary tools) | ||
protected overridePanZoomTool: PanZoom, | ||
localizationTable: ToolbarLocalization, | ||
) { | ||
const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController); | ||
const tool = primaryHandTool ?? overridePanZoomTool; | ||
super(editor, tool, localizationTable); | ||
this.container.classList.add('dropdownShowable'); | ||
// 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 = primaryHandTool !== 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. | ||
this.touchPanningWidget = new HandModeWidget( | ||
editor, localizationTable, | ||
tool, PanZoomMode.OneFingerTouchGestures, | ||
makeTouchPanningIcon, | ||
overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, | ||
() => this.editor.icons.makeTouchPanningIcon(), | ||
@@ -171,12 +190,2 @@ localizationTable.touchPanning | ||
this.addSubWidget( | ||
new HandModeWidget( | ||
editor, localizationTable, | ||
tool, PanZoomMode.SinglePointerGestures, | ||
makeAllDevicePanningIcon, | ||
localizationTable.anyDevicePanning | ||
) | ||
); | ||
this.addSubWidget( | ||
new ZoomWidget(editor, localizationTable) | ||
@@ -186,2 +195,8 @@ ); | ||
private static getPrimaryHandTool(toolController: ToolController): PanZoom|null { | ||
const primaryPanZoomToolList = toolController.getPrimaryTools().filter(tool => tool instanceof PanZoom); | ||
const primaryPanZoomTool = primaryPanZoomToolList[0]; | ||
return primaryPanZoomTool as PanZoom|null; | ||
} | ||
protected getTitle(): string { | ||
@@ -192,11 +207,18 @@ return this.localizationTable.handTool; | ||
protected createIcon(): Element { | ||
return makeHandToolIcon(); | ||
return this.editor.icons.makeHandToolIcon(); | ||
} | ||
public setSelected(_selected: boolean): void { | ||
protected handleClick(): void { | ||
if (this.allowTogglingBaseTool) { | ||
super.handleClick(); | ||
} else { | ||
this.setDropdownVisible(!this.isDropdownVisible()); | ||
} | ||
} | ||
protected handleClick() { | ||
this.setDropdownVisible(!this.isDropdownVisible()); | ||
public setSelected(selected: boolean): void { | ||
if (this.allowTogglingBaseTool) { | ||
super.setSelected(selected); | ||
} | ||
} | ||
} |
@@ -10,3 +10,2 @@ import { makeArrowBuilder } from '../../components/builders/ArrowBuilder'; | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
import { makeIconFromFactory, makePenIcon } from '../icons'; | ||
import { ToolbarLocalization } from '../localization'; | ||
@@ -81,6 +80,6 @@ import makeColorInput from '../makeColorInput'; | ||
const color = this.tool.getColor(); | ||
return makePenIcon(scale, color.toHexString()); | ||
return this.editor.icons.makePenIcon(scale, color.toHexString()); | ||
} else { | ||
const strokeFactory = this.tool.getStrokeFactory(); | ||
return makeIconFromFactory(this.tool, strokeFactory); | ||
return this.editor.icons.makeIconFromFactory(this.tool, strokeFactory); | ||
} | ||
@@ -87,0 +86,0 @@ } |
import Editor from '../../Editor'; | ||
import SelectionTool from '../../tools/SelectionTool/SelectionTool'; | ||
import { EditorEventType } from '../../types'; | ||
import { makeDeleteSelectionIcon, makeDuplicateSelectionIcon, makeResizeViewportIcon, makeSelectionIcon } from '../icons'; | ||
import { EditorEventType, KeyPressEvent } from '../../types'; | ||
import { ToolbarLocalization } from '../localization'; | ||
@@ -17,7 +16,6 @@ import ActionButtonWidget from './ActionButtonWidget'; | ||
editor, localization, | ||
makeResizeViewportIcon, | ||
() => editor.icons.makeResizeViewportIcon(), | ||
this.localizationTable.resizeImageToSelection, | ||
() => { | ||
const selection = this.tool.getSelection(); | ||
this.editor.dispatch(this.editor.setImportExportRect(selection!.region)); | ||
this.resizeImageToSelection(); | ||
}, | ||
@@ -27,3 +25,3 @@ ); | ||
editor, localization, | ||
makeDeleteSelectionIcon, | ||
() => editor.icons.makeDeleteSelectionIcon(), | ||
this.localizationTable.deleteSelection, | ||
@@ -38,3 +36,3 @@ () => { | ||
editor, localization, | ||
makeDuplicateSelectionIcon, | ||
() => editor.icons.makeDuplicateSelectionIcon(), | ||
this.localizationTable.duplicateSelection, | ||
@@ -73,2 +71,20 @@ () => { | ||
private resizeImageToSelection() { | ||
const selection = this.tool.getSelection(); | ||
if (selection) { | ||
this.editor.dispatch(this.editor.setImportExportRect(selection.region)); | ||
} | ||
} | ||
protected onKeyPress(event: KeyPressEvent): boolean { | ||
// Resize image to selection: | ||
// Other keys are handled directly by the selection tool. | ||
if (event.ctrlKey && event.key === 'r') { | ||
this.resizeImageToSelection(); | ||
return true; | ||
} | ||
return false; | ||
} | ||
protected getTitle(): string { | ||
@@ -79,4 +95,4 @@ return this.localizationTable.select; | ||
protected createIcon(): Element { | ||
return makeSelectionIcon(); | ||
return this.editor.icons.makeSelectionIcon(); | ||
} | ||
} |
@@ -5,3 +5,2 @@ import Editor from '../../Editor'; | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
import { makeTextIcon } from '../icons'; | ||
import { ToolbarLocalization } from '../localization'; | ||
@@ -30,3 +29,3 @@ import makeColorInput from '../makeColorInput'; | ||
const textStyle = this.tool.getTextStyle(); | ||
return makeTextIcon(textStyle); | ||
return this.editor.icons.makeTextIcon(textStyle); | ||
} | ||
@@ -33,0 +32,0 @@ |
@@ -18,2 +18,4 @@ | ||
anyDevicePanning: string; | ||
copied: (count: number, description: string) => string; | ||
@@ -42,2 +44,4 @@ pasted: (count: number, description: string) => string; | ||
anyDevicePanning: 'Any device panning', | ||
copied: (count: number, description: string) => `Copied ${count} ${description}`, | ||
@@ -44,0 +48,0 @@ pasted: (count: number, description: string) => `Pasted ${count} ${description}`, |
@@ -185,6 +185,9 @@ | ||
public onKeyPress({ key }: KeyPressEvent): boolean { | ||
public onKeyPress({ key, ctrlKey, altKey }: KeyPressEvent): boolean { | ||
if (!(this.mode & PanZoomMode.Keyboard)) { | ||
return false; | ||
} | ||
if (ctrlKey || altKey) { | ||
return false; | ||
} | ||
@@ -191,0 +194,0 @@ // No need to keep the same the transform for keyboard events. |
@@ -103,2 +103,42 @@ import Color4 from '../../Color4'; | ||
}); | ||
it('shift+click should expand an existing selection', () => { | ||
const { addTestStrokeCommand: stroke1Command } = createSquareStroke(50); | ||
const { addTestStrokeCommand: stroke2Command } = createSquareStroke(500); | ||
const editor = createEditor(); | ||
editor.dispatch(stroke1Command); | ||
editor.dispatch(stroke2Command); | ||
// Select the first stroke | ||
const selectionTool = getSelectionTool(editor); | ||
selectionTool.setEnabled(true); | ||
// Select the smaller rectangle | ||
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(40, 40)); | ||
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100)); | ||
expect(selectionTool.getSelectedObjects()).toHaveLength(1); | ||
// Shift key down. | ||
editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, 'Shift'); | ||
// Select the larger stroke. | ||
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 200)); | ||
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(600, 600)); | ||
expect(selectionTool.getSelectedObjects()).toHaveLength(2); | ||
editor.sendKeyboardEvent(InputEvtType.KeyUpEvent, 'Shift'); | ||
// Select the larger stroke without shift pressed | ||
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 200)); | ||
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(600, 600)); | ||
expect(selectionTool.getSelectedObjects()).toHaveLength(1); | ||
// Select nothing | ||
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 200)); | ||
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(201, 201)); | ||
expect(selectionTool.getSelectedObjects()).toHaveLength(0); | ||
}); | ||
}); |
@@ -26,2 +26,5 @@ // Allows users to select/transform portions of the `EditorImage`. | ||
private expandingSelectionBox: boolean = false; | ||
private shiftKeyPressed: boolean = false; | ||
public constructor(private editor: Editor, description: string) { | ||
@@ -54,4 +57,7 @@ super(editor.notifier, description); | ||
); | ||
// Remove any previous selection rects | ||
this.handleOverlay.replaceChildren(); | ||
if (!this.expandingSelectionBox) { | ||
// Remove any previous selection rects | ||
this.prevSelectionBox?.cancelSelection(); | ||
} | ||
this.selectionBox.addTo(this.handleOverlay); | ||
@@ -65,3 +71,7 @@ } | ||
this.selectionBoxHandlingEvt = true; | ||
} else { | ||
this.expandingSelectionBox = false; | ||
} | ||
else { | ||
// Shift key: Combine the new and old selection boxes at the end of the gesture. | ||
this.expandingSelectionBox = this.shiftKeyPressed; | ||
this.makeSelectionBox(event.current.canvasPos); | ||
@@ -105,2 +115,3 @@ } | ||
// Called after a gestureCancel and a pointerUp | ||
private onGestureEnd() { | ||
@@ -134,3 +145,15 @@ this.lastEvtTarget = null; | ||
this.selectionBox.setToPoint(event.current.canvasPos); | ||
this.onGestureEnd(); | ||
// Were we expanding the previous selection? | ||
if (this.expandingSelectionBox && this.prevSelectionBox) { | ||
// If so, finish expanding. | ||
this.expandingSelectionBox = false; | ||
this.selectionBox.resolveToObjects(); | ||
this.setSelection([ | ||
...this.selectionBox.getSelectedObjects(), | ||
...this.prevSelectionBox.getSelectedObjects(), | ||
]); | ||
} else { | ||
this.onGestureEnd(); | ||
} | ||
} | ||
@@ -147,2 +170,4 @@ | ||
} | ||
this.expandingSelectionBox = false; | ||
} | ||
@@ -159,2 +184,22 @@ | ||
public onKeyPress(event: KeyPressEvent): boolean { | ||
if (this.selectionBox && event.ctrlKey && event.key === 'd') { | ||
// Handle duplication on key up — we don't want to accidentally duplicate | ||
// many times. | ||
return true; | ||
} | ||
else if (event.key === 'a' && event.ctrlKey) { | ||
// Handle ctrl+A on key up. | ||
// Return early to prevent 'a' from moving the selection/view. | ||
return true; | ||
} | ||
else if (event.ctrlKey) { | ||
// Don't transform the selection with, for example, ctrl+i. | ||
// Pass it to another tool, if apliccable. | ||
return false; | ||
} | ||
else if (event.key === 'Shift') { | ||
this.shiftKeyPressed = true; | ||
return true; | ||
} | ||
let rotationSteps = 0; | ||
@@ -255,2 +300,17 @@ let xTranslateSteps = 0; | ||
public onKeyUp(evt: KeyUpEvent) { | ||
if (evt.key === 'Shift') { | ||
this.shiftKeyPressed = false; | ||
return true; | ||
} | ||
else if (evt.ctrlKey) { | ||
if (this.selectionBox && evt.key === 'd') { | ||
this.editor.dispatch(this.selectionBox.duplicateSelectedObjects()); | ||
return true; | ||
} | ||
else if (evt.key === 'a') { | ||
this.setSelection(this.editor.image.getAllElements()); | ||
return true; | ||
} | ||
} | ||
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) { | ||
@@ -318,2 +378,3 @@ this.selectionBox.finalizeTransform(); | ||
// Get the object responsible for displaying this' selection. | ||
// @internal | ||
public getSelection(): Selection|null { | ||
@@ -323,3 +384,11 @@ return this.selectionBox; | ||
public getSelectedObjects(): AbstractComponent[] { | ||
return this.selectionBox?.getSelectedObjects() ?? []; | ||
} | ||
// Select the given `objects`. Any non-selectable objects in `objects` are ignored. | ||
public setSelection(objects: AbstractComponent[]) { | ||
// Only select selectable objects. | ||
objects = objects.filter(obj => obj.isSelectable()); | ||
let bbox: Rect2|null = null; | ||
@@ -326,0 +395,0 @@ for (const object of objects) { |
@@ -42,2 +42,3 @@ import { InputEvtType, InputEvt, EditorEventType } from '../types'; | ||
new TextTool(editor, localization.textTool, localization), | ||
new PanZoom(editor, PanZoomMode.SinglePointerGestures, localization.anyDevicePanning) | ||
]; | ||
@@ -44,0 +45,0 @@ this.tools = [ |
@@ -6,3 +6,7 @@ { | ||
"exclude": [ | ||
"**/*.test.ts" | ||
"**/*.test.ts", | ||
"node_modules/**", | ||
"dist/", | ||
"dist-test/", | ||
"src/testing/" | ||
], | ||
@@ -9,0 +13,0 @@ "excludePrivate": true, |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1245834
356
26398