@hpcc-js/common 2.73.2 → 2.73.3

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.
Files changed (55) hide show
  1. package/LICENSE +43 -43
  2. package/README.md +59 -59
  3. package/dist/index.es6.js +28 -28
  4. package/dist/index.es6.js.map +1 -1
  5. package/dist/index.js +30 -30
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.min.js.map +1 -1
  8. package/package.json +3 -3
  9. package/src/CanvasWidget.ts +31 -31
  10. package/src/Class.ts +67 -67
  11. package/src/Database.ts +856 -856
  12. package/src/Entity.ts +235 -235
  13. package/src/EntityCard.ts +66 -66
  14. package/src/EntityPin.ts +103 -103
  15. package/src/EntityRect.css +15 -15
  16. package/src/EntityRect.ts +236 -236
  17. package/src/EntityVertex.ts +86 -86
  18. package/src/FAChar.css +2 -2
  19. package/src/FAChar.ts +82 -82
  20. package/src/HTMLWidget.ts +191 -191
  21. package/src/IList.ts +4 -4
  22. package/src/IMenu.ts +5 -5
  23. package/src/Icon.css +9 -9
  24. package/src/Icon.ts +164 -164
  25. package/src/Image.ts +95 -95
  26. package/src/List.css +13 -13
  27. package/src/List.ts +99 -99
  28. package/src/Menu.css +23 -23
  29. package/src/Menu.ts +134 -134
  30. package/src/Palette.ts +341 -341
  31. package/src/Platform.ts +125 -125
  32. package/src/ProgressBar.ts +105 -105
  33. package/src/PropertyExt.ts +793 -793
  34. package/src/ResizeSurface.css +39 -39
  35. package/src/ResizeSurface.ts +221 -221
  36. package/src/SVGWidget.ts +567 -567
  37. package/src/SVGZoomWidget.css +12 -12
  38. package/src/SVGZoomWidget.ts +426 -426
  39. package/src/Shape.css +3 -3
  40. package/src/Shape.ts +186 -186
  41. package/src/Surface.css +35 -35
  42. package/src/Surface.ts +349 -349
  43. package/src/Text.css +4 -4
  44. package/src/Text.ts +131 -131
  45. package/src/TextBox.css +4 -4
  46. package/src/TextBox.ts +168 -168
  47. package/src/TitleBar.css +99 -99
  48. package/src/TitleBar.ts +401 -401
  49. package/src/Transition.ts +45 -45
  50. package/src/Utility.ts +839 -839
  51. package/src/Widget.css +8 -8
  52. package/src/Widget.ts +730 -730
  53. package/src/WidgetArray.ts +13 -13
  54. package/src/__package__.ts +3 -3
  55. package/src/index.ts +55 -55
