ember-sortable 2.4.0 → 3.0.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 (36) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +2 -2
  2. package/.github/workflows/github_ci.yml +15 -13
  3. package/CHANGELOG.md +203 -108
  4. package/MIGRATION_GUIDE_MODIFIERS.md +20 -13
  5. package/MIGRATION_GUIDE_V2.md +20 -8
  6. package/README.md +74 -151
  7. package/V2_MIGRATION_RFC.md +188 -222
  8. package/addon/modifiers/sortable-group.js +38 -40
  9. package/addon/modifiers/sortable-handle.js +16 -18
  10. package/addon/modifiers/sortable-item.js +101 -74
  11. package/addon/services/ember-sortable.js +3 -6
  12. package/addon/system/scroll-parent.js +1 -1
  13. package/addon/utils/coordinate.js +1 -1
  14. package/addon/utils/css-calculation.js +2 -2
  15. package/addon/utils/defaults.js +4 -3
  16. package/addon/utils/keyboard.js +3 -12
  17. package/addon-test-support/helpers/drag.js +3 -11
  18. package/addon-test-support/helpers/index.js +1 -4
  19. package/addon-test-support/helpers/reorder.js +4 -6
  20. package/addon-test-support/utils/keyboard.js +3 -12
  21. package/addon-test-support/utils/offset.js +6 -6
  22. package/config/environment.js +2 -2
  23. package/index.js +0 -20
  24. package/package.json +31 -17
  25. package/addon/components/sortable-group.js +0 -741
  26. package/addon/components/sortable-handle.js +0 -30
  27. package/addon/components/sortable-item.js +0 -770
  28. package/addon/styles/components/sortable-group.css +0 -9
  29. package/addon/templates/components/sortable-group.hbs +0 -16
  30. package/addon/templates/components/sortable-handle.hbs +0 -1
  31. package/addon/templates/components/sortable-item.hbs +0 -2
  32. package/addon/utils/transitionend.js +0 -22
  33. package/app/components/sortable-group.js +0 -3
  34. package/app/components/sortable-handle.js +0 -4
  35. package/app/components/sortable-item.js +0 -3
  36. package/vendor/polyfills/closest.js +0 -21
