js-draw 0.1.7 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +15 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +1 -0
- package/dist/src/Editor.js +32 -15
- package/dist/src/UndoRedoHistory.js +3 -0
- package/dist/src/bundle/bundled.d.ts +2 -1
- package/dist/src/bundle/bundled.js +2 -1
- package/dist/src/components/builders/RectangleBuilder.d.ts +3 -1
- package/dist/src/components/builders/RectangleBuilder.js +17 -8
- package/dist/src/geometry/LineSegment2.d.ts +1 -0
- package/dist/src/geometry/LineSegment2.js +16 -0
- package/dist/src/geometry/Rect2.d.ts +1 -0
- package/dist/src/geometry/Rect2.js +16 -0
- package/dist/src/localizations/en.d.ts +3 -0
- package/dist/src/localizations/en.js +4 -0
- package/dist/src/localizations/es.d.ts +3 -0
- package/dist/src/localizations/es.js +18 -0
- package/dist/src/localizations/getLocalizationTable.d.ts +3 -0
- package/dist/src/localizations/getLocalizationTable.js +43 -0
- package/dist/src/rendering/caching/RenderingCacheNode.js +5 -1
- package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
- package/dist/src/toolbar/HTMLToolbar.js +10 -8
- package/dist/src/toolbar/icons.js +13 -13
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/toolbar/makeColorInput.js +22 -8
- package/dist/src/toolbar/widgets/BaseWidget.js +29 -0
- package/dist/src/tools/BaseTool.d.ts +2 -1
- package/dist/src/tools/BaseTool.js +3 -0
- package/dist/src/tools/PanZoom.d.ts +2 -1
- package/dist/src/tools/PanZoom.js +4 -0
- package/dist/src/tools/SelectionTool.d.ts +9 -2
- package/dist/src/tools/SelectionTool.js +131 -19
- package/dist/src/tools/ToolController.js +6 -2
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +8 -2
- package/dist/src/types.js +1 -0
- package/package.json +9 -1
- package/src/Editor.ts +36 -14
- package/src/EditorImage.test.ts +1 -1
- package/src/UndoRedoHistory.ts +4 -0
- package/src/bundle/bundled.ts +2 -1
- package/src/components/builders/RectangleBuilder.ts +23 -8
- package/src/geometry/LineSegment2.test.ts +15 -0
- package/src/geometry/LineSegment2.ts +20 -0
- package/src/geometry/Rect2.test.ts +20 -7
- package/src/geometry/Rect2.ts +19 -1
- package/src/localizations/en.ts +8 -0
- package/src/localizations/es.ts +60 -0
- package/src/localizations/getLocalizationTable.test.ts +27 -0
- package/src/localizations/getLocalizationTable.ts +53 -0
- package/src/rendering/caching/RenderingCacheNode.ts +6 -1
- package/src/toolbar/HTMLToolbar.ts +11 -9
- package/src/toolbar/icons.ts +13 -13
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/makeColorInput.ts +25 -10
- package/src/toolbar/toolbar.css +5 -0
- package/src/toolbar/widgets/BaseWidget.ts +34 -0
- package/src/tools/BaseTool.ts +5 -1
- package/src/tools/PanZoom.ts +6 -0
- package/src/tools/SelectionTool.test.ts +24 -1
- package/src/tools/SelectionTool.ts +158 -23
- package/src/tools/ToolController.ts +6 -2
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +9 -1
- package/dist-test/test-dist-bundle.html +0 -42
@@ -147,14 +147,27 @@ describe('Rect2', () => {
|
|
147
147
|
it('division of empty square', () => {
|
148
148
|
expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
|
149
149
|
});
|
150
|
+
|
151
|
+
it('division of rectangle', () => {
|
152
|
+
expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
|
153
|
+
[
|
154
|
+
new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
|
155
|
+
new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
|
156
|
+
]
|
157
|
+
);
|
158
|
+
});
|
150
159
|
});
|
151
160
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
161
|
+
describe('should correctly return the closest point on the edge of a rectangle', () => {
|
162
|
+
it('with the unit square', () => {
|
163
|
+
const rect = Rect2.unitSquare;
|
164
|
+
expect(rect.getClosestPointOnBoundaryTo(Vec2.zero)).objEq(Vec2.zero);
|
165
|
+
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, -1))).objEq(Vec2.zero);
|
166
|
+
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, 0.5))).objEq(Vec2.of(0, 0.5));
|
167
|
+
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(1, 0.5))).objEq(Vec2.of(1, 0.5));
|
168
|
+
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6));
|
169
|
+
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(2, 0.5))).objEq(Vec2.of(1, 0.5));
|
170
|
+
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6));
|
171
|
+
});
|
159
172
|
});
|
160
173
|
});
|
package/src/geometry/Rect2.ts
CHANGED
@@ -51,6 +51,7 @@ export default class Rect2 {
|
|
51
51
|
return new Rect2(vec.x + this.x, vec.y + this.y, this.w, this.h);
|
52
52
|
}
|
53
53
|
|
54
|
+
// Returns a copy of this with the given size (but same top-left).
|
54
55
|
public resizedTo(size: Vec2): Rect2 {
|
55
56
|
return new Rect2(this.x, this.y, size.x, size.y);
|
56
57
|
}
|
@@ -163,6 +164,23 @@ export default class Rect2 {
|
|
163
164
|
);
|
164
165
|
}
|
165
166
|
|
167
|
+
public getClosestPointOnBoundaryTo(target: Point2) {
|
168
|
+
const closestEdgePoints = this.getEdges().map(edge => {
|
169
|
+
return edge.closestPointTo(target);
|
170
|
+
});
|
171
|
+
|
172
|
+
let closest: Point2|null = null;
|
173
|
+
let closestDist: number|null = null;
|
174
|
+
for (const point of closestEdgePoints) {
|
175
|
+
const dist = point.minus(target).length();
|
176
|
+
if (closestDist === null || dist < closestDist) {
|
177
|
+
closest = point;
|
178
|
+
closestDist = dist;
|
179
|
+
}
|
180
|
+
}
|
181
|
+
return closest!;
|
182
|
+
}
|
183
|
+
|
166
184
|
public get corners(): Point2[] {
|
167
185
|
return [
|
168
186
|
this.bottomRight,
|
@@ -172,7 +190,7 @@ export default class Rect2 {
|
|
172
190
|
];
|
173
191
|
}
|
174
192
|
|
175
|
-
public get maxDimension()
|
193
|
+
public get maxDimension() {
|
176
194
|
return Math.max(this.w, this.h);
|
177
195
|
}
|
178
196
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import { defaultEditorLocalization, EditorLocalization } from '../localization';
|
2
|
+
|
3
|
+
// A partial Spanish localization.
|
4
|
+
const localization: EditorLocalization = {
|
5
|
+
...defaultEditorLocalization,
|
6
|
+
|
7
|
+
// Strings for the main editor interface
|
8
|
+
// (see src/localization.ts)
|
9
|
+
loading: (percentage: number) => `Cargando: ${percentage}%...`,
|
10
|
+
imageEditor: 'Editor de dibujos',
|
11
|
+
|
12
|
+
undoAnnouncement: (commandDescription: string) => `${commandDescription} fue deshecho`,
|
13
|
+
redoAnnouncement: (commandDescription: string) => `${commandDescription} fue rehecho`,
|
14
|
+
undo: 'Deshace',
|
15
|
+
redo: 'Rehace',
|
16
|
+
|
17
|
+
// Strings for the toolbar
|
18
|
+
// (see src/toolbar/localization.ts)
|
19
|
+
pen: 'Lapiz',
|
20
|
+
eraser: 'Borrador',
|
21
|
+
select: 'Selecciona',
|
22
|
+
thicknessLabel: 'Tamaño: ',
|
23
|
+
colorLabel: 'Color: ',
|
24
|
+
doneLoading: 'El cargado terminó',
|
25
|
+
fontLabel: 'Fuente: ',
|
26
|
+
anyDevicePanning: 'Mover la pantalla con todo dispotivo',
|
27
|
+
touchPanning: 'Mover la pantalla con un dedo',
|
28
|
+
touchPanTool: 'Instrumento de mover la pantalla con un dedo',
|
29
|
+
outlinedRectanglePen: 'Rectángulo con nada más que un borde',
|
30
|
+
filledRectanglePen: 'Rectángulo sin borde',
|
31
|
+
linePen: 'Línea',
|
32
|
+
arrowPen: 'Flecha',
|
33
|
+
freehandPen: 'Dibuja sin restricción de forma',
|
34
|
+
selectObjectType: 'Forma de dibuja:',
|
35
|
+
handTool: 'Mover',
|
36
|
+
resizeImageToSelection: 'Redimensionar la imagen a lo que está seleccionado',
|
37
|
+
deleteSelection: 'Borra la selección',
|
38
|
+
duplicateSelection: 'Duplica la selección',
|
39
|
+
pickColorFronScreen: 'Selecciona un color de la pantalla',
|
40
|
+
dropdownShown(toolName: string): string {
|
41
|
+
return `Menú por ${toolName} es visible`;
|
42
|
+
},
|
43
|
+
dropdownHidden: function (toolName: string): string {
|
44
|
+
return `Menú por ${toolName} fue ocultado`;
|
45
|
+
},
|
46
|
+
colorChangedAnnouncement: function (color: string): string {
|
47
|
+
return `Color fue cambiado a ${color}`;
|
48
|
+
},
|
49
|
+
keyboardPanZoom: 'Mover la pantalla con el teclado',
|
50
|
+
penTool: function (penId: number): string {
|
51
|
+
return `Lapiz ${penId}`;
|
52
|
+
},
|
53
|
+
selectionTool: 'Selecciona',
|
54
|
+
eraserTool: 'Borrador',
|
55
|
+
textTool: 'Texto',
|
56
|
+
enterTextToInsert: 'Entra texto',
|
57
|
+
rerenderAsText: 'Redibuja la pantalla al texto',
|
58
|
+
};
|
59
|
+
|
60
|
+
export default localization;
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
import { defaultEditorLocalization } from '../localization';
|
3
|
+
import en from './en';
|
4
|
+
import es from './es';
|
5
|
+
import getLocalizationTable from './getLocalizationTable';
|
6
|
+
|
7
|
+
describe('getLocalizationTable', () => {
|
8
|
+
it('should return the en localization for es_TEST', () => {
|
9
|
+
expect(getLocalizationTable([ 'es_TEST' ]) === es).toBe(true);
|
10
|
+
});
|
11
|
+
|
12
|
+
it('should return the default localization for unsupported language', () => {
|
13
|
+
expect(getLocalizationTable([ 'test' ]) === defaultEditorLocalization).toBe(true);
|
14
|
+
});
|
15
|
+
|
16
|
+
it('should return the first localization matching a language in the list of user locales', () => {
|
17
|
+
expect(getLocalizationTable([ 'test_TEST1', 'test_TEST2', 'test_TEST3', 'en_TEST', 'notalanguage']) === en).toBe(true);
|
18
|
+
});
|
19
|
+
|
20
|
+
it('should return the default localization for unsupported language', () => {
|
21
|
+
expect(getLocalizationTable([ 'test' ]) === defaultEditorLocalization).toBe(true);
|
22
|
+
});
|
23
|
+
|
24
|
+
it('should return first of user\'s supported languages', () => {
|
25
|
+
expect(getLocalizationTable([ 'es_MX', 'es_ES', 'en_US' ]) === es).toBe(true);
|
26
|
+
});
|
27
|
+
});
|
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
import { defaultEditorLocalization, EditorLocalization } from '../localization';
|
3
|
+
import en from './en';
|
4
|
+
import es from './es';
|
5
|
+
|
6
|
+
const allLocales: Record<string, EditorLocalization> = {
|
7
|
+
en,
|
8
|
+
es,
|
9
|
+
};
|
10
|
+
|
11
|
+
// [locale]: A string in the format languageCode_Region or just languageCode. For example, en_US.
|
12
|
+
const languageFromLocale = (locale: string) => {
|
13
|
+
const matches = /^(\w+)[_-](\w+)$/.exec(locale);
|
14
|
+
if (!matches) {
|
15
|
+
// If not in languageCode_region format, the locale should be the
|
16
|
+
// languageCode. Return that.
|
17
|
+
return locale;
|
18
|
+
}
|
19
|
+
|
20
|
+
return matches[1];
|
21
|
+
};
|
22
|
+
|
23
|
+
const getLocalizationTable = (userLocales?: readonly string[]): EditorLocalization => {
|
24
|
+
userLocales ??= navigator.languages;
|
25
|
+
|
26
|
+
let prevLanguage: string|undefined;
|
27
|
+
for (const locale of userLocales) {
|
28
|
+
const language = languageFromLocale(locale);
|
29
|
+
|
30
|
+
// If the specific localization of the language is not available, but
|
31
|
+
// a localization for the language is,
|
32
|
+
if (prevLanguage && language !== prevLanguage) {
|
33
|
+
if (prevLanguage in allLocales) {
|
34
|
+
return allLocales[prevLanguage];
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
// If the full locale (e.g. en_US) is available,
|
39
|
+
if (locale in allLocales) {
|
40
|
+
return allLocales[locale];
|
41
|
+
}
|
42
|
+
|
43
|
+
prevLanguage = language;
|
44
|
+
}
|
45
|
+
|
46
|
+
if (prevLanguage && prevLanguage in allLocales) {
|
47
|
+
return allLocales[prevLanguage];
|
48
|
+
} else {
|
49
|
+
return defaultEditorLocalization;
|
50
|
+
}
|
51
|
+
};
|
52
|
+
|
53
|
+
export default getLocalizationTable;
|
@@ -65,6 +65,11 @@ export default class RenderingCacheNode {
|
|
65
65
|
if (this.instantiatedChildren.length === 0) {
|
66
66
|
const childRects = this.region.divideIntoGrid(cacheDivisionSize, cacheDivisionSize);
|
67
67
|
|
68
|
+
if (this.region.size.x === 0 || this.region.size.y === 0) {
|
69
|
+
console.warn('Cache element has zero size! Not generating children.');
|
70
|
+
return;
|
71
|
+
}
|
72
|
+
|
68
73
|
for (const rect of childRects) {
|
69
74
|
const child = new RenderingCacheNode(rect, this.cacheState);
|
70
75
|
child.parent = this;
|
@@ -357,7 +362,7 @@ export default class RenderingCacheNode {
|
|
357
362
|
|
358
363
|
private checkRep() {
|
359
364
|
if (this.instantiatedChildren.length !== cacheDivisionSize * cacheDivisionSize && this.instantiatedChildren.length !== 0) {
|
360
|
-
throw new Error(
|
365
|
+
throw new Error(`Repcheck: Wrong number of children. Got ${this.instantiatedChildren.length}`);
|
361
366
|
}
|
362
367
|
|
363
368
|
if (this.renderedIds[1] !== undefined && this.renderedIds[0] >= this.renderedIds[1]) {
|
@@ -25,6 +25,8 @@ export const toolbarCSSPrefix = 'toolbar-';
|
|
25
25
|
export default class HTMLToolbar {
|
26
26
|
private container: HTMLElement;
|
27
27
|
|
28
|
+
private static colorisStarted: boolean = false;
|
29
|
+
|
28
30
|
public constructor(
|
29
31
|
private editor: Editor, parent: HTMLElement,
|
30
32
|
private localizationTable: ToolbarLocalization = defaultToolbarLocalization,
|
@@ -34,7 +36,10 @@ export default class HTMLToolbar {
|
|
34
36
|
this.container.setAttribute('role', 'toolbar');
|
35
37
|
parent.appendChild(this.container);
|
36
38
|
|
37
|
-
|
39
|
+
if (!HTMLToolbar.colorisStarted) {
|
40
|
+
colorisInit();
|
41
|
+
HTMLToolbar.colorisStarted = true;
|
42
|
+
}
|
38
43
|
this.setupColorPickers();
|
39
44
|
}
|
40
45
|
|
@@ -133,13 +138,13 @@ export default class HTMLToolbar {
|
|
133
138
|
undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
|
134
139
|
|
135
140
|
const undoButton = this.addActionButton({
|
136
|
-
label:
|
141
|
+
label: this.localizationTable.undo,
|
137
142
|
icon: makeUndoIcon()
|
138
143
|
}, () => {
|
139
144
|
this.editor.history.undo();
|
140
145
|
}, undoRedoGroup);
|
141
146
|
const redoButton = this.addActionButton({
|
142
|
-
label:
|
147
|
+
label: this.localizationTable.redo,
|
143
148
|
icon: makeRedoIcon(),
|
144
149
|
}, () => {
|
145
150
|
this.editor.history.redo();
|
@@ -195,12 +200,9 @@ export default class HTMLToolbar {
|
|
195
200
|
(new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
196
201
|
}
|
197
202
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
}
|
202
|
-
|
203
|
-
(new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
203
|
+
const panZoomTool = toolController.getMatchingTools(ToolType.PanZoom)[0];
|
204
|
+
if (panZoomTool && panZoomTool instanceof PanZoom) {
|
205
|
+
(new HandToolWidget(this.editor, panZoomTool, this.localizationTable)).addTo(this.container);
|
204
206
|
}
|
205
207
|
|
206
208
|
this.setupColorPickers();
|
package/src/toolbar/icons.ts
CHANGED
@@ -9,11 +9,11 @@ import { StrokeDataPoint } from '../types';
|
|
9
9
|
import Viewport from '../Viewport';
|
10
10
|
|
11
11
|
const svgNamespace = 'http://www.w3.org/2000/svg';
|
12
|
-
const
|
13
|
-
style='fill: var(--
|
12
|
+
const iconColorFill = `
|
13
|
+
style='fill: var(--icon-color);'
|
14
14
|
`;
|
15
|
-
const
|
16
|
-
style='fill: var(--
|
15
|
+
const iconColorStrokeFill = `
|
16
|
+
style='fill: var(--icon-color); stroke: var(--icon-color);'
|
17
17
|
`;
|
18
18
|
const checkerboardPatternDef = `
|
19
19
|
<pattern
|
@@ -39,7 +39,7 @@ export const makeRedoIcon = (mirror: boolean = false) => {
|
|
39
39
|
icon.innerHTML = `
|
40
40
|
<style>
|
41
41
|
.toolbar-svg-undo-redo-icon {
|
42
|
-
stroke: var(--
|
42
|
+
stroke: var(--icon-color);
|
43
43
|
stroke-width: 12;
|
44
44
|
stroke-linejoin: round;
|
45
45
|
stroke-linecap: round;
|
@@ -63,7 +63,7 @@ export const makeDropdownIcon = () => {
|
|
63
63
|
<g>
|
64
64
|
<path
|
65
65
|
d='M5,10 L50,90 L95,10 Z'
|
66
|
-
${
|
66
|
+
${iconColorFill}
|
67
67
|
/>
|
68
68
|
</g>
|
69
69
|
`;
|
@@ -80,7 +80,7 @@ export const makeEraserIcon = () => {
|
|
80
80
|
<rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
|
81
81
|
<rect
|
82
82
|
x=10 y=10 width=80 height=50
|
83
|
-
${
|
83
|
+
${iconColorFill}
|
84
84
|
/>
|
85
85
|
</g>
|
86
86
|
`;
|
@@ -132,7 +132,7 @@ export const makeHandToolIcon = () => {
|
|
132
132
|
|
133
133
|
fill='none'
|
134
134
|
style='
|
135
|
-
stroke: var(--
|
135
|
+
stroke: var(--icon-color);
|
136
136
|
stroke-width: 2;
|
137
137
|
'
|
138
138
|
/>
|
@@ -175,7 +175,7 @@ export const makeTouchPanningIcon = () => {
|
|
175
175
|
'
|
176
176
|
fill='none'
|
177
177
|
style='
|
178
|
-
stroke: var(--
|
178
|
+
stroke: var(--icon-color);
|
179
179
|
stroke-width: 2;
|
180
180
|
'
|
181
181
|
/>
|
@@ -241,7 +241,7 @@ export const makeAllDevicePanningIcon = () => {
|
|
241
241
|
'
|
242
242
|
fill='none'
|
243
243
|
style='
|
244
|
-
stroke: var(--
|
244
|
+
stroke: var(--icon-color);
|
245
245
|
stroke-width: 2;
|
246
246
|
'
|
247
247
|
/>
|
@@ -263,7 +263,7 @@ export const makeZoomIcon = () => {
|
|
263
263
|
textNode.style.textAlign = 'center';
|
264
264
|
textNode.style.textAnchor = 'middle';
|
265
265
|
textNode.style.fontSize = '55px';
|
266
|
-
textNode.style.fill = 'var(--
|
266
|
+
textNode.style.fill = 'var(--icon-color)';
|
267
267
|
textNode.style.fontFamily = 'monospace';
|
268
268
|
|
269
269
|
icon.appendChild(textNode);
|
@@ -315,7 +315,7 @@ export const makePenIcon = (tipThickness: number, color: string) => {
|
|
315
315
|
<!-- Pen grip -->
|
316
316
|
<path
|
317
317
|
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
|
318
|
-
${
|
318
|
+
${iconColorStrokeFill}
|
319
319
|
/>
|
320
320
|
</g>
|
321
321
|
<g>
|
@@ -389,7 +389,7 @@ export const makePipetteIcon = (color?: Color4) => {
|
|
389
389
|
65,15 65,5 47,6
|
390
390
|
Z
|
391
391
|
`);
|
392
|
-
pipette.style.fill = 'var(--
|
392
|
+
pipette.style.fill = 'var(--icon-color)';
|
393
393
|
|
394
394
|
if (color) {
|
395
395
|
const defs = document.createElementNS(svgNamespace, 'defs');
|
@@ -23,6 +23,7 @@ export interface ToolbarLocalization {
|
|
23
23
|
undo: string;
|
24
24
|
redo: string;
|
25
25
|
zoom: string;
|
26
|
+
selectionToolKeyboardShortcuts: string;
|
26
27
|
|
27
28
|
dropdownShown: (toolName: string)=> string;
|
28
29
|
dropdownHidden: (toolName: string)=> string;
|
@@ -46,6 +47,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
|
|
46
47
|
redo: 'Redo',
|
47
48
|
selectObjectType: 'Object type: ',
|
48
49
|
pickColorFronScreen: 'Pick color from screen',
|
50
|
+
selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
|
49
51
|
|
50
52
|
touchPanning: 'Touchscreen panning',
|
51
53
|
anyDevicePanning: 'Any device panning',
|
@@ -20,7 +20,7 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
|
|
20
20
|
colorInputContainer.appendChild(colorInput);
|
21
21
|
addPipetteTool(editor, colorInputContainer, (color: Color4) => {
|
22
22
|
colorInput.value = color.toHexString();
|
23
|
-
|
23
|
+
onInputEnd();
|
24
24
|
|
25
25
|
// Update the color preview, if it exists (may be managed by Coloris).
|
26
26
|
const parentElem = colorInput.parentElement;
|
@@ -32,15 +32,20 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
|
|
32
32
|
let currentColor: Color4|undefined;
|
33
33
|
const handleColorInput = () => {
|
34
34
|
currentColor = Color4.fromHex(colorInput.value);
|
35
|
-
|
36
|
-
|
37
|
-
);
|
38
|
-
onColorChange(currentColor);
|
35
|
+
};
|
36
|
+
const onInputEnd = () => {
|
37
|
+
handleColorInput();
|
39
38
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
if (currentColor) {
|
40
|
+
editor.announceForAccessibility(
|
41
|
+
editor.localization.colorChangedAnnouncement(currentColor.toHexString())
|
42
|
+
);
|
43
|
+
onColorChange(currentColor);
|
44
|
+
editor.notifier.dispatch(EditorEventType.ColorPickerColorSelected, {
|
45
|
+
kind: EditorEventType.ColorPickerColorSelected,
|
46
|
+
color: currentColor,
|
47
|
+
});
|
48
|
+
}
|
44
49
|
};
|
45
50
|
|
46
51
|
colorInput.oninput = handleColorInput;
|
@@ -55,6 +60,7 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
|
|
55
60
|
kind: EditorEventType.ColorPickerToggled,
|
56
61
|
open: false,
|
57
62
|
});
|
63
|
+
onInputEnd();
|
58
64
|
});
|
59
65
|
|
60
66
|
return [ colorInput, colorInputContainer ];
|
@@ -72,10 +78,13 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
|
|
72
78
|
updatePipetteIcon();
|
73
79
|
|
74
80
|
const pipetteTool: PipetteTool|undefined = editor.toolController.getMatchingTools(ToolType.Pipette)[0] as PipetteTool|undefined;
|
75
|
-
const
|
81
|
+
const endColorSelectMode = () => {
|
76
82
|
pipetteTool?.clearColorListener();
|
77
83
|
updatePipetteIcon();
|
78
84
|
pipetteButton.classList.remove('active');
|
85
|
+
};
|
86
|
+
const pipetteColorSelect = (color: Color4|null) => {
|
87
|
+
endColorSelectMode();
|
79
88
|
|
80
89
|
if (color) {
|
81
90
|
onColorChange(color);
|
@@ -90,6 +99,12 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
|
|
90
99
|
};
|
91
100
|
|
92
101
|
pipetteButton.onclick = () => {
|
102
|
+
// If already picking, cancel it.
|
103
|
+
if (pipetteButton.classList.contains('active')) {
|
104
|
+
endColorSelectMode();
|
105
|
+
return;
|
106
|
+
}
|
107
|
+
|
93
108
|
pipetteTool?.setColorListener(
|
94
109
|
pipetteColorPreview,
|
95
110
|
pipetteColorSelect,
|
package/src/toolbar/toolbar.css
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
.toolbar-root {
|
2
2
|
background-color: var(--primary-background-color);
|
3
|
+
--icon-color: var(--primary-foreground-color);
|
4
|
+
|
3
5
|
|
4
6
|
border: 1px solid var(--secondary-background-color);
|
5
7
|
border-radius: 2px;
|
@@ -38,6 +40,7 @@
|
|
38
40
|
text-align: center;
|
39
41
|
border-radius: 6px;
|
40
42
|
|
43
|
+
--icon-color: var(--primary-foreground-color);
|
41
44
|
background-color: var(--primary-background-color);
|
42
45
|
color: var(--primary-foreground-color);
|
43
46
|
border: none;
|
@@ -83,6 +86,7 @@
|
|
83
86
|
.toolbar-toolContainer.selected .toolbar-button {
|
84
87
|
background-color: var(--secondary-background-color);
|
85
88
|
color: var(--secondary-foreground-color);
|
89
|
+
--icon-color: var(--secondary-foreground-color);
|
86
90
|
}
|
87
91
|
|
88
92
|
.toolbar-toolContainer:not(.selected):not(.dropdownShowable) > .toolbar-button > .toolbar-showHideDropdownIcon {
|
@@ -173,4 +177,5 @@
|
|
173
177
|
|
174
178
|
.color-input-container .pipetteButton.active {
|
175
179
|
background-color: var(--secondary-background-color);
|
180
|
+
--icon-color: var(--secondary-foreground-color);
|
176
181
|
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
|
+
import { InputEvtType } from '../../types';
|
2
3
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
3
4
|
import { makeDropdownIcon } from '../icons';
|
4
5
|
import { ToolbarLocalization } from '../localization';
|
@@ -50,6 +51,39 @@ export default abstract class BaseWidget {
|
|
50
51
|
}
|
51
52
|
|
52
53
|
protected setupActionBtnClickListener(button: HTMLElement) {
|
54
|
+
const clickTriggers = { enter: true, ' ': true, };
|
55
|
+
button.onkeydown = (evt) => {
|
56
|
+
let handled = false;
|
57
|
+
|
58
|
+
if (evt.key in clickTriggers) {
|
59
|
+
if (!this.disabled) {
|
60
|
+
this.handleClick();
|
61
|
+
handled = true;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
// If we didn't do anything with the event, send it to the editor.
|
66
|
+
if (!handled) {
|
67
|
+
this.editor.toolController.dispatchInputEvent({
|
68
|
+
kind: InputEvtType.KeyPressEvent,
|
69
|
+
key: evt.key,
|
70
|
+
ctrlKey: evt.ctrlKey,
|
71
|
+
});
|
72
|
+
}
|
73
|
+
};
|
74
|
+
|
75
|
+
button.onkeyup = evt => {
|
76
|
+
if (evt.key in clickTriggers) {
|
77
|
+
return;
|
78
|
+
}
|
79
|
+
|
80
|
+
this.editor.toolController.dispatchInputEvent({
|
81
|
+
kind: InputEvtType.KeyUpEvent,
|
82
|
+
key: evt.key,
|
83
|
+
ctrlKey: evt.ctrlKey,
|
84
|
+
});
|
85
|
+
};
|
86
|
+
|
53
87
|
button.onclick = () => {
|
54
88
|
if (!this.disabled) {
|
55
89
|
this.handleClick();
|
package/src/tools/BaseTool.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent } from '../types';
|
1
|
+
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent } from '../types';
|
2
2
|
import { ToolType } from './ToolController';
|
3
3
|
import ToolEnabledGroup from './ToolEnabledGroup';
|
4
4
|
|
@@ -24,6 +24,10 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
24
24
|
return false;
|
25
25
|
}
|
26
26
|
|
27
|
+
public onKeyUp(_event: KeyUpEvent): boolean {
|
28
|
+
return false;
|
29
|
+
}
|
30
|
+
|
27
31
|
public setEnabled(enabled: boolean) {
|
28
32
|
this.enabled = enabled;
|
29
33
|
|
package/src/tools/PanZoom.ts
CHANGED
@@ -16,11 +16,13 @@ interface PinchData {
|
|
16
16
|
dist: number;
|
17
17
|
}
|
18
18
|
|
19
|
+
|
19
20
|
export enum PanZoomMode {
|
20
21
|
OneFingerTouchGestures = 0x1,
|
21
22
|
TwoFingerTouchGestures = 0x1 << 1,
|
22
23
|
RightClickDrags = 0x1 << 2,
|
23
24
|
SinglePointerGestures = 0x1 << 3,
|
25
|
+
Keyboard = 0x1 << 4,
|
24
26
|
}
|
25
27
|
|
26
28
|
export default class PanZoom extends BaseTool {
|
@@ -180,6 +182,10 @@ export default class PanZoom extends BaseTool {
|
|
180
182
|
}
|
181
183
|
|
182
184
|
public onKeyPress({ key }: KeyPressEvent): boolean {
|
185
|
+
if (!(this.mode & PanZoomMode.Keyboard)) {
|
186
|
+
return false;
|
187
|
+
}
|
188
|
+
|
183
189
|
let translation = Vec2.zero;
|
184
190
|
let scale = 1;
|
185
191
|
let rotation = 0;
|
@@ -66,7 +66,7 @@ describe('SelectionTool', () => {
|
|
66
66
|
|
67
67
|
// Drag the object
|
68
68
|
selection!.handleBackgroundDrag(Vec2.of(5, 5));
|
69
|
-
selection!.
|
69
|
+
selection!.finalizeTransform();
|
70
70
|
|
71
71
|
expect(testStroke.getBBox().topLeft).toMatchObject({
|
72
72
|
x: 5,
|
@@ -80,4 +80,27 @@ describe('SelectionTool', () => {
|
|
80
80
|
y: 0,
|
81
81
|
});
|
82
82
|
});
|
83
|
+
|
84
|
+
it('moving the selection with a keyboard should move the view to keep the selection in view', () => {
|
85
|
+
const { addTestStrokeCommand } = createSquareStroke();
|
86
|
+
const editor = createEditor();
|
87
|
+
editor.dispatch(addTestStrokeCommand);
|
88
|
+
|
89
|
+
// Select the stroke
|
90
|
+
const selectionTool = getSelectionTool(editor);
|
91
|
+
selectionTool.setEnabled(true);
|
92
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
93
|
+
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
|
94
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100));
|
95
|
+
|
96
|
+
const selection = selectionTool.getSelection();
|
97
|
+
if (selection === null) {
|
98
|
+
// Throw to allow TypeScript's non-null checker to understand that selection
|
99
|
+
// must be non-null after this.
|
100
|
+
throw new Error('Selection should be non-null.');
|
101
|
+
}
|
102
|
+
|
103
|
+
selection.handleBackgroundDrag(Vec2.of(0, -1000));
|
104
|
+
expect(editor.viewport.visibleRect.containsPoint(selection.region.center)).toBe(true);
|
105
|
+
});
|
83
106
|
});
|