bm-core-ui 2.11.8 → 2.11.9-beta.2

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  import {YES, NO, BMExtend, BMCopyProperties} from '../Core/BMCoreUI'
4
4
  import {BMPointMake} from '../Core/BMPoint'
5
- import {BMRectMake, BMRectMakeWithNodeFrame} from '../Core/BMRect'
5
+ import {BMRectMake, BMRectMakeWithNodeFrame, BMRectMakeWithOrigin} from '../Core/BMRect'
6
6
  import {BMAnimateWithBlock, BMAnimationContextGetCurrent, BMAnimationContextEnableWebAnimations, __BMVelocityAnimate, BMHook} from '../Core/BMAnimationContext'
7
7
  import {BMView, BMViewColorScheme} from '../BMView/BMView_v2.5'
8
8
  import {BMLayoutOrientation} from '../BMView/BMLayoutSizeClass'
@@ -132,16 +132,60 @@ class BMWindowOverlay extends BMView {
132
132
 
133
133
  // @type BMWindow extends BMView
134
134
 
135
+ /**
136
+ * The duration, in milliseconds, for most window animations.
137
+ */
135
138
  var _BMWindowAnimationDurationDefault = 400;
139
+
140
+ /**
141
+ * The easing to use for most window animations.
142
+ */
136
143
  var _BMWindowAnimationEasingDefault = 'easeInOutQuart';
144
+
145
+ /**
146
+ * The easing to use for the default window appearing animation.
147
+ */
137
148
  const _BMWindowAnimationEasingIncomingDefault = 'easeOutQuart';
149
+
150
+ /**
151
+ * The duration, in milliseconds, to use for the default window appearing animation.
152
+ */
138
153
  const _BMWindowAnimationDurationIncomingDefault = 200;
154
+
155
+ /**
156
+ * The easing to use for the default window disappearing animation.
157
+ */
139
158
  const _BMWindowAnimationEasingOutgoingDefault = 'easeInQuart';
159
+
160
+ /**
161
+ * The duration, in milliseconds, to use for the default window disappearing animation.
162
+ */
140
163
  const _BMWindowAnimationDurationOutgoingDefault = 200;
141
164
 
165
+ /**
166
+ * The delay, in milliseconds, to wait until hiding the minimize strip after the mouse pointer
167
+ * moves away from the bottom of the screen.
168
+ */
169
+ const _BMWindowMinimzeStripHideDelay = 1000;
170
+
171
+ // The maximum iterations allowed for the window showcase since it's never guaranteed that
172
+ // the algorithm can finish within a known number of iterations; in practice it's rare that
173
+ // this value will ever be reached.
142
174
  const BM_WINDOW_SHOWCASE_MAX_ITERATIONS = 5000;
175
+
176
+ // The maximum z-index to use for the topmost window.
143
177
  var BM_WINDOW_Z_INDEX_MAX = 2007;
144
178
 
179
+ // A flag that controls the size of a minimized window.
180
+ const BM_WINDOW_MINIMIZED_SIZE = 160;
181
+
182
+ // A flag that controls the spacing between the edges of the window
183
+ // and the minimized window
184
+ const BM_WINDOW_MINIMIZED_EDGE_SPACING = 30;
185
+
186
+ // A flag that controls the spacing between minimized windows
187
+ const BM_WINDOW_MINIMIZED_SPACING = 16;
188
+
145
189
  /**
146
190
  * A window is an object that manages the display and lifecycle of a popup window.
147
191
  */
@@ -156,6 +200,8 @@ BMWindow._windows = [];
156
200
  // An array containing all minimized non-modal window DOM nodes
157
201
  BMWindow._minimizedWindows = [];
158
202
 
203
+ // A weak map containing the association between minimized window nodes and window objects
204
+ BMWindow._minimizedWindowMap = new WeakMap;
159
205
 
160
206
  /**
161
207
  * Returns the z-index value that the topmost non-modal window will have.
@@ -169,6 +215,221 @@ BMWindow.zIndexMax = function () {
169
215
  return BM_WINDOW_Z_INDEX_MAX;
170
216
  }
171
217
 
218
+ /**
219
+ * Updates the position of all animated windows based on how many there are.
220
+ * @param animated <Boolean, nullable> Defaults to `YES`. If set to `YES`, this change will be animated.
221
+ */
222
+ BMWindow._updateMinimizedWindowsAnimated = function (animated = YES) {
223
+ const length = this._minimizedWindows.length * BM_WINDOW_MINIMIZED_SIZE;
224
+ const spacing = (this._minimizedWindows.length - 1) * BM_WINDOW_MINIMIZED_SPACING;
225
+
226
+ let left = window.innerWidth / 2 - (length + spacing) / 2;
227
+
228
+ if (animated) {
229
+ BMAnimateWithBlock(() => {
230
+ BMAnimationContextEnableWebAnimations();
231
+
232
+ for (const minimizedWindow of this._minimizedWindows) {
233
+ const controller = BMAnimationContextGetCurrent().controllerForObject(minimizedWindow, {node: minimizedWindow});
234
+ controller.registerBuiltInProperty('left', {withValue: left + 'px'});
235
+ controller.registerBuiltInProperty('top', {withValue: window.innerHeight - BM_WINDOW_MINIMIZED_SIZE + 'px'});
236
+
237
+ // Determine where the window thumbnail should be positioned
238
+ const targetWindow = this._minimizedWindowMap.get(minimizedWindow);
239
+ const minimizedSize = targetWindow._minimizedSize;
240
+ const minimizedFrame = BMRectMake(left, window.innerHeight - BM_WINDOW_MINIMIZED_SIZE, BM_WINDOW_MINIMIZED_SIZE, BM_WINDOW_MINIMIZED_SIZE - BM_WINDOW_MINIMIZED_EDGE_SPACING);
241
+ const targetFrame = BMRectMakeWithOrigin(BMPointMake(0, 0), {size: minimizedSize});
242
+ targetFrame.center = minimizedFrame.center;
243
+ targetFrame.origin.y = minimizedFrame.bottom - targetFrame.height;
244
+
245
+ // Move the window thumbnail to the appropriate position
246
+ const windowFrame = targetWindow._fullScreen ? BMRectMake(0, 0, window.innerWidth, window.innerHeight) : targetWindow.frame;
247
+ const transformRect = windowFrame.rectWithTransformToRect(targetFrame);
248
+ const windowController = BMAnimationContextGetCurrent().controllerForObject(targetWindow, {node: targetWindow.node});
249
+ windowController.registerBuiltInPropertiesWithDictionary({translateX: transformRect.left + 'px', translateY: transformRect.top + 'px', scaleX: transformRect.width, scaleY: transformRect.height});
250
+
251
+ left += BM_WINDOW_MINIMIZED_SIZE + BM_WINDOW_MINIMIZED_SPACING;
252
+ }
253
+ }, {
254
+ duration: _BMWindowAnimationDurationDefault,
255
+ easing: _BMWindowAnimationEasingDefault
256
+ });
257
+ }
258
+ else {
259
+ for (const minimizedWindow of this._minimizedWindows) {
260
+ // Move the container to the appropriate slot
261
+ BMHook(minimizedWindow, {left: left + 'px', top: window.innerHeight - BM_WINDOW_MINIMIZED_SIZE + 'px'});
262
+
263
+ // Determine where the window thumbnail should be positioned
264
+ const targetWindow = this._minimizedWindowMap.get(minimizedWindow);
265
+ const minimizedSize = targetWindow._minimizedSize;
266
+ const minimizedFrame = BMRectMake(left, window.innerHeight - BM_WINDOW_MINIMIZED_SIZE, BM_WINDOW_MINIMIZED_SIZE, BM_WINDOW_MINIMIZED_SIZE - BM_WINDOW_MINIMIZED_EDGE_SPACING);
267
+ const targetFrame = BMRectMakeWithOrigin(BMPointMake(0, 0), {size: minimizedSize});
268
+ targetFrame.center = minimizedFrame.center;
269
+
270
+ // Move the window thumbnail to the appropriate position
271
+ const windowFrame = targetWindow._fullScreen ? BMRectMake(0, 0, window.innerWidth, window.innerHeight) : targetWindow.frame;
272
+ const transformRect = windowFrame.rectWithTransformToRect(targetFrame);
273
+ BMHook(targetWindow.node, {translateX: transformRect.left + 'px', translateY: transformRect.top + 'px', scaleX: transformRect.width, scaleY: transformRect.height});
274
+
275
+ left += BM_WINDOW_MINIMIZED_SIZE + BM_WINDOW_MINIMIZED_SPACING;
276
+ }
277
+ }
278
+
279
+ if (this._minimizedWindows.length) {
280
+ // If there are any minimized windows, set up an event handler that shows and hides the minimize strip based on the pointer's position
281
+ this._startMinimizeStripAutohide();
282
+ }
283
+ else {
284
+ // If there are no remaining windows, stop monitoring the pointer's position
285
+ this._stopMinimizeStripAutohide();
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Set to `YES` while monitoring the mouse pointer to autohide the minimized windows strip.
291
+ */
292
+ BMWindow._minimizeAutohideActive = NO;
293
+
294
+ /**
295
+ * Sets up event handlers that automatically hide the minimize strip after the mouse pointer moves away
296
+ * from the bottom of the screen for a certain amount of time. For touch devices, this will retain the top part
297
+ * of the minimize strip so that it can be invoked by tapping it. For mouse devices, this will completely hide
298
+ * the minimize strip.
299
+ *
300
+ * Additionally, sets up event handlers to reposition the minimize strip whenever the browser window resized.
301
+ */
302
+ BMWindow._startMinimizeStripAutohide = function () {
303
+ // If handlers are already set up, don't take any action
304
+ if (BMWindow._minimizeAutohideActive) return;
305
+ BMWindow._minimizeAutohideActive = YES;
306
+
307
+ window.addEventListener('mousemove', BMWindow._mouseDidMoveWithEvent);
308
+ window.addEventListener('resize', BMWindow._viewportDidResizeWithEvent);
309
+ }
310
+
311
+ /**
312
+ * Brings the minimize strip back into view and stops monitoring the mouse pointer to autohide it.
313
+ */
314
+ BMWindow._stopMinimizeStripAutohide = function () {
315
+ // If handlers are not set up, don't take any action
316
+ if (BMWindow._minimizeAutohideActive) return;
317
+ BMWindow._minimizeAutohideActive = NO;
318
+
319
+ window.removeEventListener('mousemove', BMWindow._mouseDidMoveWithEvent);
320
+ window.removeEventListener('resize', BMWindow._viewportDidResizeWithEvent);
321
+ }
322
+
323
+ /**
324
+ * Invoked whenever the viewport resizes while the minimize strip is visible. Updates the position
325
+ * of the minimize strip and its windows to match the new viewport size.
326
+ * @param event <Event> The event that triggered this action.
327
+ */
328
+ BMWindow._viewportDidResizeWithEvent = function (event) {
329
+ BMWindow._updateMinimizedWindowsAnimated(NO);
330
+ }
331
+
332
+ /**
333
+ * Invoked whenever the mouse pointer moves while the minimize strip is visible. Tests if the mouse pointer moves
334
+ * sufficiently far away from the strip and hides it if it does, or shows the minimize strip if the mouse pointer
335
+ * moves sufficiently close to the bottom of the viewport.
336
+ * @param event <MouseEvent> The event that triggered this action.
337
+ */
338
+ BMWindow._mouseDidMoveWithEvent = function (event) {
339
+ const position = BMPointMake(event.clientX, event.clientY);
340
+ const visibleHeight = BM_WINDOW_MINIMIZED_SIZE + BM_WINDOW_MINIMIZED_SPACING;
341
+
342
+ // The rect outside of which the pointer must remain in order for the strip to be hidden
343
+ const retainVisibilityRect = BMRectMake(0, window.innerHeight - visibleHeight, window.innerWidth, visibleHeight);
344
+
345
+ // The rect inside of which the pointer must be in order for the strip to become visible
346
+ const showRect = BMRectMake(0, window.innerHeight - BM_WINDOW_MINIMIZED_SPACING, window.innerWidth, BM_WINDOW_MINIMIZED_SPACING);
347
+
348
+ // Show the minimize strip as soon as the mouse pointer intersects the show rect
349
+ if (showRect.intersectsPoint(position) && BMWindow._minimizeStripHidden) {
350
+ BMWindow.showMinimizeStripAnimated();
351
+ }
352
+
353
+ if (!retainVisibilityRect.intersectsPoint(position)) {
354
+ if (!BMWindow._minimizeStripHidden) {
355
+ // If the pointer stops intersecting the retain visibility rect, start a timer that hides
356
+ // the minimize strip after a delay unless the mouse pointer is brought back
357
+ if (BMWindow._minimizeStripHideTimeout) return;
358
+
359
+ BMWindow._minimizeStripHideTimeout = window.setTimeout(() => {
360
+ BMWindow.hideMinimizeStripAnimated();
361
+ BMWindow._minimizeStripHideTimeout = undefined;
362
+ }, _BMWindowMinimzeStripHideDelay);
363
+ }
364
+ }
365
+ else {
366
+ // If the pointer starts intersecting the retain visibility rect before the timer elapses,
367
+ // cancel it and require the mouse to exit again to hide the strip
368
+ if (BMWindow._minimizeStripHideTimeout) {
369
+ window.clearTimeout(BMWindow._minimizeStripHideTimeout);
370
+ BMWindow._minimizeStripHideTimeout = undefined;
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Shows the minimize strip if it is hidden.
377
+ * @param animated <Boolean, nullable> Defaults to `YES`. If set to `YES`, this change will be animated.
378
+ * @returns <Promise<void>> A promise that resolves when the associated animation completes.
379
+ */
380
+ BMWindow.showMinimizeStripAnimated = async function (animated) {
381
+ // Wait for the entire timeout to hide the strip again
382
+ if (BMWindow._minimizeStripHideTimeout) {
383
+ window.clearTimeout(BMWindow._minimizeStripHideTimeout);
384
+ BMWindow._minimizeStripHideTimeout = undefined;
385
+ }
386
+
387
+ if (!BMWindow._minimizeStripHidden) return;
388
+ BMWindow._minimizeStripHidden = NO;
389
+
390
+ document.body.classList.remove('BMWindowMinimizeStripHidden');
391
+ document.body.classList.add('BMWindowMinimizeStripShown');
392
+
393
+ // After the shown animation ends, remove the animation class
394
+ if (!document.body.getAnimations) return;
395
+
396
+ if (BMWindow._minimizedWindows.length) {
397
+ // Immediately commit the animation
398
+ window.getComputedStyle(BMWindow._minimizedWindows[0]);
399
+
400
+ const animation = BMWindow._minimizedWindows[0].getAnimations()[0];
401
+ if (animation) {
402
+ await animation.finished;
403
+ document.body.classList.remove('BMWindowMinimizeStripShown');
404
+ }
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Hides the minimize strip if it is visible.
410
+ * @param animated <Boolean, nullable> Defaults to `YES`. If set to `YES`, this change will be animated.
411
+ */
412
+ BMWindow.hideMinimizeStripAnimated = function () {
413
+ if (BMWindow._minimizeStripHidden) return;
414
+ BMWindow._minimizeStripHidden = YES;
415
+
416
+ // Before hiding the minimize strip, add the appropraite classes to all minimize elements in order
417
+ // to play the minimize animation
418
+ for (const node of BMWindow._minimizedWindows) {
419
+ if (!node.classList.contains('BMWindowMinimizedControl')) {
420
+ node.classList.add('BMWindowMinimizedControl');
421
+ }
422
+
423
+ const window = BMWindow._minimizedWindowMap.get(node);
424
+ if (window && !window.node.classList.contains('BMWindowMinimizedThumbnail')) {
425
+ window.node.classList.add('BMWindowMinimizedThumbnail');
426
+ }
427
+ }
428
+
429
+ document.body.classList.add('BMWindowMinimizeStripHidden');
430
+ document.body.classList.remove('BMWindowMinimizeStripShown');
431
+ }
432
+
172
433
  /**
173
434
  * Minimizes all non-minimized windows.
174
435
  * @param animated <Boolean, nullable> Defaults to `YES`. If set to `YES`, this change will be animated.
@@ -610,6 +871,7 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
610
871
  * A string that is used for when this window is minimized.
611
872
  */
612
873
  _title: 'New Window', // <String>
874
+
613
875
  get title() {
614
876
  return this._title;
615
877
  },
@@ -958,7 +1220,6 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
958
1220
  dragHandle.style.cursor = 'nwse-resize';
959
1221
  node.appendChild(dragHandle);
960
1222
 
961
-
962
1223
  // Initialize dragging touch events for the drag handler
963
1224
  dragHandle.addEventListener('mousedown', event => {
964
1225
 
@@ -1106,6 +1367,60 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1106
1367
  }
1107
1368
  },
1108
1369
 
1370
+ /**
1371
+ * @protected
1372
+ * Invoked whenever a drag operation began for this window.
1373
+ * @param position <BMPoint> The position where the drag began, relative to the viewport.
1374
+ * {
1375
+ * @param withEvent <MouseEvent | TouchEvent> The event that triggered this action.
1376
+ * }
1377
+ */
1378
+ dragBeganAtPosition(position, {withEvent: event}) {
1379
+ event.preventDefault();
1380
+ },
1381
+
1382
+ /**
1383
+ * @protected
1384
+ * Invoked whenever the pointer position changes during a drag event. Displaces this window's
1385
+ * frame by the amount the event pointer has moved.
1386
+ * @param fromPosition <BMPoint> The previous pointer position relative to the viewport.
1387
+ * {
1388
+ * @param toPosition <BMPoint> The current pointer position relative to the viewport.
1389
+ * @param event <MouseEvent | TouchEvent> The event that triggered this action.
1390
+ * }
1391
+ */
1392
+ dragPositionDidChangeFromPosition(fromPosition, {toPosition, event}) {
1393
+ const currentPosition = BMPointMake(this.leftConstraint.constant, this.topConstraint.constant);
1394
+ const newPosition = BMPointMake(
1395
+ currentPosition.x + toPosition.x - fromPosition.x,
1396
+ currentPosition.y + toPosition.y - fromPosition.y
1397
+ );
1398
+
1399
+ // Check if the window can move
1400
+ let canMove = YES;
1401
+ if (this.delegate && this.delegate.windowShouldMove) {
1402
+ canMove = this.delegate.windowShouldMove(this, newPosition);
1403
+ }
1404
+ if (!canMove) return;
1405
+
1406
+ this.leftConstraint.constant = newPosition.x;
1407
+ this.topConstraint.constant = newPosition.y;
1408
+ this.layout();
1409
+ event.preventDefault();
1410
+ },
1411
+
1412
+ /**
1413
+ * @protected
1414
+ * Invoked whenever a drag operation ended for this window.
1415
+ * @param position <BMPoint> The position where the drag began, relative to the viewport.
1416
+ * {
1417
+ * @param withEvent <MouseEvent | TouchEvent> The event that triggered this action.
1418
+ * }
1419
+ */
1420
+ dragEndedAtPosition(position, {withEvent: event}) {
1421
+ // Intentionally blank
1422
+ },
1423
+
1109
1424
  /**
1110
1425
  * Starts a drag event from the specified `mousedown` event that will move this window.
1111
1426
  * When this method is invoked, further mouse events will be blocked until the drag finishes.
@@ -1120,37 +1435,25 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1120
1435
 
1121
1436
  this._dragged = YES;
1122
1437
 
1123
- this._position = BMPointMake(this.node.offsetLeft, this.node.offsetTop);
1124
1438
  let lastPosition = BMPointMake(event.clientX, event.clientY);
1125
1439
 
1126
1440
  let mouseMoveEventListener = event => {
1127
1441
  let position = BMPointMake(event.clientX, event.clientY);
1128
- const newPosition = BMPointMake(this._position.x + position.x - lastPosition.x, this._position.y + position.y - lastPosition.y);
1129
-
1130
- // Check if the window can move
1131
- let canMove = YES;
1132
- if (this.delegate && this.delegate.windowShouldMove) {
1133
- canMove = this.delegate.windowShouldMove(this, newPosition);
1134
- }
1135
- if (!canMove) return;
1136
-
1137
- this.leftConstraint.constant = this._position.x + position.x - lastPosition.x;
1138
- this.topConstraint.constant = this._position.y + position.y - lastPosition.y;
1139
- this._position = newPosition;
1442
+ this.dragPositionDidChangeFromPosition(lastPosition, {toPosition: position, event});
1140
1443
  lastPosition = position;
1141
- this.layout();
1142
- event.preventDefault();
1143
1444
  };
1144
1445
 
1145
1446
  let mouseUpEventListener = event => {
1146
1447
  window.removeEventListener('mousemove', mouseMoveEventListener, YES);
1147
1448
  window.removeEventListener('mouseup', mouseUpEventListener, YES);
1449
+
1450
+ this.dragEndedAtPosition(lastPosition.copy(), {withEvent: event});
1148
1451
  }
1149
1452
 
1150
1453
  window.addEventListener('mousemove', mouseMoveEventListener, YES);
1151
1454
  window.addEventListener('mouseup', mouseUpEventListener, YES);
1152
1455
 
1153
- event.preventDefault();
1456
+ this.dragBeganAtPosition(lastPosition.copy(), {withEvent: event});
1154
1457
  },
1155
1458
 
1156
1459
  /**
@@ -1181,7 +1484,6 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1181
1484
  this._touchDragPoint = event.changedTouches[0].identifier;
1182
1485
  this._dragged = YES;
1183
1486
 
1184
- this._position = BMPointMake(this.node.offsetLeft, this.node.offsetTop);
1185
1487
  let lastPosition = BMPointMake(event.changedTouches[0].clientX, event.changedTouches[0].clientY);
1186
1488
 
1187
1489
  let mouseMoveEventListener = event => {
@@ -1198,21 +1500,8 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1198
1500
  if (!touch) return;
1199
1501
 
1200
1502
  let position = BMPointMake(touch.clientX, touch.clientY);
1201
- const newPosition = BMPointMake(this._position.x + position.x - lastPosition.x, this._position.y + position.y - lastPosition.y);
1202
-
1203
- // Check if the window can move
1204
- let canMove = YES;
1205
- if (this.delegate && this.delegate.windowShouldMove) {
1206
- canMove = this.delegate.windowShouldMove(this, newPosition);
1207
- }
1208
- if (!canMove) return;
1209
-
1210
- this.leftConstraint.constant = this._position.x + position.x - lastPosition.x;
1211
- this.topConstraint.constant = this._position.y + position.y - lastPosition.y;
1212
- this._position = newPosition;
1503
+ this.dragPositionDidChangeFromPosition(lastPosition, {toPosition: position, event});
1213
1504
  lastPosition = position;
1214
- this.layout();
1215
- event.preventDefault();
1216
1505
  };
1217
1506
 
1218
1507
  let mouseUpEventListener = event => {
@@ -1220,13 +1509,15 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1220
1509
  window.removeEventListener('touchmove', mouseMoveEventListener);
1221
1510
  window.removeEventListener('touchend', mouseUpEventListener);
1222
1511
  window.removeEventListener('touchcancel', mouseUpEventListener);
1512
+
1513
+ this.dragEndedAtPosition(lastPosition.copy(), {withEvent: event});
1223
1514
  }
1224
1515
 
1225
1516
  window.addEventListener('touchmove', mouseMoveEventListener);
1226
1517
  window.addEventListener('touchend', mouseUpEventListener);
1227
1518
  window.removeEventListener('touchcancel', mouseUpEventListener);
1228
1519
 
1229
- event.preventDefault();
1520
+ this.dragBeganAtPosition(lastPosition.copy(), {withEvent: event});
1230
1521
  },
1231
1522
 
1232
1523
  // @override - BMView
@@ -1714,6 +2005,22 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1714
2005
 
1715
2006
  },
1716
2007
 
2008
+ /**
2009
+ * The size that this should be when minimized.
2010
+ */
2011
+ get _minimizedSize() { // <BMSize>
2012
+ const minimizedSize = BMSizeMake(BM_WINDOW_MINIMIZED_SIZE, BM_WINDOW_MINIMIZED_SIZE - BM_WINDOW_MINIMIZED_EDGE_SPACING);
2013
+ const aspectRatio = minimizedSize.width / minimizedSize.height;
2014
+ if (this.frame.size.width / this.frame.size.height > aspectRatio) {
2015
+ minimizedSize.height = minimizedSize.height * (this.frame.size.height / this.frame.size.width) * aspectRatio;
2016
+ }
2017
+ else {
2018
+ minimizedSize.width = minimizedSize.width * (this.frame.size.width / this.frame.size.height) / aspectRatio;
2019
+ }
2020
+
2021
+ return minimizedSize;
2022
+ },
2023
+
1717
2024
  /**
1718
2025
  * Minimizes this window. This method will raise an error if this window is modal.
1719
2026
  * If this window is already minimized, this method will have no effect.
@@ -1731,10 +2038,30 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1731
2038
  this.delegate.windowWillMinimize(this);
1732
2039
  }
1733
2040
 
2041
+ this.resignKeyWindow();
2042
+
2043
+ // Temporarily show the minimize strip while minimizing a window
2044
+ BMWindow.showMinimizeStripAnimated();
2045
+
2046
+ this.node.classList.add('BMWindowMinimizedPreview');
2047
+
2048
+ // Determine the minimized size
2049
+ const minimizedSize = this._minimizedSize;
2050
+
1734
2051
  let minimizedWindow = document.createElement('div');
1735
2052
  minimizedWindow.className = 'BMWindowMinimized';
1736
- minimizedWindow.innerText = this.title;
1737
- minimizedWindow.style.left = 2 + BMWindow._minimizedWindows.length * 258 + 'px';
2053
+
2054
+ const minimizedWindowTitle = document.createElement('span');
2055
+ minimizedWindowTitle.innerText = this.title;
2056
+ minimizedWindow.appendChild(minimizedWindowTitle);
2057
+
2058
+ minimizedWindow.style.left =
2059
+ // Move to the center of the screen
2060
+ (window.innerWidth / 2) +
2061
+ // At the end of the current minimized windows
2062
+ (BMWindow._minimizedWindows.length * BM_WINDOW_MINIMIZED_SIZE + (BMWindow._minimizedWindows.length - 1) * BM_WINDOW_MINIMIZED_SPACING) / 2 +
2063
+ // Then displace current minimized windows by half the current window size and add in the spacing
2064
+ BM_WINDOW_MINIMIZED_SPACING / 2 - BM_WINDOW_MINIMIZED_SIZE / 2 + 'px';
1738
2065
  minimizedWindow.addEventListener('click', event => {
1739
2066
  if (event.altKey) {
1740
2067
  BMWindow.restoreAllAnimated(YES);
@@ -1745,6 +2072,9 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1745
2072
  });
1746
2073
 
1747
2074
  BMWindow._minimizedWindows.push(minimizedWindow);
2075
+ BMWindow._minimizedWindowMap.set(minimizedWindow, this);
2076
+
2077
+ BMWindow._updateMinimizedWindowsAnimated();
1748
2078
 
1749
2079
  document.body.appendChild(minimizedWindow);
1750
2080
 
@@ -1757,50 +2087,43 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1757
2087
  var node = minimizedWindow;
1758
2088
 
1759
2089
  var rect = BMRectMakeWithNodeFrame(node);
1760
- var animationNode = node.cloneNode(YES);
1761
-
1762
- animationNode.style.position = 'fixed';
1763
- animationNode.style.zIndex = BM_WINDOW_Z_INDEX_MAX + 1;
1764
- animationNode.style.transform = '';
1765
- BMCopyProperties(animationNode.style, {left: rect.origin.x + 'px', top: rect.origin.y + 'px', width: rect.size.width + 'px', height: rect.size.height + 'px', margin: 0});
1766
-
1767
- // Disable pointer events on the new view so that hover states do not cause the layer to be redrawn mid-animation
1768
- animationNode.style.pointerEvents = 'none';
2090
+ rect.size.height -= BM_WINDOW_MINIMIZED_EDGE_SPACING;
1769
2091
 
1770
2092
  // Compute the transform and apply it first to the window
1771
2093
  // and then to the animation node, but inversed
1772
2094
  var frame = self._fullScreen ? BMRectMake(0, 0, window.innerWidth, window.innerHeight) : self.frame;
1773
- var transformRect = frame.rectWithTransformToRect(rect);
1774
-
1775
- BMHook(animationNode, {translateX: -transformRect.origin.x + 'px', translateY: -transformRect.origin.y + 'px',
1776
- scaleX: 1 / transformRect.size.width, scaleY: 1 / transformRect.size.height, opacity: 0});
1777
-
1778
- document.body.appendChild(animationNode);
1779
- node.style.display = 'none';
1780
-
1781
- __BMVelocityAnimate(this._window, {opacity: 0, translateX: transformRect.origin.x + 'px', translateY: transformRect.origin.y + 'px',
1782
- scaleX: transformRect.size.width, scaleY: transformRect.size.height}, {
1783
- duration: _BMWindowAnimationDurationDefault,
1784
- easing: _BMWindowAnimationEasingDefault,
1785
- display: 'none'
1786
- }, YES);
2095
+ const targetFrame = BMRectMakeWithOrigin(BMPointMake(0, 0), {size: minimizedSize});
2096
+ targetFrame.center = rect.center;
2097
+ targetFrame.origin.y = window.innerHeight - BM_WINDOW_MINIMIZED_EDGE_SPACING - targetFrame.size.height;
2098
+ var transformRect = frame.rectWithTransformToRect(targetFrame);
2099
+
2100
+ BMHook(node, {opacity: 0});
2101
+
2102
+ this.node.style.pointerEvents = 'none';
2103
+ this.node.inert = YES;
1787
2104
 
1788
- // After finishing the animation, remove the temporary node and restore the original node's display
1789
- __BMVelocityAnimate(animationNode, {opacity: [1, 0], translateX: ['0px', -transformRect.origin.x + 'px'], translateY: ['0px', -transformRect.origin.y + 'px'],
1790
- scaleX: [1, 1 / transformRect.size.width], scaleY: [1, 1 / transformRect.size.height]}, {
1791
- duration: _BMWindowAnimationDurationDefault,
1792
- easing: _BMWindowAnimationEasingDefault,
1793
- complete: function () {
1794
- node.style.display = 'block';
1795
- animationNode.remove();
2105
+ __BMVelocityAnimate(this._window, {
2106
+ translateX: transformRect.origin.x + 'px',
2107
+ translateY: transformRect.origin.y + 'px',
2108
+ scaleX: transformRect.size.width,
2109
+ scaleY: transformRect.size.height
2110
+ }, {
2111
+ duration: _BMWindowAnimationDurationDefault,
2112
+ easing: _BMWindowAnimationEasingDefault,
2113
+ complete: function () {
1796
2114
 
1797
- if (self.delegate && self.delegate.windowDidMinimize) {
1798
- self.delegate.windowDidMinimize(self);
1799
- }
2115
+ if (self.delegate && self.delegate.windowDidMinimize) {
2116
+ self.delegate.windowDidMinimize(self);
2117
+ }
1800
2118
 
1801
- if (args && args.completionHandler) args.completionHandler();
1802
- }
1803
- }, YES);
2119
+ if (args && args.completionHandler) args.completionHandler();
2120
+ }
2121
+ }, YES);
2122
+
2123
+ const title = minimizedWindow.querySelector('span');
2124
+ __BMVelocityAnimate(title, {translateX: ['0px', '-30px']}, {duration: _BMWindowAnimationDurationDefault, easing: _BMWindowAnimationEasingDefault}, YES);
2125
+
2126
+ __BMVelocityAnimate(node, {opacity: 1}, {duration: _BMWindowAnimationDurationDefault, easing: _BMWindowAnimationEasingDefault}, YES);
1804
2127
  }
1805
2128
  },
1806
2129
 
@@ -1821,71 +2144,82 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
1821
2144
  this.delegate.windowWillRestore(this);
1822
2145
  }
1823
2146
 
2147
+ // Prevent this window from reacting to minimize strip autohide animations
2148
+ this.node.classList.remove('BMWindowMinimizedThumbnail');
2149
+
2150
+ // Temporarily show the minimize strip while minimizing a window
2151
+ BMWindow.showMinimizeStripAnimated();
2152
+
1824
2153
  let self = this;
1825
2154
 
1826
2155
  if (animated) {
1827
- // Create a copy of the given node which will be used in the animation in place of the original node
1828
- var node = this._minimizedWindow;
1829
- var rect = BMRectMakeWithNodeFrame(node);
1830
- var animationNode = node.cloneNode(YES);
1831
-
1832
- this._minimizedWindow = undefined;
1833
- let removedWindow = NO;
1834
- for (let i = 0, length = BMWindow._minimizedWindows.length, windows = BMWindow._minimizedWindows; i < length; i++) {
1835
- if (windows[i] == node) {
1836
- windows.splice(i, 1);
1837
- i--;
1838
- length--;
1839
- removedWindow = YES;
1840
- continue;
1841
- }
2156
+ // Create a copy of the given node which will be used in the animation in place of the original node
2157
+ var node = this._minimizedWindow;
2158
+ var rect = BMRectMakeWithNodeFrame(node);
2159
+ var animationNode = node.cloneNode(YES);
1842
2160
 
1843
- if (removedWindow) {
1844
- windows[i].style.left = 2 + 258 * i + 'px';
1845
- }
2161
+ this._minimizedWindow = undefined;
2162
+ let removedWindow = NO;
2163
+ for (let i = 0, length = BMWindow._minimizedWindows.length, windows = BMWindow._minimizedWindows; i < length; i++) {
2164
+ if (windows[i] == node) {
2165
+ windows.splice(i, 1);
2166
+ i--;
2167
+ length--;
2168
+ removedWindow = YES;
2169
+ break;
1846
2170
  }
2171
+ }
1847
2172
 
1848
- if (!this.isKeyWindow) this.becomeKeyWindow();
1849
-
1850
- animationNode.style.position = 'fixed';
1851
- animationNode.style.zIndex = BM_WINDOW_Z_INDEX_MAX + 1;
1852
- animationNode.style.transform = '';
1853
- BMCopyProperties(animationNode.style, {left: rect.origin.x + 'px', top: rect.origin.y + 'px', width: rect.size.width + 'px', height: rect.size.height + 'px', margin: 0});
1854
-
1855
- // Disable pointer events on the new view so that hover states do not cause the layer to be redrawn mid-animation
1856
- animationNode.style.pointerEvents = 'none';
1857
-
1858
- // Compute the transform and apply it first to the window
1859
- // and then to the animation node, but inversed
1860
- var frame = self._fullScreen ? BMRectMake(0, 0, window.innerWidth, window.innerHeight) : self.frame;
1861
- var transformRect = frame.rectWithTransformToRect(rect);
1862
-
1863
- document.body.appendChild(animationNode);
1864
- node.style.display = 'none';
1865
-
1866
- __BMVelocityAnimate(this._window, {opacity: 1, translateX: ['0px', transformRect.origin.x + 'px'], translateY: ['0px', transformRect.origin.y + 'px'],
1867
- scaleX: [1, transformRect.size.width], scaleY: [1, transformRect.size.height]}, {
1868
- duration: _BMWindowAnimationDurationDefault,
1869
- easing: _BMWindowAnimationEasingDefault,
1870
- display: 'block'
1871
- }, YES);
1872
-
1873
- // After finishing the animation, remove the temporary node and restore the original node's display
1874
- __BMVelocityAnimate(animationNode, {opacity: [0, 1], translateX: -transformRect.origin.x + 'px', translateY: -transformRect.origin.y + 'px',
1875
- scaleX: 1 / transformRect.size.width, scaleY: 1 / transformRect.size.height}, {
1876
- duration: _BMWindowAnimationDurationDefault,
1877
- easing: _BMWindowAnimationEasingDefault,
1878
- complete: function () {
1879
- animationNode.remove();
1880
- node.remove();
1881
-
1882
- if (self.delegate && self.delegate.windowDidRestore) {
1883
- self.delegate.windowDidRestore(self);
1884
- }
1885
-
1886
- if (args && args.completionHandler) args.completionHandler();
1887
- }
1888
- }, YES);
2173
+ BMWindow._updateMinimizedWindowsAnimated();
2174
+
2175
+ if (!this.isKeyWindow) this.becomeKeyWindow();
2176
+
2177
+ animationNode.style.position = 'fixed';
2178
+ animationNode.style.zIndex = BM_WINDOW_Z_INDEX_MAX + 1;
2179
+ animationNode.style.transform = '';
2180
+ BMCopyProperties(animationNode.style, {left: rect.origin.x + 'px', top: rect.origin.y + 'px', width: rect.size.width + 'px', height: rect.size.height + 'px', margin: 0});
2181
+
2182
+ // Disable pointer events on the new view so that hover states do not cause the layer to be redrawn mid-animation
2183
+ animationNode.style.pointerEvents = 'none';
2184
+
2185
+ document.body.appendChild(animationNode);
2186
+ node.style.display = 'none';
2187
+
2188
+ __BMVelocityAnimate(this._window, {
2189
+ opacity: 1,
2190
+ translateX: '0px',
2191
+ translateY: '0px',
2192
+ scaleX: 1,
2193
+ scaleY: 1
2194
+ }, {
2195
+ duration: _BMWindowAnimationDurationDefault,
2196
+ easing: _BMWindowAnimationEasingDefault,
2197
+ display: 'block'
2198
+ }, YES);
2199
+
2200
+ const title = animationNode.querySelector('span');
2201
+ __BMVelocityAnimate(title, {translateX: ['30px', '0px']}, {duration: _BMWindowAnimationDurationDefault, easing: _BMWindowAnimationEasingDefault}, YES);
2202
+
2203
+ // After finishing the animation, remove the temporary node and restore the original node's display
2204
+ __BMVelocityAnimate(animationNode, {opacity: [0, 1]}, {
2205
+ duration: _BMWindowAnimationDurationDefault,
2206
+ easing: _BMWindowAnimationEasingDefault,
2207
+ complete: function () {
2208
+ animationNode.remove();
2209
+ node.remove();
2210
+
2211
+ self.node.classList.remove('BMWindowMinimizedPreview');
2212
+
2213
+ self.node.style.pointerEvents = '';
2214
+ self.node.inert = NO;
2215
+
2216
+ if (self.delegate && self.delegate.windowDidRestore) {
2217
+ self.delegate.windowDidRestore(self);
2218
+ }
2219
+
2220
+ if (args && args.completionHandler) args.completionHandler();
2221
+ }
2222
+ }, YES);
1889
2223
  }
1890
2224
  },
1891
2225
 
@@ -2135,6 +2469,8 @@ BMWindow.prototype = BMExtend(Object.create(BMView.prototype), {
2135
2469
  }
2136
2470
  this._minimizedWindow.remove();
2137
2471
  }
2472
+
2473
+ BMWindow._updateMinimizedWindowsAnimated(NO);
2138
2474
  }
2139
2475
 
2140
2476
  BMView.prototype.release.call(this);