neo.mjs 6.3.10 → 6.4.0

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 (39) hide show
  1. package/apps/ServiceWorker.mjs +2 -2
  2. package/examples/ConfigurationViewport.mjs +21 -9
  3. package/examples/ServiceWorker.mjs +2 -2
  4. package/examples/container/dialog/MainContainerController.mjs +20 -16
  5. package/examples/dialog/DemoDialog.mjs +24 -3
  6. package/examples/form/field/select/MainContainer.mjs +28 -6
  7. package/package.json +1 -1
  8. package/resources/scss/src/component/Base.scss +26 -0
  9. package/resources/scss/src/form/field/Picker.scss +0 -3
  10. package/resources/scss/src/menu/List.scss +4 -8
  11. package/resources/scss/theme-dark/menu/List.scss +2 -1
  12. package/resources/scss/theme-light/menu/List.scss +1 -0
  13. package/src/DefaultConfig.mjs +2 -2
  14. package/src/button/Base.mjs +41 -6
  15. package/src/component/Base.mjs +236 -44
  16. package/src/container/Dialog.mjs +3 -3
  17. package/src/core/Base.mjs +20 -0
  18. package/src/form/Container.mjs +2 -0
  19. package/src/form/field/Picker.mjs +30 -47
  20. package/src/form/field/Time.mjs +8 -8
  21. package/src/form/field/trigger/Base.mjs +1 -1
  22. package/src/form/field/trigger/CopyToClipboard.mjs +5 -1
  23. package/src/grid/View.mjs +5 -2
  24. package/src/grid/header/Button.mjs +10 -10
  25. package/src/list/Base.mjs +17 -14
  26. package/src/list/plugin/Animate.mjs +3 -3
  27. package/src/main/DomAccess.mjs +272 -28
  28. package/src/menu/List.mjs +35 -102
  29. package/src/table/Container.mjs +2 -2
  30. package/src/table/View.mjs +1 -0
  31. package/src/table/header/Button.mjs +21 -23
  32. package/src/tree/Accordion.mjs +1 -1
  33. package/src/util/Array.mjs +4 -18
  34. package/src/util/Css.mjs +6 -8
  35. package/src/util/HashHistory.mjs +10 -3
  36. package/src/util/Rectangle.mjs +444 -7
  37. package/test/siesta/siesta-node.js +2 -1
  38. package/test/siesta/siesta.js +1 -0
  39. package/test/siesta/tests/Rectangle.mjs +409 -0
