js-draw 1.17.0 → 1.19.1
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +70 -10
- package/dist/Editor.css +35 -3
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +38 -21
- package/dist/cjs/Editor.js +11 -2
- package/dist/cjs/{SVGLoader.d.ts → SVGLoader/index.d.ts} +1 -1
- package/dist/cjs/{SVGLoader.js → SVGLoader/index.js} +12 -29
- package/dist/cjs/SVGLoader/utils/determineFontSize.d.ts +3 -0
- package/dist/cjs/SVGLoader/utils/determineFontSize.js +27 -0
- package/dist/cjs/Viewport.d.ts +33 -1
- package/dist/cjs/components/AbstractComponent.d.ts +17 -5
- package/dist/cjs/components/AbstractComponent.js +15 -15
- package/dist/cjs/components/Stroke.d.ts +4 -1
- package/dist/cjs/components/Stroke.js +158 -2
- package/dist/cjs/components/TextComponent.js +3 -1
- package/dist/cjs/components/builders/PolylineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PolylineBuilder.js +9 -2
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
- package/dist/cjs/image/EditorImage.js +1 -1
- package/dist/cjs/localizations/de.js +1 -1
- package/dist/cjs/localizations/es.js +1 -1
- package/dist/cjs/rendering/caching/RenderingCacheNode.js +20 -15
- package/dist/cjs/testing/createEditor.d.ts +2 -2
- package/dist/cjs/testing/createEditor.js +2 -2
- package/dist/cjs/testing/findNodeWithText.d.ts +3 -0
- package/dist/cjs/testing/findNodeWithText.js +16 -0
- package/dist/cjs/testing/firstElementAncestorOfNode.d.ts +3 -0
- package/dist/cjs/testing/firstElementAncestorOfNode.js +13 -0
- package/dist/cjs/testing/sendKeyPressRelease.d.ts +3 -0
- package/dist/cjs/testing/sendKeyPressRelease.js +8 -0
- package/dist/cjs/testing/sendPenEvent.d.ts +2 -2
- package/dist/cjs/testing/sendPenEvent.js +26 -3
- package/dist/cjs/toolbar/IconProvider.d.ts +3 -1
- package/dist/cjs/toolbar/IconProvider.js +15 -3
- package/dist/cjs/toolbar/localization.d.ts +8 -1
- package/dist/cjs/toolbar/localization.js +9 -2
- package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -0
- package/dist/cjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/cjs/toolbar/widgets/EraserToolWidget.js +45 -5
- package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +22 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.js +58 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/fileToImages.d.ts +3 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/fileToImages.js +21 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/index.d.ts +37 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/index.js +281 -0
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +10 -3
- package/dist/cjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/TextToolWidget.js +5 -3
- package/dist/cjs/toolbar/widgets/TextToolWidget.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/components/makeFileInput.d.ts +12 -2
- package/dist/cjs/toolbar/widgets/components/makeFileInput.js +102 -45
- package/dist/cjs/toolbar/widgets/components/makeFileInput.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/components/makeSnappedList.d.ts +15 -0
- package/dist/cjs/toolbar/widgets/components/makeSnappedList.js +103 -0
- package/dist/cjs/toolbar/widgets/keybindings.js +1 -1
- package/dist/cjs/tools/Eraser.d.ts +31 -6
- package/dist/cjs/tools/Eraser.js +161 -21
- package/dist/cjs/tools/PasteHandler.js +0 -1
- package/dist/cjs/tools/SelectionTool/Selection.d.ts +2 -2
- package/dist/cjs/tools/SelectionTool/Selection.js +20 -20
- package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +8 -2
- package/dist/cjs/tools/SelectionTool/SelectionHandle.js +6 -0
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +1 -1
- package/dist/cjs/tools/SelectionTool/types.d.ts +19 -0
- package/dist/cjs/tools/TextTool.js +2 -1
- package/dist/cjs/tools/TextTool.test.d.ts +1 -0
- package/dist/cjs/tools/ToolController.d.ts +2 -0
- package/dist/cjs/tools/ToolController.js +10 -1
- package/dist/cjs/tools/lib.d.ts +1 -4
- package/dist/cjs/tools/lib.js +2 -4
- package/dist/cjs/util/ReactiveValue.d.ts +2 -0
- package/dist/cjs/util/ReactiveValue.js +11 -0
- package/dist/cjs/util/bytesToSizeString.d.ts +8 -0
- package/dist/cjs/util/bytesToSizeString.js +26 -0
- package/dist/cjs/util/bytesToSizeString.test.d.ts +1 -0
- package/dist/cjs/util/stopPropagationOfScrollingWheelEvents.js +10 -6
- package/dist/cjs/util/waitForAll.d.ts +2 -0
- package/dist/cjs/util/waitForAll.js +2 -0
- package/dist/cjs/util/waitForImageLoaded.js +3 -0
- package/dist/cjs/util/waitForTimeout.d.ts +1 -0
- package/dist/cjs/util/waitForTimeout.js +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +38 -21
- package/dist/mjs/Editor.mjs +11 -2
- package/dist/mjs/{SVGLoader.d.ts → SVGLoader/index.d.ts} +1 -1
- package/dist/mjs/{SVGLoader.mjs → SVGLoader/index.mjs} +12 -29
- package/dist/mjs/SVGLoader/index.test.d.ts +1 -0
- package/dist/mjs/SVGLoader/utils/determineFontSize.d.ts +3 -0
- package/dist/mjs/SVGLoader/utils/determineFontSize.mjs +25 -0
- package/dist/mjs/Viewport.d.ts +33 -1
- package/dist/mjs/components/AbstractComponent.d.ts +17 -5
- package/dist/mjs/components/AbstractComponent.mjs +15 -15
- package/dist/mjs/components/Stroke.d.ts +4 -1
- package/dist/mjs/components/Stroke.mjs +159 -3
- package/dist/mjs/components/TextComponent.mjs +3 -1
- package/dist/mjs/components/builders/PolylineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PolylineBuilder.mjs +10 -3
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
- package/dist/mjs/image/EditorImage.mjs +1 -1
- package/dist/mjs/localizations/de.mjs +1 -1
- package/dist/mjs/localizations/es.mjs +1 -1
- package/dist/mjs/rendering/caching/RenderingCacheNode.mjs +20 -15
- package/dist/mjs/testing/createEditor.d.ts +2 -2
- package/dist/mjs/testing/createEditor.mjs +2 -2
- package/dist/mjs/testing/findNodeWithText.d.ts +3 -0
- package/dist/mjs/testing/findNodeWithText.mjs +14 -0
- package/dist/mjs/testing/firstElementAncestorOfNode.d.ts +3 -0
- package/dist/mjs/testing/firstElementAncestorOfNode.mjs +11 -0
- package/dist/mjs/testing/sendKeyPressRelease.d.ts +3 -0
- package/dist/mjs/testing/sendKeyPressRelease.mjs +6 -0
- package/dist/mjs/testing/sendPenEvent.d.ts +2 -2
- package/dist/mjs/testing/sendPenEvent.mjs +3 -3
- package/dist/mjs/toolbar/IconProvider.d.ts +3 -1
- package/dist/mjs/toolbar/IconProvider.mjs +15 -3
- package/dist/mjs/toolbar/localization.d.ts +8 -1
- package/dist/mjs/toolbar/localization.mjs +9 -2
- package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -0
- package/dist/mjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/mjs/toolbar/widgets/EraserToolWidget.mjs +47 -6
- package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +22 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.mjs +54 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/fileToImages.d.ts +3 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/fileToImages.mjs +16 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/index.d.ts +37 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/index.mjs +276 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/index.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +10 -3
- package/dist/mjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/TextToolWidget.mjs +5 -3
- package/dist/mjs/toolbar/widgets/TextToolWidget.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/components/makeFileInput.d.ts +12 -2
- package/dist/mjs/toolbar/widgets/components/makeFileInput.mjs +102 -45
- package/dist/mjs/toolbar/widgets/components/makeFileInput.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/components/makeSnappedList.d.ts +15 -0
- package/dist/mjs/toolbar/widgets/components/makeSnappedList.mjs +98 -0
- package/dist/mjs/toolbar/widgets/keybindings.mjs +1 -1
- package/dist/mjs/tools/Eraser.d.ts +31 -6
- package/dist/mjs/tools/Eraser.mjs +161 -22
- package/dist/mjs/tools/PasteHandler.mjs +0 -1
- package/dist/mjs/tools/SelectionTool/Selection.d.ts +2 -2
- package/dist/mjs/tools/SelectionTool/Selection.mjs +20 -20
- package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +8 -2
- package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +6 -0
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +1 -1
- package/dist/mjs/tools/SelectionTool/types.d.ts +19 -0
- package/dist/mjs/tools/TextTool.mjs +2 -1
- package/dist/mjs/tools/TextTool.test.d.ts +1 -0
- package/dist/mjs/tools/ToolController.d.ts +2 -0
- package/dist/mjs/tools/ToolController.mjs +10 -1
- package/dist/mjs/tools/lib.d.ts +1 -4
- package/dist/mjs/tools/lib.mjs +1 -4
- package/dist/mjs/util/ReactiveValue.d.ts +2 -0
- package/dist/mjs/util/ReactiveValue.mjs +11 -0
- package/dist/mjs/util/bytesToSizeString.d.ts +8 -0
- package/dist/mjs/util/bytesToSizeString.mjs +24 -0
- package/dist/mjs/util/bytesToSizeString.test.d.ts +1 -0
- package/dist/mjs/util/stopPropagationOfScrollingWheelEvents.mjs +10 -6
- package/dist/mjs/util/waitForAll.d.ts +2 -0
- package/dist/mjs/util/waitForAll.mjs +2 -0
- package/dist/mjs/util/waitForImageLoaded.mjs +3 -0
- package/dist/mjs/util/waitForTimeout.d.ts +1 -0
- package/dist/mjs/util/waitForTimeout.mjs +1 -1
- package/dist/mjs/version.mjs +1 -1
- package/package.json +4 -4
- package/src/toolbar/toolbar.scss +1 -7
- package/src/toolbar/widgets/{InsertImageWidget.scss → InsertImageWidget/index.scss} +3 -2
- package/src/toolbar/widgets/components/components.scss +2 -1
- package/src/toolbar/widgets/components/makeFileInput.scss +14 -1
- package/src/toolbar/widgets/components/makeSnappedList.scss +28 -0
- package/src/toolbar/widgets/widgets.scss +7 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +0 -22
- package/dist/cjs/toolbar/widgets/InsertImageWidget.js +0 -269
- package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +0 -22
- package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +0 -264
- /package/dist/cjs/{SVGLoader.test.d.ts → SVGLoader/index.test.d.ts} +0 -0
- /package/dist/{mjs/SVGLoader.test.d.ts → cjs/toolbar/widgets/InsertImageWidget/index.test.d.ts} +0 -0
@@ -6,6 +6,7 @@ import AbstractComponent from './AbstractComponent';
|
|
6
6
|
import { ImageComponentLocalization } from './localization';
|
7
7
|
import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
|
8
8
|
import RenderablePathSpec, { RenderablePathSpecWithPath } from '../rendering/RenderablePathSpec';
|
9
|
+
import Viewport from '../Viewport';
|
9
10
|
/**
|
10
11
|
* Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
|
11
12
|
*
|
@@ -46,10 +47,12 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
|
|
46
47
|
* ]);
|
47
48
|
* ```
|
48
49
|
*/
|
49
|
-
constructor(parts: RenderablePathSpec[]);
|
50
|
+
constructor(parts: RenderablePathSpec[], initialZIndex?: number);
|
50
51
|
getStyle(): ComponentStyle;
|
51
52
|
updateStyle(style: ComponentStyle): SerializableCommand;
|
52
53
|
forceStyle(style: ComponentStyle, editor: Editor | null): void;
|
54
|
+
/** @beta -- May fail for concave `path`s */
|
55
|
+
withRegionErased(eraserPath: Path, viewport: Viewport): Stroke[];
|
53
56
|
intersects(line: LineSegment2): boolean;
|
54
57
|
intersectsRect(rect: Rect2): boolean;
|
55
58
|
private simplifiedPath;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Path, Rect2 } from '@js-draw/math';
|
1
|
+
import { Path, Rect2, PathCommandType, comparePathIndices, stepPathIndexBy } from '@js-draw/math';
|
2
2
|
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle.mjs';
|
3
3
|
import AbstractComponent from './AbstractComponent.mjs';
|
4
4
|
import { createRestyleComponentCommand } from './RestylableComponent.mjs';
|
@@ -39,8 +39,8 @@ export default class Stroke extends AbstractComponent {
|
|
39
39
|
* ]);
|
40
40
|
* ```
|
41
41
|
*/
|
42
|
-
constructor(parts) {
|
43
|
-
super('stroke');
|
42
|
+
constructor(parts, initialZIndex) {
|
43
|
+
super('stroke', initialZIndex);
|
44
44
|
// @internal
|
45
45
|
// eslint-disable-next-line @typescript-eslint/prefer-as-const
|
46
46
|
this.isRestylableComponent = true;
|
@@ -118,6 +118,162 @@ export default class Stroke extends AbstractComponent {
|
|
118
118
|
editor.queueRerender();
|
119
119
|
}
|
120
120
|
}
|
121
|
+
/** @beta -- May fail for concave `path`s */
|
122
|
+
withRegionErased(eraserPath, viewport) {
|
123
|
+
const polyline = eraserPath.polylineApproximation();
|
124
|
+
const isPointInsideEraser = (point) => {
|
125
|
+
return eraserPath.closedContainsPoint(point);
|
126
|
+
};
|
127
|
+
const newStrokes = [];
|
128
|
+
let failedAssertions = false;
|
129
|
+
for (const part of this.parts) {
|
130
|
+
const path = part.path;
|
131
|
+
const makeStroke = (path) => {
|
132
|
+
if (part.style.fill.a > 0) {
|
133
|
+
// Remove visually empty paths.
|
134
|
+
if (path.parts.length < 1 || (path.parts.length === 1 && path.parts[0].kind === PathCommandType.LineTo)) {
|
135
|
+
// TODO: If this isn't present, a very large number of strokes are created while erasing.
|
136
|
+
return null;
|
137
|
+
}
|
138
|
+
else {
|
139
|
+
// Filled paths must be closed (allows for optimizations elsewhere)
|
140
|
+
path = path.asClosed();
|
141
|
+
}
|
142
|
+
}
|
143
|
+
if (isNaN(path.getExactBBox().area)) {
|
144
|
+
console.warn('Prevented creating a stroke with NaN area');
|
145
|
+
failedAssertions = true;
|
146
|
+
return null;
|
147
|
+
}
|
148
|
+
return new Stroke([pathToRenderable(path, part.style)], this.getZIndex());
|
149
|
+
};
|
150
|
+
const intersectionPoints = [];
|
151
|
+
// If stroked, finds intersections with the middle of the stroke.
|
152
|
+
// If filled, finds intersections with the edge of the stroke.
|
153
|
+
for (const segment of polyline) {
|
154
|
+
intersectionPoints.push(...path.intersection(segment));
|
155
|
+
}
|
156
|
+
// When stroked, if the stroke width is significantly larger than the eraser,
|
157
|
+
// it can't intersect both the edge of the stroke and its middle at the same time
|
158
|
+
// (generally, erasing is triggered by the eraser touching the edge of this stroke).
|
159
|
+
//
|
160
|
+
// As such, we also look for intersections along the edge of this, if none with the
|
161
|
+
// center were found, but only within a certain range of sizes because:
|
162
|
+
// 1. Intersection testing with stroked paths is generally much slower than with
|
163
|
+
// non-stroked paths.
|
164
|
+
// 2. If zoomed in significantly, it's unlikely that the user wants to erase a large
|
165
|
+
// part of the stroke.
|
166
|
+
let isErasingFromEdge = false;
|
167
|
+
if (intersectionPoints.length === 0
|
168
|
+
&& part.style.stroke
|
169
|
+
&& part.style.stroke.width > eraserPath.bbox.minDimension * 0.3
|
170
|
+
&& part.style.stroke.width < eraserPath.bbox.maxDimension * 30) {
|
171
|
+
for (const segment of polyline) {
|
172
|
+
intersectionPoints.push(...path.intersection(segment, part.style.stroke.width / 2));
|
173
|
+
}
|
174
|
+
isErasingFromEdge = true;
|
175
|
+
}
|
176
|
+
// Sort first by curve index, then by parameter value
|
177
|
+
intersectionPoints.sort(comparePathIndices);
|
178
|
+
const isInsideJustBeforeFirst = (() => {
|
179
|
+
if (intersectionPoints.length === 0) {
|
180
|
+
return false;
|
181
|
+
}
|
182
|
+
// The eraser may not be near the center of the curve -- approximate.
|
183
|
+
if (isErasingFromEdge) {
|
184
|
+
return intersectionPoints[0].curveIndex === 0 && intersectionPoints[0].parameterValue <= 0;
|
185
|
+
}
|
186
|
+
const justBeforeFirstIntersection = stepPathIndexBy(intersectionPoints[0], -1e-10);
|
187
|
+
return isPointInsideEraser(path.at(justBeforeFirstIntersection));
|
188
|
+
})();
|
189
|
+
let intersectionCount = isInsideJustBeforeFirst ? 1 : 0;
|
190
|
+
const addNewPath = (path, knownToBeInside) => {
|
191
|
+
const component = makeStroke(path);
|
192
|
+
let isInside = intersectionCount % 2 === 1;
|
193
|
+
intersectionCount++;
|
194
|
+
if (knownToBeInside !== undefined) {
|
195
|
+
isInside = knownToBeInside;
|
196
|
+
}
|
197
|
+
// Here, we work around bugs in the underlying Bezier curve library
|
198
|
+
// (including https://github.com/Pomax/bezierjs/issues/179).
|
199
|
+
// Even if not all intersections are returned correctly, we still want
|
200
|
+
// isInside to be roughly correct.
|
201
|
+
if (knownToBeInside === undefined && !isInside && eraserPath.closedContainsPoint(path.getExactBBox().center)) {
|
202
|
+
isInside = !isInside;
|
203
|
+
}
|
204
|
+
if (!component) {
|
205
|
+
return;
|
206
|
+
}
|
207
|
+
// Assertion: Avoid deleting sections that are much larger than the eraser.
|
208
|
+
failedAssertions ||= isInside && path.getExactBBox().maxDimension > eraserPath.getExactBBox().maxDimension * 2;
|
209
|
+
if (!isInside) {
|
210
|
+
newStrokes.push(component);
|
211
|
+
}
|
212
|
+
};
|
213
|
+
if (part.style.fill.a === 0) { // Not filled?
|
214
|
+
// An additional case where we erase completely -- without the padding of the stroke,
|
215
|
+
// the path is smaller than the eraser (allows us to erase dots completely).
|
216
|
+
const shouldEraseCompletely = eraserPath.getExactBBox().maxDimension / 10 > path.getExactBBox().maxDimension;
|
217
|
+
if (!shouldEraseCompletely) {
|
218
|
+
const split = path.splitAt(intersectionPoints, { mapNewPoint: p => viewport.roundPoint(p) });
|
219
|
+
for (const splitPart of split) {
|
220
|
+
addNewPath(splitPart);
|
221
|
+
}
|
222
|
+
}
|
223
|
+
}
|
224
|
+
else if (intersectionPoints.length >= 2 && intersectionPoints.length % 2 === 0) {
|
225
|
+
// TODO: Support subtractive erasing on small scales -- see https://github.com/personalizedrefrigerator/js-draw/pull/63/commits/568686e2384219ad0bb07617ea4efff1540aed00
|
226
|
+
// for a broken implementation.
|
227
|
+
//
|
228
|
+
// We currently assume that a 4-point intersection means that the intersection
|
229
|
+
// looks similar to this:
|
230
|
+
// -----------
|
231
|
+
// | STROKE |
|
232
|
+
// | |
|
233
|
+
//%%x-----------x%%%%%%%
|
234
|
+
//% %
|
235
|
+
//% ERASER %
|
236
|
+
//% %
|
237
|
+
//%%x-----------x%%%%%%%
|
238
|
+
// | STROKE |
|
239
|
+
// -----------
|
240
|
+
//
|
241
|
+
// Our goal is to separate STROKE into the contiguous parts outside
|
242
|
+
// of the eraser (as shown above).
|
243
|
+
//
|
244
|
+
// To do this, we split STROKE at each intersection:
|
245
|
+
// 3 3 3 3 3 3
|
246
|
+
// 3 STROKE 3
|
247
|
+
// 3 3
|
248
|
+
// x x
|
249
|
+
// 2 4
|
250
|
+
// 2 STROKE 4
|
251
|
+
// 2 4
|
252
|
+
// x x
|
253
|
+
// 1 STROKE 5
|
254
|
+
// . 5 5 5 5 5
|
255
|
+
// ^
|
256
|
+
// Start
|
257
|
+
//
|
258
|
+
// The difficulty here is correctly pairing edges to create the the output
|
259
|
+
// strokes, particularly because we don't know the order of intersection points.
|
260
|
+
const parts = path.splitAt(intersectionPoints, { mapNewPoint: p => viewport.roundPoint(p) });
|
261
|
+
for (let i = 0; i < Math.floor(parts.length / 2); i++) {
|
262
|
+
addNewPath(parts[i].union(parts[parts.length - i - 1]).asClosed());
|
263
|
+
}
|
264
|
+
if (parts.length % 2 !== 0) {
|
265
|
+
addNewPath(parts[Math.floor(parts.length / 2)].asClosed());
|
266
|
+
}
|
267
|
+
}
|
268
|
+
else {
|
269
|
+
addNewPath(path, false);
|
270
|
+
}
|
271
|
+
}
|
272
|
+
if (failedAssertions) {
|
273
|
+
return [this];
|
274
|
+
}
|
275
|
+
return newStrokes;
|
276
|
+
}
|
121
277
|
intersects(line) {
|
122
278
|
for (const part of this.parts) {
|
123
279
|
const strokeWidth = part.style.stroke?.width;
|
@@ -83,7 +83,9 @@ class TextComponent extends AbstractComponent {
|
|
83
83
|
}
|
84
84
|
static applyTextStyles(ctx, style) {
|
85
85
|
// Quote the font family if necessary.
|
86
|
-
const
|
86
|
+
const hasSpaces = style.fontFamily.match(/\s/);
|
87
|
+
const isQuoted = style.fontFamily.match(/^".*"$/);
|
88
|
+
const fontFamily = hasSpaces && !isQuoted ? `"${style.fontFamily.replace(/["]/g, '\\"')}"` : style.fontFamily;
|
87
89
|
ctx.font = [
|
88
90
|
style.fontStyle ?? '',
|
89
91
|
style.fontWeight ?? '',
|
@@ -9,7 +9,6 @@ import RenderingStyle from '../../rendering/RenderingStyle';
|
|
9
9
|
/**
|
10
10
|
* Creates strokes from line segments rather than Bézier curves.
|
11
11
|
*
|
12
|
-
* @beta Output behavior may change significantly between versions. For now, intended for debugging.
|
13
12
|
*/
|
14
13
|
export declare const makePolylineBuilder: ComponentBuilderFactory;
|
15
14
|
export default class PolylineBuilder implements ComponentBuilder {
|
@@ -21,6 +20,7 @@ export default class PolylineBuilder implements ComponentBuilder {
|
|
21
20
|
private widthAverageNumSamples;
|
22
21
|
private lastPoint;
|
23
22
|
private startPoint;
|
23
|
+
private lastLineSegment;
|
24
24
|
constructor(startPoint: StrokeDataPoint, minFitAllowed: number, viewport: Viewport);
|
25
25
|
getBBox(): Rect2;
|
26
26
|
protected getRenderingStyle(): RenderingStyle;
|
@@ -1,11 +1,10 @@
|
|
1
|
-
import { Rect2, Color4, PathCommandType } from '@js-draw/math';
|
1
|
+
import { Rect2, Color4, PathCommandType, Vec2, LineSegment2 } from '@js-draw/math';
|
2
2
|
import Stroke from '../Stroke.mjs';
|
3
3
|
import Viewport from '../../Viewport.mjs';
|
4
4
|
import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs';
|
5
5
|
/**
|
6
6
|
* Creates strokes from line segments rather than Bézier curves.
|
7
7
|
*
|
8
|
-
* @beta Output behavior may change significantly between versions. For now, intended for debugging.
|
9
8
|
*/
|
10
9
|
export const makePolylineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => {
|
11
10
|
const minFit = viewport.getSizeOfPixelOnCanvas();
|
@@ -17,6 +16,7 @@ export default class PolylineBuilder {
|
|
17
16
|
this.viewport = viewport;
|
18
17
|
this.parts = [];
|
19
18
|
this.widthAverageNumSamples = 1;
|
19
|
+
this.lastLineSegment = null;
|
20
20
|
this.averageWidth = startPoint.width;
|
21
21
|
this.startPoint = {
|
22
22
|
...startPoint,
|
@@ -50,7 +50,7 @@ export default class PolylineBuilder {
|
|
50
50
|
if (commands.length <= 1) {
|
51
51
|
commands.push({
|
52
52
|
kind: PathCommandType.LineTo,
|
53
|
-
point: startPoint,
|
53
|
+
point: startPoint.plus(Vec2.of(this.averageWidth / 4, 0)),
|
54
54
|
});
|
55
55
|
}
|
56
56
|
return {
|
@@ -98,11 +98,18 @@ export default class PolylineBuilder {
|
|
98
98
|
+ newPoint.width / this.widthAverageNumSamples;
|
99
99
|
const roundedPoint = this.roundPoint(newPoint.pos);
|
100
100
|
if (!roundedPoint.eq(this.lastPoint)) {
|
101
|
+
// If almost exactly in the same line as the previous
|
102
|
+
if (this.lastLineSegment && this.lastLineSegment.direction.dot(roundedPoint.minus(this.lastPoint).normalized()) > 0.997) {
|
103
|
+
this.parts.pop();
|
104
|
+
this.lastPoint = this.lastLineSegment.p1;
|
105
|
+
}
|
101
106
|
this.parts.push({
|
102
107
|
kind: PathCommandType.LineTo,
|
103
108
|
point: this.roundPoint(newPoint.pos),
|
104
109
|
});
|
105
110
|
this.bbox = this.bbox.grownToPoint(roundedPoint);
|
111
|
+
this.lastLineSegment = new LineSegment2(this.lastPoint, roundedPoint);
|
112
|
+
this.lastPoint = roundedPoint;
|
106
113
|
}
|
107
114
|
}
|
108
115
|
}
|
@@ -12,6 +12,7 @@ export default class PressureSensitiveFreehandLineBuilder implements ComponentBu
|
|
12
12
|
private isFirstSegment;
|
13
13
|
private pathStartConnector;
|
14
14
|
private mostRecentConnector;
|
15
|
+
private nextCurveStartConnector;
|
15
16
|
private upperSegments;
|
16
17
|
private lowerSegments;
|
17
18
|
private lastUpperBezier;
|
@@ -25,7 +26,6 @@ export default class PressureSensitiveFreehandLineBuilder implements ComponentBu
|
|
25
26
|
private getRenderingStyle;
|
26
27
|
private previewCurrentPath;
|
27
28
|
private previewFullPath;
|
28
|
-
private previewStroke;
|
29
29
|
preview(renderer: AbstractRenderer): void;
|
30
30
|
build(): Stroke;
|
31
31
|
private roundPoint;
|
@@ -1,5 +1,4 @@
|
|
1
|
-
import {
|
2
|
-
import { Vec2, Rect2, PathCommandType } from '@js-draw/math';
|
1
|
+
import { Vec2, Rect2, PathCommandType, QuadraticBezier } from '@js-draw/math';
|
3
2
|
import Stroke from '../Stroke.mjs';
|
4
3
|
import Viewport from '../../Viewport.mjs';
|
5
4
|
import { StrokeSmoother } from '../util/StrokeSmoother.mjs';
|
@@ -25,6 +24,7 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
25
24
|
this.isFirstSegment = true;
|
26
25
|
this.pathStartConnector = null;
|
27
26
|
this.mostRecentConnector = null;
|
27
|
+
this.nextCurveStartConnector = null;
|
28
28
|
this.lastUpperBezier = null;
|
29
29
|
this.lastLowerBezier = null;
|
30
30
|
this.parts = [];
|
@@ -42,18 +42,18 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
42
42
|
fill: this.startPoint.color ?? null,
|
43
43
|
};
|
44
44
|
}
|
45
|
-
previewCurrentPath() {
|
45
|
+
previewCurrentPath(extendWithLatest = true) {
|
46
46
|
const upperPath = this.upperSegments.slice();
|
47
47
|
const lowerPath = this.lowerSegments.slice();
|
48
48
|
let lowerToUpperCap;
|
49
49
|
let pathStartConnector;
|
50
50
|
const currentCurve = this.curveFitter.preview();
|
51
|
-
if (currentCurve) {
|
51
|
+
if (currentCurve && extendWithLatest) {
|
52
52
|
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand } = this.segmentToPath(currentCurve);
|
53
53
|
upperPath.push(upperCurveCommand);
|
54
54
|
lowerPath.push(lowerCurveCommand);
|
55
55
|
lowerToUpperCap = lowerToUpperConnector;
|
56
|
-
pathStartConnector = this.pathStartConnector ?? upperToLowerConnector;
|
56
|
+
pathStartConnector = this.pathStartConnector ?? [upperToLowerConnector];
|
57
57
|
}
|
58
58
|
else {
|
59
59
|
if (this.mostRecentConnector === null || this.pathStartConnector === null) {
|
@@ -94,7 +94,7 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
94
94
|
// __/ __/
|
95
95
|
// /___ /
|
96
96
|
// •
|
97
|
-
pathStartConnector,
|
97
|
+
...pathStartConnector,
|
98
98
|
// Move back to the start point:
|
99
99
|
// •
|
100
100
|
// __/ __/
|
@@ -111,13 +111,6 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
111
111
|
}
|
112
112
|
return null;
|
113
113
|
}
|
114
|
-
previewStroke() {
|
115
|
-
const pathPreview = this.previewFullPath();
|
116
|
-
if (pathPreview) {
|
117
|
-
return new Stroke(pathPreview);
|
118
|
-
}
|
119
|
-
return null;
|
120
|
-
}
|
121
114
|
preview(renderer) {
|
122
115
|
const paths = this.previewFullPath();
|
123
116
|
if (paths) {
|
@@ -135,7 +128,7 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
135
128
|
// Ensure we have something.
|
136
129
|
this.addCurve(null);
|
137
130
|
}
|
138
|
-
return this.
|
131
|
+
return new Stroke(this.previewFullPath());
|
139
132
|
}
|
140
133
|
roundPoint(point) {
|
141
134
|
let minFit = Math.min(this.minFitAllowed, this.curveStartWidth / 3);
|
@@ -150,25 +143,16 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
150
143
|
return false;
|
151
144
|
}
|
152
145
|
const getIntersection = (curve1, curve2) => {
|
153
|
-
const
|
154
|
-
if (!
|
146
|
+
const intersections = curve1.intersectsBezier(curve2);
|
147
|
+
if (!intersections.length)
|
155
148
|
return null;
|
156
|
-
|
157
|
-
// From http://pomax.github.io/bezierjs/#intersect-curve,
|
158
|
-
// .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
|
159
|
-
const firstTPair = intersection[0];
|
160
|
-
const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
|
161
|
-
if (!match) {
|
162
|
-
throw new Error(`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`);
|
163
|
-
}
|
164
|
-
const t = parseFloat(match[1]);
|
165
|
-
return Vec2.ofXY(curve1.get(t));
|
149
|
+
return intersections[0].point;
|
166
150
|
};
|
167
151
|
const getExitDirection = (curve) => {
|
168
|
-
return
|
152
|
+
return curve.p2.minus(curve.p1).normalized();
|
169
153
|
};
|
170
154
|
const getEnterDirection = (curve) => {
|
171
|
-
return
|
155
|
+
return curve.p1.minus(curve.p0).normalized();
|
172
156
|
};
|
173
157
|
// Prevent
|
174
158
|
// /
|
@@ -179,8 +163,8 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
179
163
|
// where the next stroke and the previous stroke are in different directions.
|
180
164
|
//
|
181
165
|
// Are the exit/enter directions of the previous and current curves in different enough directions?
|
182
|
-
if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.
|
183
|
-
|| getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.
|
166
|
+
if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.35
|
167
|
+
|| getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.35
|
184
168
|
// Also handle if the curves exit/enter directions differ
|
185
169
|
|| getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
|
186
170
|
|| getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
|
@@ -236,32 +220,37 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
236
220
|
controlPoint: center.plus(Vec2.of(width, -width)),
|
237
221
|
endPoint: center.plus(Vec2.of(width, 0)),
|
238
222
|
});
|
239
|
-
|
223
|
+
const connector = {
|
240
224
|
kind: PathCommandType.LineTo,
|
241
225
|
point: startPoint,
|
242
226
|
};
|
243
|
-
this.
|
227
|
+
this.pathStartConnector = [connector];
|
228
|
+
this.mostRecentConnector = connector;
|
244
229
|
return;
|
245
230
|
}
|
246
|
-
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, } = this.segmentToPath(curve);
|
247
|
-
|
231
|
+
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, nextCurveStartConnector, } = this.segmentToPath(curve);
|
232
|
+
let shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
|
248
233
|
if (shouldStartNew) {
|
249
|
-
const part = this.previewCurrentPath();
|
234
|
+
const part = this.previewCurrentPath(false);
|
250
235
|
if (part) {
|
251
236
|
this.parts.push(part);
|
252
237
|
this.upperSegments = [];
|
253
238
|
this.lowerSegments = [];
|
254
239
|
}
|
240
|
+
else {
|
241
|
+
shouldStartNew = false;
|
242
|
+
}
|
255
243
|
}
|
256
244
|
if (this.isFirstSegment || shouldStartNew) {
|
257
245
|
// We draw the upper path (reversed), then the lower path, so we need the
|
258
246
|
// upperToLowerConnector to join the two paths.
|
259
|
-
this.pathStartConnector = upperToLowerConnector;
|
247
|
+
this.pathStartConnector = this.nextCurveStartConnector ?? [upperToLowerConnector];
|
260
248
|
this.isFirstSegment = false;
|
261
249
|
}
|
262
250
|
// With the most recent connector, we're joining the end of the lowerPath to the most recent
|
263
251
|
// upperPath:
|
264
252
|
this.mostRecentConnector = lowerToUpperConnector;
|
253
|
+
this.nextCurveStartConnector = nextCurveStartConnector;
|
265
254
|
this.lowerSegments.push(lowerCurveCommand);
|
266
255
|
this.upperSegments.push(upperCurveCommand);
|
267
256
|
this.lastLowerBezier = lowerCurve;
|
@@ -270,9 +259,9 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
270
259
|
}
|
271
260
|
// Returns [upper curve, connector, lower curve]
|
272
261
|
segmentToPath(curve) {
|
273
|
-
const bezier = new
|
274
|
-
let startVec =
|
275
|
-
let endVec =
|
262
|
+
const bezier = new QuadraticBezier(curve.startPoint, curve.controlPoint, curve.endPoint);
|
263
|
+
let startVec = bezier.normal(0);
|
264
|
+
let endVec = bezier.normal(1);
|
276
265
|
startVec = startVec.times(curve.startWidth / 2);
|
277
266
|
endVec = endVec.times(curve.endWidth / 2);
|
278
267
|
if (!isFinite(startVec.magnitude())) {
|
@@ -283,18 +272,9 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
283
272
|
const endPt = curve.endPoint;
|
284
273
|
const controlPoint = curve.controlPoint;
|
285
274
|
// Approximate the normal at the location of the control point
|
286
|
-
|
287
|
-
if (!projectionT) {
|
288
|
-
if (startPt.squareDistanceTo(controlPoint) < endPt.squareDistanceTo(controlPoint)) {
|
289
|
-
projectionT = 0.1;
|
290
|
-
}
|
291
|
-
else {
|
292
|
-
projectionT = 0.9;
|
293
|
-
}
|
294
|
-
}
|
275
|
+
const projectionT = bezier.nearestPointTo(controlPoint).parameterValue;
|
295
276
|
const halfVecT = projectionT;
|
296
|
-
const halfVec =
|
297
|
-
.normalized().times(curve.startWidth / 2 * halfVecT
|
277
|
+
const halfVec = bezier.normal(halfVecT).times(curve.startWidth / 2 * halfVecT
|
298
278
|
+ curve.endWidth / 2 * (1 - halfVecT));
|
299
279
|
// Each starts at startPt ± startVec
|
300
280
|
const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
|
@@ -318,16 +298,29 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
318
298
|
kind: PathCommandType.LineTo,
|
319
299
|
point: upperCurveStartPoint,
|
320
300
|
};
|
301
|
+
// The segment to be used to start the next path (to insert to connect the start of its
|
302
|
+
// lower and the end of its upper).
|
303
|
+
const nextCurveStartConnector = [
|
304
|
+
{
|
305
|
+
kind: PathCommandType.LineTo,
|
306
|
+
point: upperCurveStartPoint,
|
307
|
+
},
|
308
|
+
{
|
309
|
+
kind: PathCommandType.LineTo,
|
310
|
+
point: lowerCurveEndPoint,
|
311
|
+
},
|
312
|
+
];
|
321
313
|
const upperCurveCommand = {
|
322
314
|
kind: PathCommandType.QuadraticBezierTo,
|
323
315
|
controlPoint: upperCurveControlPoint,
|
324
316
|
endPoint: upperCurveEndPoint,
|
325
317
|
};
|
326
|
-
const upperCurve = new
|
327
|
-
const lowerCurve = new
|
318
|
+
const upperCurve = new QuadraticBezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
|
319
|
+
const lowerCurve = new QuadraticBezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
|
328
320
|
return {
|
329
321
|
upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
|
330
322
|
upperCurve, lowerCurve,
|
323
|
+
nextCurveStartConnector,
|
331
324
|
};
|
332
325
|
}
|
333
326
|
addPoint(newPoint) {
|
@@ -629,7 +629,7 @@ export class ImageNode {
|
|
629
629
|
this.bbox = Rect2.union(...this.children.map(child => child.getBBox()));
|
630
630
|
}
|
631
631
|
if (bubbleUp && !oldBBox.eq(this.bbox)) {
|
632
|
-
if (
|
632
|
+
if (this.bbox.containsRect(oldBBox)) {
|
633
633
|
this.parent?.unionBBoxWith(this.bbox);
|
634
634
|
}
|
635
635
|
else {
|
@@ -27,7 +27,7 @@ const localization = {
|
|
27
27
|
selectionToolKeyboardShortcuts: 'Auswahl-Werkzeug: Verwende die Pfeiltasten, um ausgewählte Elemente zu verschieben und ‚i‘ und ‚o‘, um ihre Größe zu ändern.',
|
28
28
|
touchPanning: 'Ansicht mit Touchscreen verschieben',
|
29
29
|
anyDevicePanning: 'Ansicht mit jedem Eingabegerät verschieben',
|
30
|
-
|
30
|
+
selectPenType: 'Objekt-Typ: ',
|
31
31
|
roundedTipPen: 'Freihand',
|
32
32
|
flatTipPen: 'Stift (druckempfindlich)',
|
33
33
|
arrowPen: 'Pfeil',
|
@@ -22,7 +22,7 @@ const localization = {
|
|
22
22
|
save: 'Guardar',
|
23
23
|
undo: 'Deshace',
|
24
24
|
redo: 'Rehace',
|
25
|
-
|
25
|
+
selectPenType: 'Punta',
|
26
26
|
selectShape: 'Forma',
|
27
27
|
pickColorFromScreen: 'Selecciona un color de la pantalla',
|
28
28
|
clickToPickColorAnnouncement: 'Haga un clic en la pantalla para seleccionar un color',
|
@@ -139,24 +139,29 @@ export default class RenderingCacheNode {
|
|
139
139
|
|| items.length === 0) {
|
140
140
|
return;
|
141
141
|
}
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
const
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
142
|
+
// Divide [items] until nodes are smaller than this, or are leaves.
|
143
|
+
const divideUntilSmallerThanThis = (itemsToDivide) => {
|
144
|
+
const newItems = [];
|
145
|
+
for (const item of itemsToDivide) {
|
146
|
+
const bbox = item.getBBox();
|
147
|
+
if (!bbox.intersects(this.region)) {
|
148
|
+
continue;
|
149
|
+
}
|
150
|
+
if (bbox.maxDimension >= this.region.maxDimension) {
|
151
|
+
newItems.push(...item.getChildrenOrSelfIntersectingRegion(this.region));
|
152
|
+
}
|
153
|
+
else {
|
154
|
+
newItems.push(item);
|
155
|
+
}
|
154
156
|
}
|
155
|
-
|
156
|
-
|
157
|
+
return newItems;
|
158
|
+
};
|
159
|
+
items = divideUntilSmallerThanThis(items);
|
157
160
|
// Can we cache at all?
|
158
161
|
if (!this.cacheState.props.isOfCorrectType(screenRenderer)) {
|
159
|
-
|
162
|
+
for (const item of items) {
|
163
|
+
item.render(screenRenderer, viewport.visibleRect);
|
164
|
+
}
|
160
165
|
return;
|
161
166
|
}
|
162
167
|
if (this.cacheState.debugMode) {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import Editor from '../Editor';
|
1
|
+
import Editor, { EditorSettings } from '../Editor';
|
2
2
|
/** Creates an editor. Should only be used in test files. */
|
3
|
-
declare const _default: () => Editor;
|
3
|
+
declare const _default: (settings?: Partial<EditorSettings>) => Editor;
|
4
4
|
export default _default;
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import { RenderingMode } from '../rendering/Display.mjs';
|
2
2
|
import Editor from '../Editor.mjs';
|
3
3
|
/** Creates an editor. Should only be used in test files. */
|
4
|
-
export default () => {
|
4
|
+
export default (settings) => {
|
5
5
|
if (jest === undefined) {
|
6
6
|
throw new Error('Files in the testing/ folder should only be used in tests!');
|
7
7
|
}
|
8
|
-
return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
|
8
|
+
return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer, ...settings });
|
9
9
|
};
|
@@ -0,0 +1,14 @@
|
|
1
|
+
/** Returns the first node or element with `textContent` matching `expectedText`. */
|
2
|
+
const findNodeWithText = (expectedText, parent) => {
|
3
|
+
if (parent.textContent === expectedText) {
|
4
|
+
return parent;
|
5
|
+
}
|
6
|
+
for (const child of parent.childNodes) {
|
7
|
+
const results = findNodeWithText(expectedText, child);
|
8
|
+
if (results) {
|
9
|
+
return results;
|
10
|
+
}
|
11
|
+
}
|
12
|
+
return null;
|
13
|
+
};
|
14
|
+
export default findNodeWithText;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
/** Returns the first ancestor of the given node that is an HTMLElement */
|
2
|
+
const firstElementAncestorOfNode = (node) => {
|
3
|
+
if (node instanceof HTMLElement) {
|
4
|
+
return node;
|
5
|
+
}
|
6
|
+
else if (node?.parentNode) {
|
7
|
+
return firstElementAncestorOfNode(node.parentNode);
|
8
|
+
}
|
9
|
+
return null;
|
10
|
+
};
|
11
|
+
export default firstElementAncestorOfNode;
|