@@ -1,770 +0,0 @@
1
- import { Promise, defer } from 'rsvp';
2
- import Component from '@ember/component';
3
- import { defineProperty } from '@ember/object';
4
- import { or, readOnly } from '@ember/object/computed';
5
- import { run, throttle, bind, scheduleOnce, later } from '@ember/runloop';
6
- import { DEBUG } from '@glimmer/env';
7
- import scrollParent from '../system/scroll-parent';
8
- import ScrollContainer from '../system/scroll-container';
9
- import layout from '../templates/components/sortable-item';
10
- import { getBorderSpacing } from '../utils/css-calculation';
11
- import { DRAG_ACTIONS, ELEMENT_CLICK_ACTION, END_ACTIONS } from '../utils/constant';
12
- import { getX, getY } from '../utils/coordinate';
13
- import { buildWaiter } from '@ember/test-waiters';
14
- import config from 'ember-get-config';
15
- import { deprecate } from '@ember/debug';
16
-
17
- const { environment } = config;
18
- const isTesting = environment === 'test';
19
-
20
-
21
-
22
- const sortableItemWaiter = buildWaiter("sortable-item-waiter");
23
-
24
- /**
25
- * This component represents an individual model of `sortable-group`.
26
- *
27
- * Public API
28
- * @param {String} handle the attached handle, by default, it looks for a `sortable-handle`.
29
- * @param {Function} model the model that this component is associated with.
30
- * @param {Function} [onDragStart] An optional callback for when dragging starts.
31
- * @param {Function} [onDragStop] An optional callback for when dragging stops.
32
- *
33
- * @module drag-drop/-private/draggable-item
34
- */
35
- export default Component.extend({
36
- layout,
37
- tagName: 'li',
38
- classNames: ['sortable-item'],
39
- classNameBindings: ['isDragging', 'isDropping'],
40
-
41
- attributeBindings: ['data-test-selector', 'tabindex'],
42
-
43
- /**
44
- Model which the item represents.
45
- @property model
46
- @type Object
47
- @default null
48
- */
49
- model: null,
50
-
51
- /**
52
- Selector for the element to use as handle.
53
- 1. By default, we will hook it the yielded sortable-handle.
54
- 2. If you don't use the sortable-handle, the entire element will be used as the handle.
55
- 3. In very rare situations, if you want to use a handle, but not the sortable-handle,
56
- you can override this class with your own handle's selector. This behavior will be
57
- synonymous with v1
58
- @property handle
59
- @type String
60
- @default "[data-sortable-handle]"
61
- */
62
- handle: "[data-sortable-handle]",
63
-
64
- /**
65
- * Tolerance, in pixels, for when sorting should start.
66
- * If specified, sorting will not start until after mouse
67
- * is dragged beyond distance. Can be used to allow for clicks
68
- * on elements within a handle.
69
- *
70
- * @property distance
71
- * @type Integer
72
- * @default 0
73
- */
74
- distance: 0,
75
-
76
- /**
77
- * True if the item is currently being dragged.
78
- *
79
- * @property isDragging
80
- * @type Boolean
81
- * @default false
82
- * @protected
83
- */
84
- isDragging: false,
85
-
86
- /**
87
- Action that fires when the item starts being dragged.
88
- @property onDragStart
89
- @type Action
90
- @param object item model
91
- @default null
92
- */
93
- onDragStart: function() {},
94
-
95
- /**
96
- Action that fires when the item stops being dragged.
97
- @property onDragStop
98
- @type Action
99
- @param object item model
100
- @default null
101
- */
102
- onDragStop: function() {},
103
-
104
- /**
105
- True if the item is currently dropping.
106
- @property isDropping
107
- @type Boolean
108
- @default false
109
- */
110
- isDropping: false,
111
-
112
- /**
113
- True if the item was dropped during the interaction
114
- @property wasDropped
115
- @type Boolean
116
- @default false
117
- */
118
- wasDropped: false,
119
-
120
- /**
121
- @property isBusy
122
- @type Boolean
123
- */
124
- isBusy: or('isDragging', 'isDropping'),
125
-
126
- /**
127
- The frequency with which the group is informed
128
- that an update is required.
129
- @property updateInterval
130
- @type Number
131
- @default 125
132
- */
133
- updateInterval: 125,
134
-
135
- /**
136
- Additional spacing between active item and the rest of the elements.
137
- @property spacing
138
- @type Number
139
- @default 0[px]
140
- */
141
- spacing: 0,
142
-
143
- /**
144
- Removes the ability for the current item to be dragged
145
- @property isDraggingDisabled
146
- @type Boolean
147
- @default false
148
- */
149
- isDraggingDisabled: false,
150
-
151
- /**
152
- @private
153
- Allows host instance to use the `group` property for something else with
154
- minimal overriding.
155
- */
156
- _direction: readOnly('direction'),
157
-
158
-
159
- disableCheckScrollBounds: isTesting,
160
-
161
- init() {
162
- this._super(...arguments);
163
-
164
- deprecate(
165
- 'The <SortableItem> component is deprecated. Please use the modifier version {{sortable-item}}.',
166
- false,
167
- {
168
- id: 'ember-sortable:sortable-item-component-deprecated',
169
- until: '3.0.0',
170
- for: 'ember-sortable',
171
- since: {
172
- available: '2.2.6',
173
- enabled: '2.2.6',
174
- },
175
- }
176
- );
177
- this._setGetterSetters();
178
- },
179
-
180
- /**
181
- @method didInsertElement
182
- */
183
- didInsertElement() {
184
- this._super(...arguments);
185
-
186
- this.registerItem(this);
187
-
188
- // Instead of using `event.preventDefault()` in the 'primeDrag' event,
189
- // (doesn't work in Chrome 56), we set touch-action: none as a workaround.
190
- let handleElement = this.element.querySelector(this.get('handle'));
191
-
192
- if (!handleElement) {
193
- // Handle does not exist, so we set it null here.
194
- this.set('handle', null);
195
- handleElement = this.element;
196
- }
197
-
198
- handleElement.style['touch-action'] = 'none';
199
- },
200
-
201
- /**
202
- @method willDestroyElement
203
- */
204
- willDestroyElement() {
205
- this._super(...arguments);
206
- this.deregisterItem(this);
207
-
208
- // remove event listeners that may still be attached
209
- DRAG_ACTIONS.forEach(event => window.removeEventListener(event, this._prepareDragListener));
210
- END_ACTIONS.forEach(event => window.removeEventListener(event, this._cancelStartDragListener));
211
- this.element.removeEventListener(ELEMENT_CLICK_ACTION, this._preventClickHandler);
212
- this.set('isDragging', false);
213
- this.set('isDropping', false);
214
- },
215
-
216
- /**
217
- @method mouseDown
218
- */
219
- mouseDown(event) {
220
- if (event.which !== 1) { return; }
221
- if (event.ctrlKey) { return; }
222
-
223
- this._primeDrag(event);
224
- },
225
-
226
- keyDown(event) {
227
- const handleElement = this.element.querySelector(this.get('handle'));
228
-
229
- // If the event is coming from within the item, we do not want to activate keyboard reorder mode.
230
- if (event.target === handleElement || event.target === this.element) {
231
- this.setSelectedItem(this);
232
- this.activateKeyDown();
233
- } else {
234
- this.deactivateKeyDown();
235
- }
236
- },
237
-
238
- /**
239
- @method touchStart
240
- */
241
- touchStart(event) {
242
- this._primeDrag(event);
243
- },
244
-
245
- /**
246
- @method freeze
247
- */
248
- freeze() {
249
- let el = this.element;
250
- if (!el) { return; }
251
-
252
- el.style.transition = 'none';
253
- },
254
-
255
- /**
256
- @method reset
257
- */
258
- reset() {
259
- let el = this.element;
260
- if (!el) { return; }
261
-
262
- delete this._y;
263
- delete this._x;
264
-
265
- el.style.transform = '';
266
- },
267
-
268
- /**
269
- @method thaw
270
- */
271
- thaw() {
272
- let el = this.element;
273
- if (!el) { return; }
274
-
275
- el.style.transition = '';
276
- },
277
-
278
- /**
279
- * Setup event listeners for drag and drop
280
- *
281
- * @method _primeDrag
282
- * @param {Event} startEvent JS Event object
283
- * @private
284
- */
285
- _primeDrag(startEvent) {
286
- // Prevent dragging if the sortable-item is destroying or is disabled.
287
- if (this.isDestroying || this.get('isDraggingDisabled')) {
288
- return;
289
- }
290
-
291
- let handle = this.get('handle');
292
-
293
- if (handle && !startEvent.target.closest(handle)) {
294
- return;
295
- }
296
-
297
- startEvent.preventDefault();
298
- startEvent.stopPropagation();
299
-
300
- this._prepareDragListener = bind(this, this._prepareDrag, startEvent);
301
-
302
- DRAG_ACTIONS.forEach(event => window.addEventListener(event, this._prepareDragListener));
303
-
304
- this._cancelStartDragListener = () => {
305
- DRAG_ACTIONS.forEach(event => window.removeEventListener(event, this._prepareDragListener));
306
- };
307
-
308
- const selfCancellingCallback = () => {
309
- END_ACTIONS.forEach(event => window.removeEventListener(event, selfCancellingCallback));
310
- this._cancelStartDragListener();
311
- };
312
-
313
- END_ACTIONS.forEach(event => window.addEventListener(event, selfCancellingCallback));
314
- },
315
-
316
- /**
317
- * Prepares for the drag event
318
- *
319
- * @method _prepareDrag
320
- * @param {Event} startEvent JS Event object
321
- * @param {Event} event JS Event object
322
- * @private
323
- */
324
- _prepareDrag(startEvent, event) {
325
- let distance = this.get('distance');
326
- let dx = Math.abs(getX(startEvent) - getX(event));
327
- let dy = Math.abs(getY(startEvent) - getY(event));
328
-
329
- if (distance <= dx || distance <= dy) {
330
- DRAG_ACTIONS.forEach(event => window.removeEventListener(event, this._prepareDragListener));
331
- this._startDrag(startEvent);
332
- }
333
- },
334
-
335
- /**
336
- * Start dragging & setup more event listeners
337
- *
338
- * @method _startDrag
339
- * @param {Event} event JS Event object
340
- * @private
341
- */
342
- _startDrag(event) {
343
- if (this.isDestroying || this.get('isBusy')) { return; }
344
-
345
- let drag = this._makeDragHandler(event);
346
- let dragThrottled = ev => throttle(this, drag, ev, 16, false);
347
-
348
- let drop = () => {
349
- DRAG_ACTIONS.forEach(event => window.removeEventListener(event, dragThrottled));
350
- END_ACTIONS.forEach(event => window.removeEventListener(event, drop));
351
-
352
- run(() => {
353
- this._drop();
354
- });
355
- };
356
-
357
- DRAG_ACTIONS.forEach(event => window.addEventListener(event, dragThrottled));
358
- END_ACTIONS.forEach(event => window.addEventListener(event, drop));
359
-
360
- this.prepare();
361
- this.set('isDragging', true);
362
- this.onDragStart(this.get('model'));
363
- this._scrollOnEdges(drag);
364
- },
365
-
366
- /**
367
- The maximum scroll speed when dragging element.
368
- @property maxScrollSpeed
369
- @default 20
370
- */
371
- maxScrollSpeed: 20,
372
-
373
- _scrollOnEdges(drag) {
374
- let groupDirection = this.get('_direction');
375
- let element = this.element;
376
- let scrollContainer = new ScrollContainer(scrollParent(element));
377
- let itemContainer = {
378
- width: parseInt(getComputedStyle(element).width, 10),
379
- get height() {
380
- return parseInt(getComputedStyle(element).height, 10);
381
- },
382
- get left() {
383
- return element.getBoundingClientRect().left;
384
- },
385
- get right() {
386
- return this.left + this.width;
387
- },
388
- get top() {
389
- return element.getBoundingClientRect().top;
390
- },
391
- get bottom() {
392
- return this.top + this.height;
393
- }
394
- };
395
-
396
- let leadingEdgeKey, trailingEdgeKey, scrollKey, pageKey;
397
- if (groupDirection === 'x') {
398
- leadingEdgeKey = 'left';
399
- trailingEdgeKey = 'right';
400
- scrollKey = 'scrollLeft';
401
- pageKey = 'pageX';
402
- } else {
403
- leadingEdgeKey = 'top';
404
- trailingEdgeKey = 'bottom';
405
- scrollKey = 'scrollTop';
406
- pageKey = 'pageY';
407
- }
408
-
409
- let createFakeEvent = () => {
410
- if (this._pageX == null && this._pageY == null) { return; }
411
- return {
412
- pageX: this._pageX,
413
- pageY: this._pageY,
414
- clientX: this._pageX,
415
- clientY: this._pageY
416
- };
417
- };
418
-
419
- // Set a trigger padding that will start scrolling
420
- // the box when the item reaches within padding pixels
421
- // of the edge of the scroll container.
422
- let checkScrollBounds = () => {
423
- let leadingEdge = itemContainer[leadingEdgeKey];
424
- let trailingEdge = itemContainer[trailingEdgeKey];
425
- let scroll = scrollContainer[scrollKey]();
426
-
427
- let delta = 0;
428
- if (trailingEdge >= scrollContainer[trailingEdgeKey]) {
429
- delta = trailingEdge - scrollContainer[trailingEdgeKey];
430
- } else if (leadingEdge <= scrollContainer[leadingEdgeKey]) {
431
- delta = leadingEdge - scrollContainer[leadingEdgeKey];
432
- }
433
-
434
- if (delta !== 0) {
435
- let speed = this.get('maxScrollSpeed');
436
- delta = Math.min(Math.max(delta, -1 * speed), speed);
437
-
438
- delta = scrollContainer[scrollKey](scroll + delta) - scroll;
439
-
440
- let event = createFakeEvent();
441
- if (event) {
442
- if (scrollContainer.isWindow) {
443
- event[pageKey] += delta;
444
- }
445
- run(() => drag(event));
446
- }
447
- }
448
- if (this.get('isDragging')) {
449
- requestAnimationFrame(checkScrollBounds);
450
- }
451
- };
452
- if (!this.disableCheckScrollBounds) {
453
- requestAnimationFrame(checkScrollBounds);
454
- }
455
- },
456
-
457
- /**
458
- @method _makeDragHandler
459
- @param {Event} startEvent
460
- @return {Function}
461
- @private
462
- */
463
- _makeDragHandler(startEvent) {
464
- const groupDirection = this.get('_direction');
465
- let dragOrigin;
466
- let elementOrigin;
467
- let scrollOrigin;
468
- // if component is destroying or element does not exist or its parent does not exist, stop.
469
- if (this.isDestroying || !this.element || !this.element.parentNode) {
470
- return;
471
- }
472
- let parentElement = this.element.parentNode;
473
-
474
- if (groupDirection === 'x') {
475
- dragOrigin = getX(startEvent);
476
- elementOrigin = this.get('x');
477
- scrollOrigin = parentElement.getBoundingClientRect().left;
478
-
479
- return event => {
480
- this._pageX = getX(event);
481
- let dx = this._pageX - dragOrigin;
482
- let scrollX = parentElement.getBoundingClientRect().left;
483
- let x = elementOrigin + dx + (scrollOrigin - scrollX);
484
-
485
- this._drag(x);
486
- };
487
- }
488
-
489
- if (groupDirection === 'y') {
490
- dragOrigin = getY(startEvent);
491
- elementOrigin = this.get('y');
492
- scrollOrigin = parentElement.getBoundingClientRect().top;
493
-
494
- return event => {
495
- this._pageY = getY(event);
496
- let dy = this._pageY - dragOrigin;
497
- let scrollY = parentElement.getBoundingClientRect().top;
498
- let y = elementOrigin + dy + (scrollOrigin - scrollY);
499
-
500
- this._drag(y);
501
- };
502
- }
503
- },
504
-
505
- /**
506
- @method _scheduleApplyPosition
507
- @private
508
- */
509
- _scheduleApplyPosition() {
510
- scheduleOnce('render', this, '_applyPosition');
511
- },
512
-
513
- /**
514
- @method _applyPosition
515
- @private
516
- */
517
- _applyPosition() {
518
- if (!this.element || !this.element) { return; }
519
-
520
- const groupDirection = this.get('_direction');
521
-
522
- if (groupDirection === 'x') {
523
- let x = this.get('x');
524
- let dx = x - this.element.offsetLeft + parseFloat(getComputedStyle(this.element).marginLeft);
525
-
526
- this.element.style.transform = `translateX(${dx}px)`;
527
- }
528
- if (groupDirection === 'y') {
529
- let y = this.get('y');
530
- let dy = y - this.element.offsetTop;
531
-
532
- this.element.style.transform = `translateY(${dy}px)`;
533
- }
534
- },
535
-
536
- /**
537
- @method _drag
538
- @private
539
- */
540
- _drag(dimension) {
541
- if(!this.get("isDragging")) {
542
- return;
543
- }
544
- let updateInterval = this.get('updateInterval');
545
- const groupDirection = this.get('_direction');
546
-
547
- if (groupDirection === 'x') {
548
- this.set('x', dimension);
549
- }
550
- if (groupDirection === 'y') {
551
- this.set('y', dimension);
552
- }
553
-
554
- throttle(this, 'update', updateInterval);
555
- },
556
-
557
- /**
558
- @method _drop
559
- @private
560
- */
561
- _drop() {
562
- if (!this.element) { return; }
563
-
564
- let transitionPromise = this._waitForTransition();
565
-
566
- this._preventClick();
567
-
568
- this.set('isDragging', false);
569
- this.set('isDropping', true);
570
-
571
- this.update();
572
- transitionPromise.then(() => this._complete());
573
- },
574
-
575
- /**
576
- @method _preventClick
577
- @private
578
- */
579
- _preventClick() {
580
- const selfCancellingCallback = (event) => {
581
- this.element.removeEventListener(ELEMENT_CLICK_ACTION, selfCancellingCallback);
582
- this._preventClickHandler(event);
583
- };
584
-
585
- this.element.addEventListener(ELEMENT_CLICK_ACTION, selfCancellingCallback);
586
- },
587
-
588
- /**
589
- @method _preventClickHandler
590
- @private
591
- */
592
- _preventClickHandler(e) {
593
- e.stopPropagation();
594
- e.preventDefault();
595
- e.stopImmediatePropagation();
596
- },
597
-
598
- /**
599
- @method _waitForTransition
600
- @private
601
- @return Promise
602
- */
603
- _waitForTransition() {
604
- let waiterToken;
605
-
606
- if (DEBUG) {
607
- waiterToken = sortableItemWaiter.beginAsync();
608
- }
609
-
610
- let transitionPromise;
611
-
612
- if (this.get('isAnimated')) {
613
- const deferred = defer();
614
- this.element.addEventListener('transitionend', deferred.resolve);
615
- transitionPromise = deferred.promise.finally(() => {
616
- this.element.removeEventListener('transitionend', deferred.resolve);
617
- });
618
- } else {
619
- const duration = this.get('isAnimated') ? this.get('transitionDuration') : 200;
620
- transitionPromise = new Promise((resolve) => later(resolve, duration));
621
- }
622
-
623
- if (DEBUG) {
624
- transitionPromise = transitionPromise.finally(() => {
625
- sortableItemWaiter.endAsync(waiterToken);
626
- });
627
- }
628
-
629
- return transitionPromise;
630
- },
631
-
632
- /**
633
- @method _complete
634
- @private
635
- */
636
- _complete() {
637
- this.onDragStop(this.get('model'));
638
- this.set('isDropping', false);
639
- this.set('wasDropped', true);
640
- this.commit();
641
- },
642
-
643
- /**
644
- * Defining getters and setters to support native getter and setters until we decide to drop support for ember versions below 3.10
645
- */
646
- _setGetterSetters() {
647
- /**
648
- True if the item transitions with animation.
649
- @property isAnimated
650
- @type Boolean
651
- */
652
- defineProperty(this, 'isAnimated', {
653
- get() {
654
- if (!this.element) { return; }
655
-
656
- let el = this.element;
657
- let property = getComputedStyle(el).transitionProperty;
658
-
659
- return /all|transform/.test(property) && this.get("transitionDuration") > 0;
660
- }
661
- });
662
-
663
- /**
664
- The current transition duration in milliseconds.
665
- @property transitionDuration
666
- @type Number
667
- */
668
- defineProperty(this, 'transitionDuration', {
669
- get() {
670
- let el = this.element;
671
- let rule = getComputedStyle(el).transitionDuration;
672
- let match = rule.match(/([\d.]+)([ms]*)/);
673
-
674
- if (match) {
675
- let value = parseFloat(match[1]);
676
- let unit = match[2];
677
-
678
- if (unit === 's') {
679
- value = value * 1000;
680
- }
681
-
682
- return value;
683
- }
684
-
685
- return 0;
686
- }
687
- });
688
-
689
- /**
690
- Horizontal position of the item.
691
- @property x
692
- @type Number
693
- */
694
- defineProperty(this, 'x', {
695
- get() {
696
- if (this._x === undefined) {
697
- let marginLeft = parseFloat(getComputedStyle(this.element).marginLeft);
698
- this._x = this.element.scrollLeft + this.element.offsetLeft - marginLeft;
699
- }
700
-
701
- return this._x;
702
- },
703
- set(value) {
704
- if (value !== this._x) {
705
- this._x = value;
706
- this._scheduleApplyPosition();
707
- }
708
- }
709
- });
710
-
711
- /**
712
- Vertical position of the item relative to its offset parent.
713
- @property y
714
- @type Number
715
- */
716
- defineProperty(this, 'y', {
717
- get() {
718
- if (this._y === undefined) {
719
- this._y = this.element.offsetTop;
720
- }
721
-
722
- return this._y;
723
- },
724
- set(value) {
725
- if (value !== this._y) {
726
- this._y = value;
727
- this._scheduleApplyPosition();
728
- }
729
- }
730
- });
731
-
732
- /**
733
- Width of the item.
734
- @property height
735
- @type Number
736
- */
737
- defineProperty(this, 'width', {
738
- get() {
739
- let el = this.element;
740
- let width = el.offsetWidth;
741
- let elStyles = getComputedStyle(el);
742
-
743
- width += parseInt(elStyles.marginLeft) + parseInt(elStyles.marginRight); // equal to jQuery.outerWidth(true)
744
-
745
- width += getBorderSpacing(el).horizontal;
746
-
747
- return width;
748
- }
749
- });
750
-
751
- /**
752
- Height of the item including margins.
753
- @property height
754
- @type Number
755
- */
756
- defineProperty(this, 'height', {
757
- get() {
758
- let el = this.element;
759
- let height = el.offsetHeight;
760
-
761
- let marginBottom = parseFloat(getComputedStyle(el).marginBottom);
762
- height += marginBottom;
763
-
764
- height += getBorderSpacing(el).vertical;
765
-
766
- return height;
767
- }
768
- });
769
- },
770
- });