@@ -9,11 +9,11 @@ import TextField from '../../form/field/Text.mjs';
9
9
  class Button extends BaseButton {
10
10
  /**
11
11
  * Valid values for align
12
- * @member {String[]} alignValues: ['left','center','right']
12
+ * @member {String[]} cellAlignValues: ['left','center','right']
13
13
  * @protected
14
14
  * @static
15
15
  */
16
- static alignValues = ['left', 'center', 'right']
16
+ static cellAlignValues = ['left', 'center', 'right']
17
17
 
18
18
  static config = {
19
19
  /**
@@ -26,15 +26,15 @@ class Button extends BaseButton {
26
26
  * @protected
27
27
  */
28
28
  ntype: 'table-header-button',
29
- /**
30
- * Alignment of the matching table cells. Valid values are left, center, right
31
- * @member {String} align_='left'
32
- */
33
- align_: 'left',
34
29
  /**
35
30
  * @member {String[]} baseCls=['neo-table-header-button']
36
31
  */
37
32
  baseCls: ['neo-table-header-button'],
33
+ /**
34
+ * Alignment of the matching table cells. Valid values are left, center, right
35
+ * @member {String} cellAlign_='left'
36
+ */
37
+ cellAlign_: 'left',
38
38
  /**
39
39
  * @member {String|null} dataField=null
40
40
  */
@@ -111,19 +111,17 @@ class Button extends BaseButton {
111
111
  construct(config) {
112
112
  super.construct(config);
113
113
 
114
- let me = this;
115
-
116
- if (me.draggable) {
117
- me.addDomListeners({
118
- dragend : me.onDragEnd,
119
- dragenter: me.onDragEnter,
120
- dragleave: me.onDragLeave,
121
- dragover : me.onDragOver,
122
- dragstart: me.onDragStart,
123
- drop : me.onDrop,
124
- scope : me
125
- });
126
- }
114
+ let me = this;
115
+
116
+ me.draggable && me.addDomListeners({
117
+ dragend : me.onDragEnd,
118
+ dragenter: me.onDragEnter,
119
+ dragleave: me.onDragLeave,
120
+ dragover : me.onDragOver,
121
+ dragstart: me.onDragStart,
122
+ drop : me.onDrop,
123
+ scope : me
124
+ })
127
125
  }
128
126
 
129
127
  /**
@@ -255,13 +253,13 @@ class Button extends BaseButton {
255
253
  }
256
254
 
257
255
  /**
258
- * Triggered before the align config gets changed
256
+ * Triggered before the cellAlign config gets changed
259
257
  * @param {String} value
260
258
  * @param {String} oldValue
261
259
  * @protected
262
260
  */
263
- beforeSetAlign(value, oldValue) {
264
- return this.beforeSetEnumValue(value, oldValue, 'align', 'alignValues');
261
+ beforeSetCellAlign(value, oldValue) {
262
+ return this.beforeSetEnumValue(value, oldValue, 'cellAlign', 'cellAlignValues');
265
263
  }
266
264
 
267
265
  /**
@@ -266,7 +266,7 @@ class AccordionTree extends TreeList {
266
266
  cn : [{
267
267
  flag : 'iconCls',
268
268
  tag : 'span',
269
- cls : ['neo-accordion-item-icon', item[me.fields.icon]],
269
+ cls : itemIconCls,
270
270
  id : id + '__icon',
271
271
  removeDom: (!item.isLeaf || !me.showIcon)
272
272
  }, {
@@ -132,29 +132,15 @@ class NeoArray extends Base {
132
132
  }
133
133
 
134
134
  /**
135
- * Returns an array of items which are present in array1 and array2
135
+ * Returns an array of items which are present in the passed arrays.
136
+ * Multiple arrays may be passed.
136
137
  * Only supports primitive items
137
138
  * @param {Array} array1
138
139
  * @param {Array} array2
139
140
  * @returns {Array}
140
141
  */
141
- static union(array1, array2) {
142
- let result = [],
143
- merge = array1.concat(array2),
144
- len = merge.length,
145
- assoc = {},
146
- item;
147
-
148
- while (len--) {
149
- item = merge[len];
150
-
151
- if (!assoc[item]) {
152
- result.unshift(item);
153
- assoc[item] = true;
154
- }
155
- }
156
-
157
- return result;
142
+ static union() {
143
+ return [...new Set(Array.prototype.concat(...arguments))];
158
144
  }
159
145
 
160
146
  /**
package/src/util/Css.mjs CHANGED
@@ -15,29 +15,27 @@ class Css extends Base {
15
15
 
16
16
  /**
17
17
  * Pass the selectorText of the rules which you want to remove
18
+ * @param {String} appName
18
19
  * @param {String[]|String} rules
19
20
  */
20
- static deleteRules(rules) {
21
+ static deleteRules(appName, rules) {
21
22
  if (!Array.isArray(rules)) {
22
23
  rules = [rules];
23
24
  }
24
25
 
25
- Neo.main.addon.Stylesheet.deleteCssRules({
26
- rules: rules
27
- });
26
+ Neo.main.addon.Stylesheet.deleteCssRules({appName, rules})
28
27
  }
29
28
 
30
29
  /**
30
+ * @param {String} appName
31
31
  * @param {String[]|String} rules
32
32
  */
33
- static insertRules(rules) {
33
+ static insertRules(appName, rules) {
34
34
  if (!Array.isArray(rules)) {
35
35
  rules = [rules];
36
36
  }
37
37
 
38
- Neo.main.addon.Stylesheet.insertCssRules({
39
- rules: rules
40
- });
38
+ Neo.main.addon.Stylesheet.insertCssRules({appName, rules})
41
39
  }
42
40
  }
43
41
 
@@ -41,14 +41,14 @@ class HashHistory extends Base {
41
41
  * @returns {Object}
42
42
  */
43
43
  first() {
44
- return this.stack[0];
44
+ return this.stack[0] || null
45
45
  }
46
46
 
47
47
  /**
48
48
  * @returns {Number}
49
49
  */
50
50
  getCount() {
51
- return this.stack.length;
51
+ return this.stack.length
52
52
  }
53
53
 
54
54
  /**
@@ -65,12 +65,19 @@ class HashHistory extends Base {
65
65
  stack.unshift(data);
66
66
 
67
67
  if (stack.length > me.maxItems) {
68
- stack.pop();
68
+ stack.pop()
69
69
  }
70
70
 
71
71
  me.fire('change', data, stack[1] || null)
72
72
  }
73
73
  }
74
+
75
+ /**
76
+ * @returns {Object}
77
+ */
78
+ second() {
79
+ return this.stack[1] || null
80
+ }
74
81
  }
75
82
 
76
83
  let instance = Neo.applyClassConfig(HashHistory);
@@ -1,11 +1,84 @@
1
- import Base from '../core/Base.mjs';
2
-
3
1
  /**
4
2
  * The class contains utility methods for working with DOMRect Objects
5
3
  * @class Neo.util.Rectangle
6
- * @extends Neo.core.Base
4
+ * @extends DOMRect
7
5
  */
8
- class Rectangle extends Base {
6
+
7
+ const
8
+ emptyArray = Object.freeze([]),
9
+ // Convert edge array values into the [T,R,B,L] form.
10
+ parseEdgeValue = (e = 0) => {
11
+ if (!Array.isArray(e)) {
12
+ e = [e];
13
+ }
14
+ switch (e.length) {
15
+ case 1:
16
+ e.length = 4;
17
+ return e.fill(e[0], 1, 4);
18
+ case 2:// top&bottom, left&right
19
+ return [e[0], e[1], e[0], e[1]];
20
+ case 3:// top, left&right, bottom
21
+ return [e[0], e[1], e[2], e[1]];
22
+ }
23
+ return e;
24
+ },
25
+ parseEdgeAlign = edgeAlign => {
26
+ const
27
+ edgeParts = edgeAlignRE.exec(edgeAlign),
28
+ ourEdgeZone = edgeZone[edgeParts[1]],
29
+ theirEdgeZone = edgeZone[edgeParts[4]];
30
+
31
+ return {
32
+ ourEdge : edgeParts[1],
33
+ ourEdgeOffset : parseInt(edgeParts[2] || 50),
34
+ ourEdgeUnit : edgeParts[3] || '%',
35
+ ourEdgeZone,
36
+ theirEdge : edgeParts[4],
37
+ theirEdgeOffset : parseInt(edgeParts[5] || 50),
38
+ theirEdgeUnit : edgeParts[6] || '%',
39
+ theirEdgeZone,
40
+
41
+ // Aligned to an edge, *outside* of the target.
42
+ // A normal align as a combo dropdown might request
43
+ edgeAligned : (ourEdgeZone & 1) === (theirEdgeZone & 1) && ourEdgeZone !== theirEdgeZone
44
+ }
45
+ },
46
+ // The opposite of parseEdgeAlign, and it has to flip the edges
47
+ createReversedEdgeAlign = edges => {
48
+ const
49
+ ourEdge = oppositeEdge[edges.ourEdge],
50
+ theirEdge = oppositeEdge[edges.theirEdge];
51
+
52
+ // reconstitute a rule string with the edges flipped to the opposite sides
53
+ return `${ourEdge}${edges.ourEdgeOffset}${edges.ourEdgeUnit}-${theirEdge}${edges.theirEdgeOffset}${edges.theirEdgeUnit}`
54
+
55
+ },
56
+ getElRect = el => {
57
+ const r = el instanceof DOMRect ? el : (el?.nodeType === 1 ? el : typeof el === 'string' ? document.getElementById(el) : null)?.getBoundingClientRect();
58
+
59
+ // Convert DOMRect into Rectangle
60
+ return r && new Rectangle(r.x, r.y, r.width, r.height);
61
+ },
62
+ oppositeEdge = {
63
+ t : 'b',
64
+ r : 'l',
65
+ b : 't',
66
+ l : 'r'
67
+ },
68
+ edgeZone = {
69
+ t : 0,
70
+ r : 1,
71
+ b : 2,
72
+ l : 3
73
+ },
74
+ zoneNames = ['top', 'right', 'bottom', 'left'],
75
+ zoneEdges = ['t', 'r', 'b', 'l'],
76
+ zoneDimension = ['width', 'height'],
77
+ zoneCoord = [0, 1, 0, 1],
78
+ zeroMargins = [0, 0, 0, 0],
79
+ edgeAlignRE = /^([trblc])(\d*)(%|px)?-([trblc])(\d*)(%|px)?$/;
80
+
81
+ export default class Rectangle extends DOMRect {
9
82
  static config = {
10
83
  /**
11
84
  * @member {String} className='Neo.util.Rectangle'
@@ -147,8 +220,372 @@ class Rectangle extends Base {
147
220
 
148
221
  return movedRect;
149
222
  }
150
- }
151
223
 
152
- Neo.applyClassConfig(Rectangle);
224
+ set bottom(b) {
225
+ this.height += b - this.bottom;
226
+ }
227
+ get bottom() {
228
+ return super.bottom;
229
+ }
230
+
231
+ set right(r) {
232
+ this.width += r - this.right;
233
+ }
234
+ get right() {
235
+ return super.right;
236
+ }
237
+
238
+ // Change the x without moving the Rectangle. The left side moves and the right side doesn't
239
+ changeX(x) {
240
+ const widthDelta = this.x - x;
241
+
242
+ this.x = x;
243
+ this.width += widthDelta;
244
+ }
245
+
246
+ // Change the y without moving the Rectangle. The top side moves and the bottom side doesn't
247
+ changeY(y) {
248
+ const heightDelta = this.y - y;
249
+
250
+ this.y = y;
251
+ this.height += heightDelta;
252
+ }
253
+
254
+ clone() {
255
+ return Rectangle.clone(this);
256
+ }
257
+
258
+ static clone(r) {
259
+ const result = new Rectangle(r.x, r.y, r.width, r.height);
260
+
261
+ result.minWidth = r.minWidth;
262
+ result.minHeight = r.minHeight;
263
+
264
+ return result;
265
+ }
266
+
267
+ intersects(other) {
268
+ const me = this;
269
+
270
+ if (other.height && other.width) {
271
+ const
272
+ left = Math.max(me.x, other.x),
273
+ top = Math.max(me.y, other.y),
274
+ right = Math.min(me.x + me.width, other.x + other.width),
275
+ bottom = Math.min(me.y + me.height, other.y + other.height);
276
+
277
+ if (left >= right || top >= bottom) {
278
+ return false;
279
+ }
280
+
281
+ return new Rectangle(left, top, right - left, bottom - top);
282
+ }
283
+ // We're dealing with a point here - zero dimensions
284
+ else {
285
+ return (other.x >= me.x && other.y >= me.y && other.right <= me.right && other.bottom <= me.bottom);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Checks if the other Rectangle is fully contained inside this Rectangle
291
+ * @param {Object} other
292
+ * @returns {Boolean}
293
+ */
294
+ contains(other) {
295
+ return this.bottom >= other.bottom
296
+ && this.left <= other.left
297
+ && this.right >= other.right
298
+ && this.top <= other.top;
299
+ }
300
+
301
+ /**
302
+ * Returns a clone of this Rectangle expanded according to the edges array.
303
+ * @param {Number}Number[]} edges
304
+ * @returns
305
+ */
306
+ expand(edges) {
307
+ edges = parseEdgeValue(edges);
308
+
309
+ return new this.constructor(this.x - edges[3], this.y - edges[0], this.width + edges[1] + edges[3], this.height + edges[0] + edges[2]);
310
+ }
153
311
 
154
- export default Rectangle;
312
+ moveBy(x = 0, y = 0) {
313
+ const result = this.clone();
314
+
315
+ if (Array.isArray(x)) {
316
+ y = x[1];
317
+ x = x[0];
318
+ }
319
+ result.x += x;
320
+ result.y += y;
321
+ return result;
322
+ }
323
+
324
+ /**
325
+ * Returns `true` if this Rectangle completely contains the other Rectangle
326
+ * @param {Rectangle} other
327
+ */
328
+ contains(other) {
329
+ return this.constructor.includes(this, other);
330
+ }
331
+
332
+ /**
333
+ * Returns a copy of this Rectangle constrained to fit within the passed Rectangle
334
+ * @param {Rectangle} constrainTo
335
+ * @returns {Rectangle|Boolean} A new Rectangle constrained to te passed Rectangle, or false if it could not be constrained.
336
+ */
337
+ constrainTo(constrainTo) {
338
+ const
339
+ me = this,
340
+ minWidth = me.minWidth || me.width,
341
+ minHeight = me.minHeight || me.height;
342
+
343
+ // Not possible, even when shrunk to minima
344
+ if (minHeight > constrainTo.height || minWidth > constrainTo.width) {
345
+ return false;
346
+ }
347
+
348
+ // We do not mutate this Rectangle, but return a constrained version
349
+ const result = me.clone();
350
+
351
+ // Translate result so that the top and left are visible
352
+ result.x = Math.max(me.x + Math.min(constrainTo.right - result.right, 0), constrainTo.x);
353
+ result.y = Math.max(me.y + Math.min(constrainTo.bottom - result.bottom, 0), constrainTo.y);
354
+
355
+ // Pull in any resulting overflow
356
+ result.bottom = Math.min(result.bottom, constrainTo.bottom);
357
+ result.right = Math.min(result.right, constrainTo.right);
358
+
359
+ return result;
360
+ }
361
+
362
+ alignTo(align) {
363
+ const
364
+ me = this,
365
+ {
366
+ minWidth,
367
+ minHeight
368
+ } = me,
369
+ {
370
+ constrainTo, // Element or Rectangle result must fit into
371
+ target, // Element or Rectangle to align to
372
+ edgeAlign, // t50-b50 type string
373
+ axisLock, // true for flip, 'flexible' for flip, then try the other edges
374
+ offset, // Final [x, y] vector to move the result by.
375
+ matchSize
376
+ } = align,
377
+ targetMargin = align.targetMargin ? parseEdgeValue(align.targetMargin) : zeroMargins,
378
+ targetRect = getElRect(target),
379
+ constrainRect = getElRect(constrainTo),
380
+ edges = parseEdgeAlign(edgeAlign),
381
+ matchDimension = zoneDimension[edges.theirEdgeZone & 1];
382
+
383
+ let result = me.clone();
384
+
385
+ if (matchSize) {
386
+ result[matchDimension] = targetRect[matchDimension];
387
+ }
388
+
389
+ // Must do the calculations after the aligned side has been matched in size if requested.
390
+ const
391
+ myPoint = result.getAnchorPoint(edges.ourEdgeZone, edges.ourEdgeOffset, edges.ourEdgeUnit),
392
+ targetPoint = targetRect.getAnchorPoint(edges.theirEdgeZone, edges.theirEdgeOffset, edges.theirEdgeUnit, targetMargin),
393
+ vector = [targetPoint[0] - myPoint[0], targetPoint[1] - myPoint[1]];
394
+
395
+ result = result.moveBy(vector);
396
+
397
+ // A useful property in the resulting rectangle which specifies which zone of the target
398
+ // It is being places in, T,R,B or L - 0, 1, 2, 3
399
+ // Some code may want to treat DOM elements differently depending on the zone
400
+ result.zone = edges.theirEdgeZone;
401
+ result.position = zoneNames[result.zone];
402
+
403
+ // Now we create the four Rectangles around the target, into which we may be constrained
404
+ // Zones T,R,B,L 0 9, 1, 2, 3:
405
+ // +-----------------------------------------------------------------------------------+
406
+ // | +-------------------------+------------------------+----------------------------+ |
407
+ // | | ^ | | ^ | |
408
+ // | | | | | | | |
409
+ // | | <-------+--------------+---------Zone 0---------+-------------+----------> | |
410
+ // | | | | | | | |
411
+ // | | | | | | | |
412
+ // | +----------+--------------+------------------------+-------------+--------------+ |
413
+ // | | | | +--------------------+ | | | |
414
+ // | | | | | | | | | |
415
+ // | | | | | | | | | |
416
+ // | | Zone 3 | | | | Zone 1 | |
417
+ // | | | | | | | | | |
418
+ // | | | | | | | | | |
419
+ // | | | | +--------------------+ | | | |
420
+ // | ++---------+--------------+------------------------+-------------+--------------+ |
421
+ // | | | | | | | |
422
+ // | | | | | | | |
423
+ // | | | | | | | |
424
+ // | | <-------+--------------+--------Zone 2----------+-------------+------------> | |
425
+ // | | | | | | | |
426
+ // | | v | | v | |
427
+ // | ++------------------------+------------------------+----------------------------+ |
428
+ // +-----------------------------------------------------------------------------------+
429
+ if (constrainRect && !constrainRect.contains(result)) {
430
+ // They asked to overlap the target, for example t0-t0
431
+ // In these cases, we just return the result
432
+ if (targetRect.intersects(result)) {
433
+ return result;
434
+ }
435
+
436
+ // This is the zone we try to fit into first, the one that was asked for
437
+ let zone = edges.theirEdgeZone;
438
+
439
+ // We create an array of four rectangles into which we try to fit with appropriate align specs.
440
+ // We must start with the requested zone, whatever that is.
441
+ const zonesToTry = [{
442
+ zone,
443
+ edgeAlign
444
+ }];
445
+
446
+ if (axisLock) {
447
+ // Flip to the opposite side for the second try.
448
+ // The alignment string has to be reversed
449
+ // so r20-l30 has to become l20-r30.
450
+ // The other two zones revert to centered so are easier
451
+ zonesToTry[1] = {
452
+ zone : zone = (zone + 2) % 4,
453
+ edgeAlign : createReversedEdgeAlign(edges)
454
+ }
455
+
456
+ // Fall back to the other two zones if we are allowed to
457
+ if (axisLock === 'flexible') {
458
+ zonesToTry.push({
459
+ zone : zone = (alignSpec.startZone + 1) % 4,
460
+ edgeAlign : `${oppositeEdge[zoneEdges[zone]]}-${zoneEdges[zone]}`
461
+ });
462
+ zonesToTry.push({
463
+ zone : zone = (zone + 2) % 4,
464
+ edgeAlign : `${oppositeEdge[zoneEdges[zone]]}-${zoneEdges[zone]}`
465
+ });
466
+ }
467
+ }
468
+ else {
469
+ // go through the other zones in order
470
+ for (let i = 1; i < 4; i++) {
471
+ zonesToTry.push({
472
+ zone : zone = (zone + 1) % 4,
473
+ edgeAlign : `${oppositeEdge[zoneEdges[zone]]}-${zoneEdges[zone]}`
474
+ });
475
+ }
476
+ }
477
+
478
+ // Calculate the constraint Rectangle for each zone
479
+ for (let i = 0; i < zonesToTry.length; i++) {
480
+ // We clone the outer constraining rectangle
481
+ // and move it into position
482
+ const c = constrainRect.clone();
483
+
484
+ switch (zonesToTry[i].zone) {
485
+ case 0:
486
+ // The zone i2 above the target - zone 0/T
487
+ c.bottom = targetRect.y - targetMargin[0];
488
+ break;
489
+ case 1:
490
+ // The zone is to the right of the target - zone 1/R
491
+ c.changeX(targetRect.right + targetMargin[1]);
492
+ break;
493
+ case 2:
494
+ // The zone is below the target - zone 2/B
495
+ c.changeY(targetRect.bottom + targetMargin[2]);
496
+ break;
497
+ case 3:
498
+ // The zone is to the left of the target - zone 3/L
499
+ c.right = targetRect.x - targetMargin[3];
500
+ break;
501
+ }
502
+ zonesToTry[i].constrainRect = c;
503
+ }
504
+
505
+ // Now try to constrain our result into each zone's constraintZone
506
+ for (let i = 0; i < zonesToTry.length; i++) {
507
+ const
508
+ {
509
+ zone,
510
+ edgeAlign,
511
+ constrainRect
512
+ } = zonesToTry[i],
513
+ edge = zoneEdges[zone];
514
+
515
+ if (matchSize) {
516
+ // If we are aligning to the requested edge, or it's opposite edge then
517
+ // match that edge size, else revert it to our own size
518
+ result[matchDimension] = edge === edges.theirEdge || edge == oppositeEdge[edges.theirEdge] ? targetRect[matchDimension] : me[matchDimension];
519
+ }
520
+
521
+ // Do a simple align to the current edge
522
+ result = result.alignTo({
523
+ target : targetRect,
524
+ edgeAlign,
525
+ targetMargin
526
+ });
527
+
528
+ let solution = result.constrainTo(constrainRect);
529
+
530
+ // As soon as we find a zone into which the result is willing to be constrained. return it
531
+ if (solution) {
532
+ solution.zone = zone;
533
+ solution.position = zoneNames[zone];
534
+ return solution;
535
+ }
536
+ }
537
+ }
538
+
539
+ return result;
540
+ }
541
+
542
+ getAnchorPoint(edgeZone, edgeOffset, edgeUnit, margin = emptyArray) {
543
+ const me = this;
544
+
545
+ let result;
546
+
547
+ // Edge zones go top, right, bottom, left
548
+ // Each one calculates the start point of that edge then moves along it by
549
+ // the edgeOffset, then moves *away* from it by the margin for that edge if there's a margin.
550
+ switch (edgeZone) {
551
+ case 0:
552
+ result = [me.x, me.y - (margin[0] || 0), me.width, 0];
553
+ break;
554
+ case 1:
555
+ result = [me.x + me.width + (margin[1] || 0), me.y, me.height, 1];
556
+ break;
557
+ case 2:
558
+ result = [me.x, me.y + me.height + (margin[2] || 0), me.width, 0];
559
+ break;
560
+ case 3:
561
+ result = [me.x - (margin[3] || 0), me.y, me.height, 1];
562
+ }
563
+ result[result[3]] += edgeUnit === '%' ? result[2] / 100 * edgeOffset : edgeOffset;
564
+ result.length = 2;
565
+ return result;
566
+ }
567
+
568
+ equals(other) {
569
+ return other instanceof DOMRect &&
570
+ other.x === this.x &&
571
+ other.y === this.y &&
572
+ other.height === this.height &&
573
+ other.width === this.width;
574
+ }
575
+
576
+ // For debugging purposes only
577
+ show(color = 'red') {
578
+ const div = document.createElement('div');
579
+
580
+ div.style = `
581
+ position:absolute;
582
+ transform:translate3d(${this.x}px, ${this.y}px, 0);
583
+ height:${this.height}px;
584
+ width:${this.width}px;
585
+ background-color:${color}
586
+ `;
587
+ document.body.appendChild(div);
588
+ setTimeout(() => div.remove(), 30000);
589
+ return div;
590
+ }
591
+ }
@@ -31,7 +31,8 @@ project.configure({
31
31
 
32
32
  project.plan(
33
33
  './tests/CollectionBase.mjs',
34
- './tests/VdomHelper.mjs'
34
+ './tests/VdomHelper.mjs',
35
+ './tests/Rectangle.mjs'
35
36
  );
36
37
 
37
38
  project.start();
@@ -20,6 +20,7 @@ project.plan(
20
20
  'tests/ClassConfigsAndFields.mjs',
21
21
  'tests/ClassSystem.mjs',
22
22
  'tests/CollectionBase.mjs',
23
+ 'tests/Rectangle.mjs',
23
24
  'tests/VdomHelper.mjs',
24
25
  'tests/VdomCalendar.mjs'
25
26
  );