package/src/SVGWidget.ts CHANGED
@@ -1,567 +1,567 @@
1
- import { rgb as d3Rgb } from "d3-color";
2
- import { select as d3Select } from "d3-selection";
3
- import { fontAwsesomeStyle } from "./FAChar";
4
- import { svgMarkerGlitch } from "./Platform";
5
- import { Transition } from "./Transition";
6
- import { debounce, downloadBlob, downloadString, timestamp } from "./Utility";
7
- import { ISize, Widget } from "./Widget";
8
-
9
- type Point = { x: number, y: number };
10
- type Rect = { x: number, y: number, width: number, height: number };
11
-
12
- const lerp = function (point: Point, that: Point, t: number): Point {
13
- // From https://github.com/thelonious/js-intersections
14
- return {
15
- x: point.x + (that.x - point.x) * t,
16
- y: point.y + (that.y - point.y) * t
17
- };
18
- };
19
-
20
- type LineIntersection = { type: "Intersection" | "No Intersection" | "Coincident" | "Parallel", points: Point[] };
21
- const intersectLineLine = function (a1: Point, a2: Point, b1: Point, b2: Point): LineIntersection {
22
- // From https://github.com/thelonious/js-intersections
23
- const result: LineIntersection = { type: "Parallel", points: [] };
24
- const uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x);
25
- const ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x);
26
- const uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
27
-
28
- if (uB !== 0) {
29
- const ua = uaT / uB;
30
- const ub = ubT / uB;
31
-
32
- if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
33
- result.type = "Intersection";
34
- result.points.push({
35
- x: a1.x + ua * (a2.x - a1.x),
36
- y: a1.y + ua * (a2.y - a1.y)
37
- });
38
- } else {
39
- result.type = "No Intersection";
40
- }
41
- } else {
42
- if (uaT === 0 || ubT === 0) {
43
- result.type = "Coincident";
44
- } else {
45
- result.type = "Parallel";
46
- }
47
- }
48
-
49
- return result;
50
- };
51
-
52
- type CircleIntersection = { type: "Outside" | "Tangent" | "Inside" | "Intersection", points: Point[] };
53
- const intersectCircleLine = function (c: Point, r: number, a1: Point, a2: Point): CircleIntersection {
54
- // From https://github.com/thelonious/js-intersections
55
- const result: CircleIntersection = { type: "Intersection", points: [] };
56
- const a = (a2.x - a1.x) * (a2.x - a1.x) +
57
- (a2.y - a1.y) * (a2.y - a1.y);
58
- const b = 2 * ((a2.x - a1.x) * (a1.x - c.x) +
59
- (a2.y - a1.y) * (a1.y - c.y));
60
- const cc = c.x * c.x + c.y * c.y + a1.x * a1.x + a1.y * a1.y -
61
- 2 * (c.x * a1.x + c.y * a1.y) - r * r;
62
- const deter = b * b - 4 * a * cc;
63
-
64
- if (deter < 0) {
65
- result.type = "Outside";
66
- } else if (deter === 0) {
67
- result.type = "Tangent";
68
- // NOTE: should calculate this point
69
- } else {
70
- const e = Math.sqrt(deter);
71
- const u1 = (-b + e) / (2 * a);
72
- const u2 = (-b - e) / (2 * a);
73
-
74
- if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
75
- if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
76
- result.type = "Outside";
77
- } else {
78
- result.type = "Inside";
79
- }
80
- } else {
81
- result.type = "Intersection";
82
-
83
- if (0 <= u1 && u1 <= 1)
84
- result.points.push(lerp(a1, a2, u1));
85
-
86
- if (0 <= u2 && u2 <= 1)
87
- result.points.push(lerp(a1, a2, u2));
88
- }
89
- }
90
-
91
- return result;
92
- };
93
-
94
- export class SVGGlowFilter {
95
- protected filter;
96
- protected feOffset;
97
- protected feColorMatrix;
98
- protected feGaussianBlur;
99
- protected feBlend;
100
-
101
- constructor(target, id: string) {
102
- this.filter = target.append("filter")
103
- .attr("id", id)
104
- .attr("width", "130%")
105
- .attr("height", "130%");
106
- this.feOffset = this.filter.append("feOffset")
107
- .attr("result", "offOut")
108
- .attr("in", "SourceGraphic")
109
- .attr("dx", "0")
110
- .attr("dy", "0");
111
- this.feColorMatrix = this.filter.append("feColorMatrix")
112
- .attr("result", "matrixOut")
113
- .attr("in", "offOut")
114
- .attr("type", "matrix")
115
- .attr("values", this.rgb2ColorMatrix("red"))
116
- ;
117
- this.feGaussianBlur = this.filter.append("feGaussianBlur")
118
- .attr("result", "blurOut")
119
- .attr("in", "matrixOut")
120
- .attr("stdDeviation", "3")
121
- ;
122
- this.feBlend = this.filter.append("feBlend")
123
- .attr("in", "SourceGraphic")
124
- .attr("in2", "blurOut")
125
- .attr("mode", "normal")
126
- ;
127
- }
128
-
129
- rgb2ColorMatrix(color: string): string {
130
- const rgb = d3Rgb(color);
131
- return [
132
- rgb.r / 255, 0, 0, 0, rgb.r ? 1 : 0,
133
- 0, rgb.g / 255, 0, 0, rgb.g ? 1 : 0,
134
- 0, 0, rgb.b / 255, 0, rgb.b ? 1 : 0,
135
- 0, 0, 0, 1, 0
136
- ].join(" ");
137
- }
138
-
139
- update(color: string) {
140
- this.feColorMatrix.attr("values", this.rgb2ColorMatrix(color));
141
- }
142
- }
143
-
144
- export class SVGWidget extends Widget {
145
- static _class = "common_SVGWidget";
146
-
147
- _tag;
148
-
149
- protected _boundingBox;
150
- protected transition;
151
- protected _drawStartPos: "center" | "origin";
152
- protected _svgSelectionFilter;
153
- protected _parentRelativeDiv;
154
- protected _parentOverlay;
155
-
156
- constructor() {
157
- super();
158
-
159
- this._tag = "g";
160
-
161
- this._boundingBox = null;
162
-
163
- this.transition = new Transition(this);
164
-
165
- this._drawStartPos = "center";
166
- }
167
-
168
- // Properties ---
169
- move(_, transitionDuration?) {
170
- const retVal = this.pos(_);
171
- if (arguments.length) {
172
- (transitionDuration ? this._element.transition().duration(transitionDuration) : this._element)
173
- .attr("transform", `translate(${_.x} ${_.y})scale(${this._widgetScale})`)
174
- ;
175
- }
176
- return retVal;
177
- }
178
-
179
- _enableOverflow = false;
180
- enableOverflow(): boolean;
181
- enableOverflow(_: boolean): this;
182
- enableOverflow(_?: boolean): boolean | this {
183
- if (!arguments.length) return this._enableOverflow;
184
- this._enableOverflow = _;
185
- return this;
186
- }
187
-
188
- _enableOverflowScroll = true;
189
- enableOverflowScroll(): boolean;
190
- enableOverflowScroll(_: boolean): this;
191
- enableOverflowScroll(_?: boolean): boolean | this {
192
- if (!arguments.length) return this._enableOverflowScroll;
193
- this._enableOverflowScroll = _;
194
- return this;
195
- }
196
-
197
- size(): ISize;
198
- size(_): this;
199
- size(_?): ISize | this {
200
- const retVal = super.size.apply(this, arguments);
201
- if (arguments.length) {
202
- this._boundingBox = null;
203
- }
204
- return retVal;
205
- }
206
-
207
- resize(_size?: { width: number, height: number }) {
208
- const retVal = super.resize.apply(this, arguments);
209
- if (this._parentRelativeDiv) {
210
- this._parentRelativeDiv
211
- .style("width", this._size.width + "px")
212
- .style("height", this._size.height + "px")
213
- ;
214
- switch (this._drawStartPos) {
215
- case "origin":
216
- this.pos({
217
- x: 0,
218
- y: 0
219
- });
220
- break;
221
- case "center":
222
- /* falls through */
223
- default:
224
- this.pos({
225
- x: this._size.width / 2,
226
- y: this._size.height / 2
227
- });
228
- break;
229
- }
230
- }
231
- if (!isNaN(this._size.width)) this._placeholderElement.attr("width", this._size.width);
232
- if (!isNaN(this._size.height)) this._placeholderElement.attr("height", this._size.height);
233
- return retVal;
234
- }
235
- // Glow Highlighting ---
236
- svgGlowID(): string {
237
- return `sel${this.id()}_glow`;
238
- }
239
-
240
- target(): null | HTMLElement | SVGElement;
241
- target(_: null | string | HTMLElement | SVGElement): this;
242
- target(_?: null | string | HTMLElement | SVGElement): null | HTMLElement | SVGElement | this {
243
- const retVal = super.target.apply(this, arguments);
244
- if (arguments.length) {
245
- if (this._target instanceof SVGElement) {
246
- this._isRootNode = false;
247
- this._placeholderElement = d3Select(this._target);
248
- this._parentWidget = this._placeholderElement.datum();
249
- if (!this._parentWidget || this._parentWidget._id === this._id) {
250
- this._parentWidget = this.locateParentWidget(this._target.parentNode);
251
- }
252
- this._parentOverlay = this.locateOverlayNode();
253
- const svg = this.locateSVGNode(this._target);
254
- const svgDefs = d3Select(svg).select<SVGDefsElement>("defs");
255
- this._svgSelectionFilter = new SVGGlowFilter(svgDefs, this.svgGlowID());
256
- } else if (this._target) {
257
- // Target is a DOM Node, so create a SVG Element ---
258
- this._parentRelativeDiv = d3Select(this._target).append("div")
259
- .style("position", "relative")
260
- ;
261
- this._placeholderElement = this._parentRelativeDiv.append("svg")
262
- .style("position", "absolute")
263
- .style("top", "0px")
264
- .style("left", "0px")
265
- ;
266
- const svgDefs = this._placeholderElement.append("defs");
267
- this._svgSelectionFilter = new SVGGlowFilter(svgDefs, this.svgGlowID());
268
- this._parentOverlay = this._parentRelativeDiv.append("div")
269
- .style("position", "absolute")
270
- .style("top", "0px")
271
- .style("left", "0px")
272
- ;
273
- if (this._size.width && this._size.height) {
274
- this.resize(this._size);
275
- } else {
276
- this.resize({ width: 0, height: 0 });
277
- }
278
- }
279
- }
280
- return retVal;
281
- }
282
-
283
- parentOverlay() {
284
- return this._parentOverlay;
285
- }
286
-
287
- enter(domNode, element) {
288
- super.enter(domNode, element);
289
- }
290
-
291
- update(domNode, element) {
292
- super.update(domNode, element);
293
- if (this._svgSelectionFilter) {
294
- this._svgSelectionFilter.update(this.selectionGlowColor());
295
- }
296
- }
297
-
298
- postUpdate(domNode, element) {
299
- super.postUpdate(domNode, element);
300
- let transX;
301
- let transY;
302
- if (this._drawStartPos === "origin" && this._target instanceof SVGElement) {
303
- transX = (this._pos.x - this._size.width / 2);
304
- transY = (this._pos.y - this._size.height / 2);
305
- this._element.attr("transform", "translate(" + transX + "," + transY + ")scale(" + this._widgetScale + ")");
306
- } else {
307
- transX = this._pos.x;
308
- transY = this._pos.y;
309
- if (this._enableOverflow) {
310
- // Individual Widgets will need to size and position themselves corrrectly (and have calculated a BBox) ---
311
- if ((transX < 0 || transY < 0) && this._boundingBox) {
312
- transX = transX < 0 ? 0 : transX;
313
- transY = transY < 0 ? 0 : transY;
314
- if (this._enableOverflowScroll) {
315
- this._parentRelativeDiv.style("overflow", "scroll");
316
- }
317
- this._placeholderElement.attr("width", this._boundingBox.width);
318
- this._placeholderElement.attr("height", this._boundingBox.height);
319
- } else {
320
- this._parentRelativeDiv.style("overflow", null);
321
- }
322
- }
323
- this._element.attr("transform", "translate(" + transX + "," + transY + ")scale(" + this._widgetScale + ")");
324
- }
325
- }
326
-
327
- exit(domNode?, element?) {
328
- if (this._parentRelativeDiv) {
329
- this._parentOverlay.remove();
330
- this._placeholderElement.remove();
331
- this._parentRelativeDiv.remove();
332
- }
333
- super.exit(domNode, element);
334
- }
335
-
336
- getOffsetPos(): Point {
337
- let retVal = { x: 0, y: 0 };
338
- if (this._parentWidget) {
339
- retVal = this._parentWidget.getOffsetPos();
340
- retVal.x += this._pos.x;
341
- retVal.y += this._pos.y;
342
- return retVal;
343
- }
344
- return retVal;
345
- }
346
-
347
- getBBox(refresh = false, round = false): Rect {
348
- if (refresh || this._boundingBox === null) {
349
- const svgNode: SVGElement = this._element.node();
350
- if (svgNode instanceof SVGElement) {
351
- this._boundingBox = (svgNode as any).getBBox();
352
- }
353
- }
354
- if (this._boundingBox === null) {
355
- return {
356
- x: 0,
357
- y: 0,
358
- width: 0,
359
- height: 0
360
- };
361
- }
362
- return {
363
- x: (round ? Math.round(this._boundingBox.x) : this._boundingBox.x) * this._widgetScale,
364
- y: (round ? Math.round(this._boundingBox.y) : this._boundingBox.y) * this._widgetScale,
365
- width: (round ? Math.round(this._boundingBox.width) : this._boundingBox.width) * this._widgetScale,
366
- height: (round ? Math.round(this._boundingBox.height) : this._boundingBox.height) * this._widgetScale
367
- };
368
- }
369
-
370
- // Intersections ---
371
- contains(point: Point): boolean {
372
- return this.containsRect(point);
373
- }
374
-
375
- containsRect(point: Point): boolean {
376
- const size = this.getBBox();
377
- return point.x >= size.x && point.x <= size.x + size.width && point.y >= size.y && point.y <= size.y + size.height;
378
- }
379
-
380
- containsCircle(radius: number, point: Point) {
381
- const center = this.getOffsetPos();
382
- return this.distance(center, point) <= radius;
383
- }
384
-
385
- intersection(pointA: Point, pointB: Point): Point | null {
386
- return this.intersectRect(pointA, pointB);
387
- }
388
-
389
- intersectRect(pointA: Point, pointB: Point): Point | null {
390
- const center = this.getOffsetPos();
391
- const size = this.getBBox();
392
- if (pointA.x === pointB.x && pointA.y === pointB.y) {
393
- return pointA;
394
- }
395
- const TL = { x: center.x - size.width / 2, y: center.y - size.height / 2 };
396
- const TR = { x: center.x + size.width / 2, y: center.y - size.height / 2 };
397
- const BR = { x: center.x + size.width / 2, y: center.y + size.height / 2 };
398
- const BL = { x: center.x - size.width / 2, y: center.y + size.height / 2 };
399
- let intersection = intersectLineLine(TL, TR, pointA, pointB);
400
- if (intersection.points.length) {
401
- return { x: intersection.points[0].x, y: intersection.points[0].y };
402
- }
403
- intersection = intersectLineLine(TR, BR, pointA, pointB);
404
- if (intersection.points.length) {
405
- return { x: intersection.points[0].x, y: intersection.points[0].y };
406
- }
407
- intersection = intersectLineLine(BR, BL, pointA, pointB);
408
- if (intersection.points.length) {
409
- return { x: intersection.points[0].x, y: intersection.points[0].y };
410
- }
411
- intersection = intersectLineLine(BL, TL, pointA, pointB);
412
- if (intersection.points.length) {
413
- return { x: intersection.points[0].x, y: intersection.points[0].y };
414
- }
415
- return null;
416
- }
417
-
418
- intersectRectRect(rect1: Rect, rect2: Rect): Rect {
419
- const x = Math.max(rect1.x, rect2.x);
420
- const y = Math.max(rect1.y, rect2.y);
421
- const xLimit = (rect1.x < rect2.x) ? Math.min(rect1.x + rect1.width, rect2.x + rect2.width) : Math.min(rect2.x + rect2.width, rect1.x + rect1.width);
422
- const yLimit = (rect1.y < rect2.y) ? Math.min(rect1.y + rect1.height, rect2.y + rect2.height) : Math.min(rect2.y + rect2.height, rect1.y + rect1.height);
423
- return {
424
- x,
425
- y,
426
- width: xLimit - x,
427
- height: yLimit - y
428
- };
429
- }
430
-
431
- intersectCircle(radius: number, pointA: Point, pointB: Point): Point | null {
432
- const center = this.getOffsetPos();
433
- const intersection = intersectCircleLine(center, radius, pointA, pointB);
434
- if (intersection.points.length) {
435
- return { x: intersection.points[0].x, y: intersection.points[0].y };
436
- }
437
- return null;
438
- }
439
-
440
- distance(pointA: Point, pointB: Point): number {
441
- return Math.sqrt((pointA.x - pointB.x) * (pointA.x - pointB.x) + (pointA.y - pointB.y) * (pointA.y - pointB.y));
442
- }
443
-
444
- // Download ---
445
- serializeSVG(extraStyles: string = fontAwsesomeStyle): string {
446
- const origSvg = this.locateSVGNode(this._element.node());
447
- const cloneSVG = origSvg.cloneNode(true) as SVGSVGElement;
448
- const origNodes = d3Select(origSvg).selectAll("*").nodes();
449
- d3Select(cloneSVG).selectAll("*").each(function (this: SVGElement, d, i) {
450
- const compStyles = window.getComputedStyle(origNodes[i] as SVGElement);
451
- for (let i = 0; i < compStyles.length; ++i) {
452
- const styleName = compStyles.item(i);
453
- const styleValue = compStyles.getPropertyValue(styleName);
454
- const stylePriority = compStyles.getPropertyPriority(styleName);
455
- this.style.setProperty(styleName, styleValue, stylePriority);
456
- }
457
- });
458
-
459
- if (extraStyles) {
460
- const defs = cloneSVG.getElementsByTagName("defs");
461
- if (defs.length) {
462
- const extraStyle = document.createElement("style");
463
- extraStyle.setAttribute("type", "text/css");
464
- extraStyle.innerText = extraStyles;
465
- defs[0].appendChild(extraStyle);
466
- }
467
- }
468
-
469
- const serializer = new XMLSerializer();
470
- return serializer.serializeToString(cloneSVG);
471
- }
472
-
473
- toBlob(extraStyles: string = fontAwsesomeStyle): Blob {
474
- return new Blob([this.serializeSVG(extraStyles)], { type: "image/svg+xml" });
475
- }
476
-
477
- rasterize(extraStyles: string = fontAwsesomeStyle, ...extraWidgets: SVGWidget[]): Promise<Blob> {
478
- const widgets = [this, ...extraWidgets];
479
- const sizes = widgets.map(widget => widget.locateSVGNode(widget.element().node()).getBoundingClientRect());
480
- const width = sizes.reduce((prev, curr) => prev + curr.width, 0);
481
- const height = Math.max(...sizes.map(s => s.height));
482
-
483
- const canvas = document.createElement("canvas");
484
- canvas.width = width;
485
- canvas.height = height;
486
- canvas.style.width = width + "px";
487
- const ctx = canvas.getContext("2d");
488
- ctx.fillStyle = "white";
489
- ctx.fillRect(0, 0, width, height);
490
- ctx.fillStyle = "transparent";
491
- return new Promise((resolve, reject) => {
492
- let xPos = 0;
493
- Promise.all(widgets.map((widget, i) => {
494
- const x = xPos;
495
- const y = (height - sizes[i].height) / 2;
496
- xPos += sizes[i].width;
497
- return new Promise<void>((resolve, reject) => {
498
- const image = new Image();
499
- image.onerror = reject;
500
- image.onload = () => {
501
- ctx.drawImage(image, 0, 0, sizes[i].width, sizes[i].height, x, y, sizes[i].width, sizes[i].height);
502
- resolve();
503
- };
504
- image.src = URL.createObjectURL(widget.toBlob(extraStyles));
505
- });
506
- })).then(() => {
507
- ctx.canvas.toBlob(resolve); // Not supported by Edge browser
508
- });
509
- });
510
- }
511
-
512
- downloadSVG(extraStyles: string = fontAwsesomeStyle) {
513
- downloadString("SVG", this.serializeSVG(extraStyles));
514
- }
515
-
516
- downloadPNG(filename: string = `image_${timestamp()}`, extraStyles: string = fontAwsesomeStyle, ...extraWidgets: SVGWidget[]) {
517
- this.rasterize(extraStyles, ...extraWidgets).then(blob => downloadBlob(blob, `${filename}.png`));
518
- }
519
-
520
- // IE Fixers ---
521
- _pushMarkers(element?) {
522
- if (svgMarkerGlitch) {
523
- element = element || this._element;
524
- element.selectAll("path[marker-start],path[marker-end]")
525
- .attr("fixme-start", function () { return this.getAttribute("marker-start"); })
526
- .attr("fixme-end", function () { return this.getAttribute("marker-end"); })
527
- .attr("marker-start", null)
528
- .attr("marker-end", null)
529
- ;
530
- }
531
- }
532
-
533
- _popMarkers(element?) {
534
- if (svgMarkerGlitch) {
535
- element = element || this._element;
536
- element.selectAll("path[fixme-start],path[fixme-end]")
537
- .attr("marker-start", function () {
538
- return this.getAttribute("fixme-start");
539
- })
540
- .attr("marker-end", function () { return this.getAttribute("fixme-end"); })
541
- .attr("fixme-start", null)
542
- .attr("fixme-end", null)
543
- ;
544
- }
545
- }
546
-
547
- _popMarkersDebounced = debounce(function (element) {
548
- if (svgMarkerGlitch) {
549
- this._popMarkers(element);
550
- }
551
- }, 250);
552
-
553
- _fixIEMarkers(element?) {
554
- if (svgMarkerGlitch) {
555
- this._pushMarkers(element);
556
- this._popMarkersDebounced(element);
557
- }
558
- }
559
- }
560
- SVGWidget.prototype._class += " common_SVGWidget";
561
-
562
- export interface SVGWidget {
563
- selectionGlowColor(): string;
564
- selectionGlowColor(_: string): this;
565
- }
566
-
567
- SVGWidget.prototype.publish("selectionGlowColor", "red", "html-color", "Selection Glow Color");
1
+ import { rgb as d3Rgb } from "d3-color";
2
+ import { select as d3Select } from "d3-selection";
3
+ import { fontAwsesomeStyle } from "./FAChar";
4
+ import { svgMarkerGlitch } from "./Platform";
5
+ import { Transition } from "./Transition";
6
+ import { debounce, downloadBlob, downloadString, timestamp } from "./Utility";
7
+ import { ISize, Widget } from "./Widget";
8
+
9
+ type Point = { x: number, y: number };
10
+ type Rect = { x: number, y: number, width: number, height: number };
11
+
12
+ const lerp = function (point: Point, that: Point, t: number): Point {
13
+ // From https://github.com/thelonious/js-intersections
14
+ return {
15
+ x: point.x + (that.x - point.x) * t,
16
+ y: point.y + (that.y - point.y) * t
17
+ };
18
+ };
19
+
20
+ type LineIntersection = { type: "Intersection" | "No Intersection" | "Coincident" | "Parallel", points: Point[] };
21
+ const intersectLineLine = function (a1: Point, a2: Point, b1: Point, b2: Point): LineIntersection {
22
+ // From https://github.com/thelonious/js-intersections
23
+ const result: LineIntersection = { type: "Parallel", points: [] };
24
+ const uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x);
25
+ const ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x);
26
+ const uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
27
+
28
+ if (uB !== 0) {
29
+ const ua = uaT / uB;
30
+ const ub = ubT / uB;
31
+
32
+ if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
33
+ result.type = "Intersection";
34
+ result.points.push({
35
+ x: a1.x + ua * (a2.x - a1.x),
36
+ y: a1.y + ua * (a2.y - a1.y)
37
+ });
38
+ } else {
39
+ result.type = "No Intersection";
40
+ }
41
+ } else {
42
+ if (uaT === 0 || ubT === 0) {
43
+ result.type = "Coincident";
44
+ } else {
45
+ result.type = "Parallel";
46
+ }
47
+ }
48
+
49
+ return result;
50
+ };
51
+
52
+ type CircleIntersection = { type: "Outside" | "Tangent" | "Inside" | "Intersection", points: Point[] };
53
+ const intersectCircleLine = function (c: Point, r: number, a1: Point, a2: Point): CircleIntersection {
54
+ // From https://github.com/thelonious/js-intersections
55
+ const result: CircleIntersection = { type: "Intersection", points: [] };
56
+ const a = (a2.x - a1.x) * (a2.x - a1.x) +
57
+ (a2.y - a1.y) * (a2.y - a1.y);
58
+ const b = 2 * ((a2.x - a1.x) * (a1.x - c.x) +
59
+ (a2.y - a1.y) * (a1.y - c.y));
60
+ const cc = c.x * c.x + c.y * c.y + a1.x * a1.x + a1.y * a1.y -
61
+ 2 * (c.x * a1.x + c.y * a1.y) - r * r;
62
+ const deter = b * b - 4 * a * cc;
63
+
64
+ if (deter < 0) {
65
+ result.type = "Outside";
66
+ } else if (deter === 0) {
67
+ result.type = "Tangent";
68
+ // NOTE: should calculate this point
69
+ } else {
70
+ const e = Math.sqrt(deter);
71
+ const u1 = (-b + e) / (2 * a);
72
+ const u2 = (-b - e) / (2 * a);
73
+
74
+ if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
75
+ if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
76
+ result.type = "Outside";
77
+ } else {
78
+ result.type = "Inside";
79
+ }
80
+ } else {
81
+ result.type = "Intersection";
82
+
83
+ if (0 <= u1 && u1 <= 1)
84
+ result.points.push(lerp(a1, a2, u1));
85
+
86
+ if (0 <= u2 && u2 <= 1)
87
+ result.points.push(lerp(a1, a2, u2));
88
+ }
89
+ }
90
+
91
+ return result;
92
+ };
93
+
94
+ export class SVGGlowFilter {
95
+ protected filter;
96
+ protected feOffset;
97
+ protected feColorMatrix;
98
+ protected feGaussianBlur;
99
+ protected feBlend;
100
+
101
+ constructor(target, id: string) {
102
+ this.filter = target.append("filter")
103
+ .attr("id", id)
104
+ .attr("width", "130%")
105
+ .attr("height", "130%");
106
+ this.feOffset = this.filter.append("feOffset")
107
+ .attr("result", "offOut")
108
+ .attr("in", "SourceGraphic")
109
+ .attr("dx", "0")
110
+ .attr("dy", "0");
111
+ this.feColorMatrix = this.filter.append("feColorMatrix")
112
+ .attr("result", "matrixOut")
113
+ .attr("in", "offOut")
114
+ .attr("type", "matrix")
115
+ .attr("values", this.rgb2ColorMatrix("red"))
116
+ ;
117
+ this.feGaussianBlur = this.filter.append("feGaussianBlur")
118
+ .attr("result", "blurOut")
119
+ .attr("in", "matrixOut")
120
+ .attr("stdDeviation", "3")
121
+ ;
122
+ this.feBlend = this.filter.append("feBlend")
123
+ .attr("in", "SourceGraphic")
124
+ .attr("in2", "blurOut")
125
+ .attr("mode", "normal")
126
+ ;
127
+ }
128
+
129
+ rgb2ColorMatrix(color: string): string {
130
+ const rgb = d3Rgb(color);
131
+ return [
132
+ rgb.r / 255, 0, 0, 0, rgb.r ? 1 : 0,
133
+ 0, rgb.g / 255, 0, 0, rgb.g ? 1 : 0,
134
+ 0, 0, rgb.b / 255, 0, rgb.b ? 1 : 0,
135
+ 0, 0, 0, 1, 0
136
+ ].join(" ");
137
+ }
138
+
139
+ update(color: string) {
140
+ this.feColorMatrix.attr("values", this.rgb2ColorMatrix(color));
141
+ }
142
+ }
143
+
144
+ export class SVGWidget extends Widget {
145
+ static _class = "common_SVGWidget";
146
+
147
+ _tag;
148
+
149
+ protected _boundingBox;
150
+ protected transition;
151
+ protected _drawStartPos: "center" | "origin";
152
+ protected _svgSelectionFilter;
153
+ protected _parentRelativeDiv;
154
+ protected _parentOverlay;
155
+
156
+ constructor() {
157
+ super();
158
+
159
+ this._tag = "g";
160
+
161
+ this._boundingBox = null;
162
+
163
+ this.transition = new Transition(this);
164
+
165
+ this._drawStartPos = "center";
166
+ }
167
+
168
+ // Properties ---
169
+ move(_, transitionDuration?) {
170
+ const retVal = this.pos(_);
171
+ if (arguments.length) {
172
+ (transitionDuration ? this._element.transition().duration(transitionDuration) : this._element)
173
+ .attr("transform", `translate(${_.x} ${_.y})scale(${this._widgetScale})`)
174
+ ;
175
+ }
176
+ return retVal;
177
+ }
178
+
179
+ _enableOverflow = false;
180
+ enableOverflow(): boolean;
181
+ enableOverflow(_: boolean): this;
182
+ enableOverflow(_?: boolean): boolean | this {
183
+ if (!arguments.length) return this._enableOverflow;
184
+ this._enableOverflow = _;
185
+ return this;
186
+ }
187
+
188
+ _enableOverflowScroll = true;
189
+ enableOverflowScroll(): boolean;
190
+ enableOverflowScroll(_: boolean): this;
191
+ enableOverflowScroll(_?: boolean): boolean | this {
192
+ if (!arguments.length) return this._enableOverflowScroll;
193
+ this._enableOverflowScroll = _;
194
+ return this;
195
+ }
196
+
197
+ size(): ISize;
198
+ size(_): this;
199
+ size(_?): ISize | this {
200
+ const retVal = super.size.apply(this, arguments);
201
+ if (arguments.length) {
202
+ this._boundingBox = null;
203
+ }
204
+ return retVal;
205
+ }
206
+
207
+ resize(_size?: { width: number, height: number }) {
208
+ const retVal = super.resize.apply(this, arguments);
209
+ if (this._parentRelativeDiv) {
210
+ this._parentRelativeDiv
211
+ .style("width", this._size.width + "px")
212
+ .style("height", this._size.height + "px")
213
+ ;
214
+ switch (this._drawStartPos) {
215
+ case "origin":
216
+ this.pos({
217
+ x: 0,
218
+ y: 0
219
+ });
220
+ break;
221
+ case "center":
222
+ /* falls through */
223
+ default:
224
+ this.pos({
225
+ x: this._size.width / 2,
226
+ y: this._size.height / 2
227
+ });
228
+ break;
229
+ }
230
+ }
231
+ if (!isNaN(this._size.width)) this._placeholderElement.attr("width", this._size.width);
232
+ if (!isNaN(this._size.height)) this._placeholderElement.attr("height", this._size.height);
233
+ return retVal;
234
+ }
235
+ // Glow Highlighting ---
236
+ svgGlowID(): string {
237
+ return `sel${this.id()}_glow`;
238
+ }
239
+
240
+ target(): null | HTMLElement | SVGElement;
241
+ target(_: null | string | HTMLElement | SVGElement): this;
242
+ target(_?: null | string | HTMLElement | SVGElement): null | HTMLElement | SVGElement | this {
243
+ const retVal = super.target.apply(this, arguments);
244
+ if (arguments.length) {
245
+ if (this._target instanceof SVGElement) {
246
+ this._isRootNode = false;
247
+ this._placeholderElement = d3Select(this._target);
248
+ this._parentWidget = this._placeholderElement.datum();
249
+ if (!this._parentWidget || this._parentWidget._id === this._id) {
250
+ this._parentWidget = this.locateParentWidget(this._target.parentNode);
251
+ }
252
+ this._parentOverlay = this.locateOverlayNode();
253
+ const svg = this.locateSVGNode(this._target);
254
+ const svgDefs = d3Select(svg).select<SVGDefsElement>("defs");
255
+ this._svgSelectionFilter = new SVGGlowFilter(svgDefs, this.svgGlowID());
256
+ } else if (this._target) {
257
+ // Target is a DOM Node, so create a SVG Element ---
258
+ this._parentRelativeDiv = d3Select(this._target).append("div")
259
+ .style("position", "relative")
260
+ ;
261
+ this._placeholderElement = this._parentRelativeDiv.append("svg")
262
+ .style("position", "absolute")
263
+ .style("top", "0px")
264
+ .style("left", "0px")
265
+ ;
266
+ const svgDefs = this._placeholderElement.append("defs");
267
+ this._svgSelectionFilter = new SVGGlowFilter(svgDefs, this.svgGlowID());
268
+ this._parentOverlay = this._parentRelativeDiv.append("div")
269
+ .style("position", "absolute")
270
+ .style("top", "0px")
271
+ .style("left", "0px")
272
+ ;
273
+ if (this._size.width && this._size.height) {
274
+ this.resize(this._size);
275
+ } else {
276
+ this.resize({ width: 0, height: 0 });
277
+ }
278
+ }
279
+ }
280
+ return retVal;
281
+ }
282
+
283
+ parentOverlay() {
284
+ return this._parentOverlay;
285
+ }
286
+
287
+ enter(domNode, element) {
288
+ super.enter(domNode, element);
289
+ }
290
+
291
+ update(domNode, element) {
292
+ super.update(domNode, element);
293
+ if (this._svgSelectionFilter) {
294
+ this._svgSelectionFilter.update(this.selectionGlowColor());
295
+ }
296
+ }
297
+
298
+ postUpdate(domNode, element) {
299
+ super.postUpdate(domNode, element);
300
+ let transX;
301
+ let transY;
302
+ if (this._drawStartPos === "origin" && this._target instanceof SVGElement) {
303
+ transX = (this._pos.x - this._size.width / 2);
304
+ transY = (this._pos.y - this._size.height / 2);
305
+ this._element.attr("transform", "translate(" + transX + "," + transY + ")scale(" + this._widgetScale + ")");
306
+ } else {
307
+ transX = this._pos.x;
308
+ transY = this._pos.y;
309
+ if (this._enableOverflow) {
310
+ // Individual Widgets will need to size and position themselves corrrectly (and have calculated a BBox) ---
311
+ if ((transX < 0 || transY < 0) && this._boundingBox) {
312
+ transX = transX < 0 ? 0 : transX;
313
+ transY = transY < 0 ? 0 : transY;
314
+ if (this._enableOverflowScroll) {
315
+ this._parentRelativeDiv.style("overflow", "scroll");
316
+ }
317
+ this._placeholderElement.attr("width", this._boundingBox.width);
318
+ this._placeholderElement.attr("height", this._boundingBox.height);
319
+ } else {
320
+ this._parentRelativeDiv.style("overflow", null);
321
+ }
322
+ }
323
+ this._element.attr("transform", "translate(" + transX + "," + transY + ")scale(" + this._widgetScale + ")");
324
+ }
325
+ }
326
+
327
+ exit(domNode?, element?) {
328
+ if (this._parentRelativeDiv) {
329
+ this._parentOverlay.remove();
330
+ this._placeholderElement.remove();
331
+ this._parentRelativeDiv.remove();
332
+ }
333
+ super.exit(domNode, element);
334
+ }
335
+
336
+ getOffsetPos(): Point {
337
+ let retVal = { x: 0, y: 0 };
338
+ if (this._parentWidget) {
339
+ retVal = this._parentWidget.getOffsetPos();
340
+ retVal.x += this._pos.x;
341
+ retVal.y += this._pos.y;
342
+ return retVal;
343
+ }
344
+ return retVal;
345
+ }
346
+
347
+ getBBox(refresh = false, round = false): Rect {
348
+ if (refresh || this._boundingBox === null) {
349
+ const svgNode: SVGElement = this._element.node();
350
+ if (svgNode instanceof SVGElement) {
351
+ this._boundingBox = (svgNode as any).getBBox();
352
+ }
353
+ }
354
+ if (this._boundingBox === null) {
355
+ return {
356
+ x: 0,
357
+ y: 0,
358
+ width: 0,
359
+ height: 0
360
+ };
361
+ }
362
+ return {
363
+ x: (round ? Math.round(this._boundingBox.x) : this._boundingBox.x) * this._widgetScale,
364
+ y: (round ? Math.round(this._boundingBox.y) : this._boundingBox.y) * this._widgetScale,
365
+ width: (round ? Math.round(this._boundingBox.width) : this._boundingBox.width) * this._widgetScale,
366
+ height: (round ? Math.round(this._boundingBox.height) : this._boundingBox.height) * this._widgetScale
367
+ };
368
+ }
369
+
370
+ // Intersections ---
371
+ contains(point: Point): boolean {
372
+ return this.containsRect(point);
373
+ }
374
+
375
+ containsRect(point: Point): boolean {
376
+ const size = this.getBBox();
377
+ return point.x >= size.x && point.x <= size.x + size.width && point.y >= size.y && point.y <= size.y + size.height;
378
+ }
379
+
380
+ containsCircle(radius: number, point: Point) {
381
+ const center = this.getOffsetPos();
382
+ return this.distance(center, point) <= radius;
383
+ }
384
+
385
+ intersection(pointA: Point, pointB: Point): Point | null {
386
+ return this.intersectRect(pointA, pointB);
387
+ }
388
+
389
+ intersectRect(pointA: Point, pointB: Point): Point | null {
390
+ const center = this.getOffsetPos();
391
+ const size = this.getBBox();
392
+ if (pointA.x === pointB.x && pointA.y === pointB.y) {
393
+ return pointA;
394
+ }
395
+ const TL = { x: center.x - size.width / 2, y: center.y - size.height / 2 };
396
+ const TR = { x: center.x + size.width / 2, y: center.y - size.height / 2 };
397
+ const BR = { x: center.x + size.width / 2, y: center.y + size.height / 2 };
398
+ const BL = { x: center.x - size.width / 2, y: center.y + size.height / 2 };
399
+ let intersection = intersectLineLine(TL, TR, pointA, pointB);
400
+ if (intersection.points.length) {
401
+ return { x: intersection.points[0].x, y: intersection.points[0].y };
402
+ }
403
+ intersection = intersectLineLine(TR, BR, pointA, pointB);
404
+ if (intersection.points.length) {
405
+ return { x: intersection.points[0].x, y: intersection.points[0].y };
406
+ }
407
+ intersection = intersectLineLine(BR, BL, pointA, pointB);
408
+ if (intersection.points.length) {
409
+ return { x: intersection.points[0].x, y: intersection.points[0].y };
410
+ }
411
+ intersection = intersectLineLine(BL, TL, pointA, pointB);
412
+ if (intersection.points.length) {
413
+ return { x: intersection.points[0].x, y: intersection.points[0].y };
414
+ }
415
+ return null;
416
+ }
417
+
418
+ intersectRectRect(rect1: Rect, rect2: Rect): Rect {
419
+ const x = Math.max(rect1.x, rect2.x);
420
+ const y = Math.max(rect1.y, rect2.y);
421
+ const xLimit = (rect1.x < rect2.x) ? Math.min(rect1.x + rect1.width, rect2.x + rect2.width) : Math.min(rect2.x + rect2.width, rect1.x + rect1.width);
422
+ const yLimit = (rect1.y < rect2.y) ? Math.min(rect1.y + rect1.height, rect2.y + rect2.height) : Math.min(rect2.y + rect2.height, rect1.y + rect1.height);
423
+ return {
424
+ x,
425
+ y,
426
+ width: xLimit - x,
427
+ height: yLimit - y
428
+ };
429
+ }
430
+
431
+ intersectCircle(radius: number, pointA: Point, pointB: Point): Point | null {
432
+ const center = this.getOffsetPos();
433
+ const intersection = intersectCircleLine(center, radius, pointA, pointB);
434
+ if (intersection.points.length) {
435
+ return { x: intersection.points[0].x, y: intersection.points[0].y };
436
+ }
437
+ return null;
438
+ }
439
+
440
+ distance(pointA: Point, pointB: Point): number {
441
+ return Math.sqrt((pointA.x - pointB.x) * (pointA.x - pointB.x) + (pointA.y - pointB.y) * (pointA.y - pointB.y));
442
+ }
443
+
444
+ // Download ---
445
+ serializeSVG(extraStyles: string = fontAwsesomeStyle): string {
446
+ const origSvg = this.locateSVGNode(this._element.node());
447
+ const cloneSVG = origSvg.cloneNode(true) as SVGSVGElement;
448
+ const origNodes = d3Select(origSvg).selectAll("*").nodes();
449
+ d3Select(cloneSVG).selectAll("*").each(function (this: SVGElement, d, i) {
450
+ const compStyles = window.getComputedStyle(origNodes[i] as SVGElement);
451
+ for (let i = 0; i < compStyles.length; ++i) {
452
+ const styleName = compStyles.item(i);
453
+ const styleValue = compStyles.getPropertyValue(styleName);
454
+ const stylePriority = compStyles.getPropertyPriority(styleName);
455
+ this.style.setProperty(styleName, styleValue, stylePriority);
456
+ }
457
+ });
458
+
459
+ if (extraStyles) {
460
+ const defs = cloneSVG.getElementsByTagName("defs");
461
+ if (defs.length) {
462
+ const extraStyle = document.createElement("style");
463
+ extraStyle.setAttribute("type", "text/css");
464
+ extraStyle.innerText = extraStyles;
465
+ defs[0].appendChild(extraStyle);
466
+ }
467
+ }
468
+
469
+ const serializer = new XMLSerializer();
470
+ return serializer.serializeToString(cloneSVG);
471
+ }
472
+
473
+ toBlob(extraStyles: string = fontAwsesomeStyle): Blob {
474
+ return new Blob([this.serializeSVG(extraStyles)], { type: "image/svg+xml" });
475
+ }
476
+
477
+ rasterize(extraStyles: string = fontAwsesomeStyle, ...extraWidgets: SVGWidget[]): Promise<Blob> {
478
+ const widgets = [this, ...extraWidgets];
479
+ const sizes = widgets.map(widget => widget.locateSVGNode(widget.element().node()).getBoundingClientRect());
480
+ const width = sizes.reduce((prev, curr) => prev + curr.width, 0);
481
+ const height = Math.max(...sizes.map(s => s.height));
482
+
483
+ const canvas = document.createElement("canvas");
484
+ canvas.width = width;
485
+ canvas.height = height;
486
+ canvas.style.width = width + "px";
487
+ const ctx = canvas.getContext("2d");
488
+ ctx.fillStyle = "white";
489
+ ctx.fillRect(0, 0, width, height);
490
+ ctx.fillStyle = "transparent";
491
+ return new Promise((resolve, reject) => {
492
+ let xPos = 0;
493
+ Promise.all(widgets.map((widget, i) => {
494
+ const x = xPos;
495
+ const y = (height - sizes[i].height) / 2;
496
+ xPos += sizes[i].width;
497
+ return new Promise<void>((resolve, reject) => {
498
+ const image = new Image();
499
+ image.onerror = reject;
500
+ image.onload = () => {
501
+ ctx.drawImage(image, 0, 0, sizes[i].width, sizes[i].height, x, y, sizes[i].width, sizes[i].height);
502
+ resolve();
503
+ };
504
+ image.src = URL.createObjectURL(widget.toBlob(extraStyles));
505
+ });
506
+ })).then(() => {
507
+ ctx.canvas.toBlob(resolve); // Not supported by Edge browser
508
+ });
509
+ });
510
+ }
511
+
512
+ downloadSVG(extraStyles: string = fontAwsesomeStyle) {
513
+ downloadString("SVG", this.serializeSVG(extraStyles));
514
+ }
515
+
516
+ downloadPNG(filename: string = `image_${timestamp()}`, extraStyles: string = fontAwsesomeStyle, ...extraWidgets: SVGWidget[]) {
517
+ this.rasterize(extraStyles, ...extraWidgets).then(blob => downloadBlob(blob, `${filename}.png`));
518
+ }
519
+
520
+ // IE Fixers ---
521
+ _pushMarkers(element?) {
522
+ if (svgMarkerGlitch) {
523
+ element = element || this._element;
524
+ element.selectAll("path[marker-start],path[marker-end]")
525
+ .attr("fixme-start", function () { return this.getAttribute("marker-start"); })
526
+ .attr("fixme-end", function () { return this.getAttribute("marker-end"); })
527
+ .attr("marker-start", null)
528
+ .attr("marker-end", null)
529
+ ;
530
+ }
531
+ }
532
+
533
+ _popMarkers(element?) {
534
+ if (svgMarkerGlitch) {
535
+ element = element || this._element;
536
+ element.selectAll("path[fixme-start],path[fixme-end]")
537
+ .attr("marker-start", function () {
538
+ return this.getAttribute("fixme-start");
539
+ })
540
+ .attr("marker-end", function () { return this.getAttribute("fixme-end"); })
541
+ .attr("fixme-start", null)
542
+ .attr("fixme-end", null)
543
+ ;
544
+ }
545
+ }
546
+
547
+ _popMarkersDebounced = debounce(function (element) {
548
+ if (svgMarkerGlitch) {
549
+ this._popMarkers(element);
550
+ }
551
+ }, 250);
552
+
553
+ _fixIEMarkers(element?) {
554
+ if (svgMarkerGlitch) {
555
+ this._pushMarkers(element);
556
+ this._popMarkersDebounced(element);
557
+ }
558
+ }
559
+ }
560
+ SVGWidget.prototype._class += " common_SVGWidget";
561
+
562
+ export interface SVGWidget {
563
+ selectionGlowColor(): string;
564
+ selectionGlowColor(_: string): this;
565
+ }
566
+
567
+ SVGWidget.prototype.publish("selectionGlowColor", "red", "html-color", "Selection Glow Color");