diff --git a/README.md b/README.md index f10b7b51..ed81c324 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,90 @@ -# Muuri +

+ +

-[![gzip size](https://img.badgesize.io/https://unpkg.com/muuri@0.8.0/dist/muuri.min.js?compression=gzip)](https://unpkg.com/muuri@0.8.0/dist/muuri.min.js) -[![npm](https://img.shields.io/npm/v/muuri.svg)](http://npm.im/muuri) +Muuri is a JavaScript layout engine that allows you to build all kinds of layouts (no kidding!) and make them responsive, sortable, filterable, draggable and/or animated. Comparing to what's out there Muuri is a combination of [Packery](http://packery.metafizzy.co/), [Masonry](http://masonry.desandro.com/), [Isotope](http://isotope.metafizzy.co/) and [Sortable](https://github.com/RubaXa/Sortable). Wanna see it in action? Check out the [demo](https://muuri.dev/) on the website. -Muuri is a JavaScript layout engine that allows you to build all kinds of layouts and make them responsive, sortable, filterable, draggable and/or animated. Comparing to what's out there Muuri is a combination of [Packery](http://packery.metafizzy.co/), [Masonry](http://masonry.desandro.com/), [Isotope](http://isotope.metafizzy.co/) and [Sortable](https://github.com/RubaXa/Sortable). Wanna see it in action? Check out the [demo](http://haltu.github.io/muuri/) on the website. +**Features** -Muuri's default "First Fit" bin packing layout algorithm generates layouts similar to [Packery](https://github.com/metafizzy/packery) and [Masonry](http://masonry.desandro.com/). The implementation is heavily based on the "maxrects" approach as described by Jukka Jylänki in his research [A Thousand Ways to Pack the Bin](http://clb.demon.fi/files/RectangleBinPack.pdf). If that's not your cup of tea you can always provide your own layout algorithm to position the items as you wish. +- Fully customizable layout +- Asynchronous layout calculations in web workers +- Drag & drop (even between grids) +- Auto-scrolling during drag +- Nested grids +- Fast animations +- Filtering +- Sorting -Muuri uses [Web Animations](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) for animations and has an internal mechanism to handle dragging. And if you're wondering about the name of the library "muuri" is Finnish meaning a wall. +

Table of contents

-**Features** +- [Motivation](#motivation) +- [Getting started](#getting-started) +- [API](#api) + - [Grid constructor](#grid-constructor) + - [Grid options](#grid-options) + - [Grid methods](#grid-methods) + - [Grid events](#grid-events) + - [Item methods](#item-methods) +- [Credits](#credits) +- [License](#license) + +

Motivation

-* Fully customizable layout -* Drag & drop (even between grids) -* Nested grids -* Fast animations -* Filtering -* Sorting +You can build pretty amazing layouts without a single line of JavaScript these days. However, sometimes (rarely though) CSS just isn't enough, and that's where Muuri comes along. At it's very core Muuri is a _layout engine_ which is limited only by your imagination. You can seriously build _any_ kind of layout, asynchronously in web workers if you wish. -## Table of contents +Custom layouts aside, you might need to sprinkle some flare (animation) and/or interactivity (filtering / sorting / drag & drop) on your layout (be it CSS or JS based). Stuff gets complex pretty fast and most of us probably reach for existing libraries to handle the complexity at that point. This is why most of these extra features are built into Muuri's core, so you don't have to go hunting for additional libraries or re-inventing the wheel for the nth time. -* [Getting started](#getting-started) -* [API](#api) - * [Grid constructor](#grid-constructor) - * [Grid options](#grid-options) - * [Grid methods](#grid-methods) - * [Grid events](#grid-events) - * [Item methods](#item-methods) -* [Credits](#credits) -* [License](#license) +The long-term goal of Muuri is to provide a simple (and as low-level as possible) API for building amazing layouts with unmatched performance and _most_ of the complexity abstracted away. -## Getting started +

Getting started

-### 1. Get Muuri +

1. Get Muuri

+ +Install via [npm](https://www.npmjs.com/package/muuri): -Install via [npm](https://www.npmjs.com/): ```bash npm install muuri ``` Or download: -* [muuri.js](https://unpkg.com/muuri@0.8.0/dist/muuri.js) - for development (not minified, with comments). -* [muuri.min.js](https://unpkg.com/muuri@0.8.0/dist/muuri.min.js) - for production (minified, no comments). + +- [muuri.js](https://cdn.jsdelivr.net/npm/muuri@0.9.0/dist/muuri.js) - for development (not minified, with comments). +- [muuri.min.js](https://cdn.jsdelivr.net/npm/muuri@0.9.0/dist/muuri.min.js) - for production (minified, no comments). Or link directly: + ```html - + ``` -### 2. Get Web Animations Polyfill (if needed) +

2. Get Web Animations Polyfill (if needed)

Muuri uses [Web Animations](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) to handle all the animations by default. If you need to use Muuri on a browser that does not support Web Animations you need to use a [polyfill](https://github.com/web-animations/web-animations-js). +Install via [npm](https://www.npmjs.com/package/web-animations-js): + ```bash npm install web-animations-js ``` -### 3. Add the script tags +Or download: -Add Muuri on your site and make sure to include the Web Animations Polyfill before Muuri (if needed). +- [web-animations.min.js](https://cdn.jsdelivr.net/npm/web-animations-js@2.3.2/web-animations.min.js) + +Or link directly: ```html - - + ``` -### 4. Add the markup +

3. Add the markup

-* Every grid must have a container element. -* Grid items must always consist of at least two elements. The outer element is used for positioning the item and the inner element (first direct child) is used for animating the item's visibility (show/hide methods). You can insert any markup you wish inside the inner item element. +- Every grid must have a container element (referred as the _grid element_ from now on). +- Grid items must always consist of at least two elements. The outer element is used for positioning the item and the inner element (first direct child) is used for animating the item's visibility (show/hide methods). You can insert any markup you wish inside the inner item element. +- Note that the class names in the below example are not required by Muuri at all, they're just there for example's sake. ```html
-
@@ -90,17 +102,16 @@ Add Muuri on your site and make sure to include the Web Animations Polyfill befo
-
``` -### 5. Add the styles +

4. Add the styles

-* The container element must be "positioned" meaning that it's CSS position property must be set to *relative*, *absolute* or *fixed*. Also note that Muuri automatically resizes the container element's width/height depending on the area the items cover and the layout algorithm configuration. -* The item elements must have their CSS position set to *absolute* and their display property set to *block*. Muuri actually enforces the `display:block;` rule and adds it as an inline style to all item elements, just in case. -* The item elements must not have any CSS transitions or animations applied to them, because they might conflict with Muuri's internal animation engine. However, the container element can have transitions applied to it if you want it to animate when it's size changes after the layout operation. -* You can control the gaps between the items by giving some margin to the item elements. -* One last thing: never ever set `overflow: auto;` or `overflow: scroll;` to the container element. Muuri's calculation logic does not account for that and you _will_ see some item jumps when dragging starts. Always use a wrapper element for the container where you set the `auto`/`scroll` overflow values. +- The grid element must be "positioned" meaning that it's CSS position property must be set to _relative_, _absolute_ or _fixed_. Also note that Muuri automatically resizes the grid element's width/height depending on the area the items cover and the layout algorithm configuration. +- The item elements must have their CSS position set to _absolute_. +- The item elements must not have any CSS transitions or animations applied to them, because they might conflict with Muuri's internal animation engine. However, the grid element can have transitions applied to it if you want it to animate when it's size changes after the layout operation. +- You can control the gaps between the items by giving some margin to the item elements. +- One last thing. Never ever set `overflow: auto;` or `overflow: scroll;` to the grid element. Muuri's calculation logic does not account for that and you _will_ see some item jumps when dragging starts. Always use a wrapper element for the grid element where you set the `auto`/`scroll` overflow values. ```css .grid { @@ -132,9 +143,9 @@ Add Muuri on your site and make sure to include the Web Animations Polyfill befo } ``` -### 6. Fire it up +

5. Fire it up

-The bare minimum configuration is demonstrated below. You must always provide the container element (or a selector so Muuri can fetch the element for you), everything else is optional. +The bare minimum configuration is demonstrated below. You must always provide the grid element (or a selector so Muuri can fetch the element for you), everything else is optional. ```javascript var grid = new Muuri('.grid'); @@ -142,11 +153,11 @@ var grid = new Muuri('.grid'); You can view this little tutorial demo [here](https://codepen.io/niklasramo/pen/wpwNjK). After that you might want to check some [other demos](https://codepen.io/collection/AWopag/) as well. -## API +

API

-### Grid constructor +

Grid constructor

-`Muuri` is a constructor function and should be always instantiated with the `new` keyword. For the sake of clarity, we refer to a Muuri instance as *grid* throughout the documentation. +`Muuri` is a constructor function and should be always instantiated with the `new` keyword. For the sake of clarity, we refer to a Muuri instance as _grid_ throughout the documentation. **Syntax** @@ -154,11 +165,11 @@ You can view this little tutorial demo [here](https://codepen.io/niklasramo/pen/ **Parameters** -* **element**  —  *element* / *string* - * Default value: `null`. - * You can provide the element directly or use a selector (string) which uses [querySelector()](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) internally. -* **options**  —  *object* - * Optional. Check out the [detailed options reference](#grid-options). +- **element**  —  _element_ / _string_ + - Default value: `null`. + - You can provide the element directly or use a selector (string) which uses [querySelector()](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) internally. +- **options**  —  _object_ + - Optional. Check out the [detailed options reference](#grid-options). **Default options** @@ -166,7 +177,7 @@ The default options are stored in `Muuri.defaultOptions` object, which in it's d ```javascript { - // Item elements + // Initial item elements items: '*', // Default show animation @@ -193,9 +204,9 @@ The default options are stored in `Muuri.defaultOptions` object, which in it's d horizontal: false, alignRight: false, alignBottom: false, - rounding: true + rounding: false }, - layoutOnResize: 100, + layoutOnResize: 150, layoutOnInit: true, layoutDuration: 300, layoutEasing: 'ease', @@ -206,12 +217,12 @@ The default options are stored in `Muuri.defaultOptions` object, which in it's d // Drag & Drop dragEnabled: false, dragContainer: null, + dragHandle: null, dragStartPredicate: { distance: 0, - delay: 0, - handle: false + delay: 0 }, - dragAxis: null, + dragAxis: 'xy', dragSort: true, dragSortHeuristics: { sortInterval: 100, @@ -220,10 +231,14 @@ The default options are stored in `Muuri.defaultOptions` object, which in it's d }, dragSortPredicate: { threshold: 50, - action: actionMove + action: 'move', + migrateAction: 'move' + }, + dragRelease: { + duration: 300, + easing: 'ease', + useDragContainer: true }, - dragReleaseDuration: 300, - dragReleaseEasing: 'ease', dragCssProps: { touchAction: 'none', userSelect: 'none', @@ -234,12 +249,21 @@ The default options are stored in `Muuri.defaultOptions` object, which in it's d }, dragPlaceholder: { enabled: false, - duration: 300, - easing: 'ease', createElement: null, onCreate: null, onRemove: null }, + dragAutoScroll: { + targets: [], + handle: null, + threshold: 50, + safeZone: 0.2, + speed: Muuri.AutoScroller.smoothSpeed(1000, 2000, 2500), + sortDuringScroll: true, + smoothStop: false, + onStart: null, + onStop: null + }, // Classnames containerClass: 'muuri', @@ -268,185 +292,260 @@ var gridA = new Muuri('.grid-a'); // Providing some options. var gridB = new Muuri('.grid-b', { - items: '.item' -}); -``` - -### Grid options - -* [items](#items-) -* [showDuration](#showduration-) -* [showEasing](#showeasing-) -* [hideDuration](#hideduration-) -* [hideEasing](#hideeasing-) -* [visibleStyles](#visiblestyles-) -* [hiddenStyles](#hiddenstyles-) -* [layout](#layout-) -* [layoutOnResize](#layoutonresize-) -* [layoutOnInit](#layoutoninit-) -* [layoutDuration](#layoutduration-) -* [layoutEasing](#layouteasing-) -* [sortData](#sortdata-) -* [dragEnabled](#dragenabled-) -* [dragContainer](#dragcontainer-) -* [dragStartPredicate](#dragstartpredicate-) -* [dragAxis](#dragaxis-) -* [dragSort](#dragsort-) -* [dragSortHeuristics](#dragsortheuristics-) -* [dragSortPredicate](#dragsortpredicate-) -* [dragReleaseDuration](#dragreleaseduration-) -* [dragReleaseEasing](#dragreleaseeasing-) -* [dragCssProps](#dragcssprops-) -* [dragPlaceholder](#dragplaceholder-) -* [containerClass](#containerclass-) -* [itemClass](#itemclass-) -* [itemVisibleClass](#itemvisibleclass-) -* [itemHiddenClass](#itemhiddenclass-) -* [itemPositioningClass](#itempositioningclass-) -* [itemDraggingClass](#itemdraggingclass-) -* [itemReleasingClass](#itemreleasingclass-) -* [itemPlaceholderClass](#itemplaceholderclass-) - -### items   - -The initial item elements, which should be children of the container element. All elements that are not children of the container will be appended to the container. You can provide an *array* of elements, a [*node list*](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or a selector (string). If you provide a selector Muuri uses it to filter the current child elements of the container element and sets them as initial items. By default all current child elements of the provided container element are used as initial items. - -* Default value: `'*'`. -* Accepted types: array (of elements), [node list](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), string, null. + items: '.item', +}); +``` + +

Grid options

+ +- [items](#grid-option-items) +- [showDuration](#grid-option-showduration) +- [showEasing](#grid-option-showeasing) +- [hideDuration](#grid-option-hideduration) +- [hideEasing](#grid-option-hideeasing) +- [visibleStyles](#grid-option-visiblestyles) +- [hiddenStyles](#grid-option-hiddenstyles) +- [layout](#grid-option-layout) +- [layoutOnResize](#grid-option-layoutonresize) +- [layoutOnInit](#grid-option-layoutoninit) +- [layoutDuration](#grid-option-layoutduration) +- [layoutEasing](#grid-option-layouteasing) +- [sortData](#grid-option-sortdata) +- [dragEnabled](#grid-option-dragenabled) +- [dragContainer](#grid-option-dragcontainer) +- [dragHandle](#grid-option-draghandle) +- [dragStartPredicate](#grid-option-dragstartpredicate) +- [dragAxis](#grid-option-dragaxis) +- [dragSort](#grid-option-dragsort) +- [dragSortHeuristics](#grid-option-dragsortheuristics) +- [dragSortPredicate](#grid-option-dragsortpredicate) +- [dragRelease](#grid-option-dragrelease) +- [dragCssProps](#grid-option-dragcssprops) +- [dragPlaceholder](#grid-option-dragplaceholder) +- [dragAutoScroll](#grid-option-dragautoscroll) +- [containerClass](#grid-option-containerclass) +- [itemClass](#grid-option-itemclass) +- [itemVisibleClass](#grid-option-itemvisibleclass) +- [itemHiddenClass](#grid-option-itemhiddenclass) +- [itemPositioningClass](#grid-option-itempositioningclass) +- [itemDraggingClass](#grid-option-itemdraggingclass) +- [itemReleasingClass](#grid-option-itemreleasingclass) +- [itemPlaceholderClass](#grid-option-itemplaceholderclass) + +

option: items

+ +The initial item elements, which should be children of the grid element. All elements that are not children of the grid element (e.g. if they are not in the DOM yet) will be appended to the grid element. You can provide an array of elements, [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), [HTMLCollection](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection) or a selector (string). If you provide a selector Muuri uses it to filter the current child elements of the container element and sets them as initial items. By default all current child elements of the provided grid element are used as initial items. + +- Default value: `'*'`. +- Accepted types: array (of elements), [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), [HTMLCollection](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection), string, null. + +**Examples** ```javascript // Use specific items. var grid = new Muuri(elem, { - items: [elemA, elemB, elemC] + items: [elemA, elemB, elemC], }); // Use node list. var grid = new Muuri(elem, { - items: elem.querySelectorAll('.item') + items: elem.querySelectorAll('.item'), }); // Use selector. var grid = new Muuri(elem, { - items: '.item' + items: '.item', }); ``` -### showDuration   +

option: showDuration

Show animation duration in milliseconds. Set to `0` to disable show animation. -* Default value: `300`. -* Accepted types: number. +- Default value: `300`. +- Accepted types: number. + +**Examples** ```javascript var grid = new Muuri(elem, { - showDuration: 600 + showDuration: 600, }); ``` -### showEasing   +

option: showEasing

Show animation easing. Accepts any valid [Animation easing](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffectTimingProperties/easing) value. -* Default value: `'ease'`. -* Accepted types: string. +- Default value: `'ease'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - showEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)' + showEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)', }); ``` -### hideDuration   +

option: hideDuration

Hide animation duration in milliseconds. Set to `0` to disable hide animation. -* Default value: `300`. -* Accepted types: number. +- Default value: `300`. +- Accepted types: number. + +**Examples** ```javascript var grid = new Muuri(elem, { - hideDuration: 600 + hideDuration: 600, }); ``` -### hideEasing   +

option: hideEasing

Hide animation easing. Accepts any valid [Animation easing](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffectTimingProperties/easing) value. -* Default value: `'ease'`. -* Accepted types: string. +- Default value: `'ease'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - hideEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)' + hideEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)', }); ``` -### visibleStyles   +

option: visibleStyles

-The styles that will be applied to all visible items. These styles are also used for the show/hide animations which means that you have to have the same style properties in visibleStyles and hiddenStyles options. Be sure to define all style properties camel cased. +The styles that will be applied to all visible items. These styles are also used for the show/hide animations which means that you have to have the same style properties in visibleStyles and hiddenStyles options. Be sure to define all style properties camel cased and without vendor prefixes (Muuri prefixes the properties automatically where needed). + +- Default value: + ```javascript + { + opacity: 1, + transform: 'scale(1)' + } + ``` +- Accepted types: object. -* Default value: `{opacity: 1, transform: 'scale(1)'}`. -* Accepted types: object. +**Examples** ```javascript var grid = new Muuri(elem, { visibleStyles: { opacity: 1, - transform: 'rotate(45deg)' + transform: 'rotate(45deg)', }, hiddenStyles: { opacity: 0, - transform: 'rotate(-45deg)' - } + transform: 'rotate(-45deg)', + }, }); ``` -### hiddenStyles   +

option: hiddenStyles

-The styles that will be applied to all hidden items. These styles are also used for the show/hide animations which means that you have to have the same style properties in visibleStyles and hiddenStyles options. Be sure to define all style properties camel cased. +The styles that will be applied to all hidden items. These styles are also used for the show/hide animations which means that you have to have the same style properties in visibleStyles and hiddenStyles options. Be sure to define all style properties camel cased and without vendor prefixes (Muuri prefixes the properties automatically where needed). -* Default value: `{opacity: 0, transform: 'scale(0.5)'}`. -* Accepted types: object. +- Default value: + ```javascript + { + opacity: 0, + transform: 'scale(0.5)' + } + ``` +- Accepted types: object. + +**Examples** ```javascript var grid = new Muuri(elem, { visibleStyles: { opacity: 1, - transform: 'rotate(45deg)' + transform: 'rotate(45deg)', }, hiddenStyles: { opacity: 0, - transform: 'rotate(-45deg)' - } + transform: 'rotate(-45deg)', + }, }); ``` -### layout   +

option: layout

+ +Define how the items will be positioned. Muuri ships with a configurable layout algorithm which is used by default. It's pretty flexible and suitable for most common situations (lists, grids and even bin packed grids). If that does not fit the bill you can always provide your own layout algorithm (it's not as scary as it sounds). -Define how the items will be laid out. Muuri ships with a configurable layout algorithm which is used by default. It's pretty flexible and suitable for most common situations (lists, grids and even bin packed grids). If that does not fit the bill you can always provide your own layout algorithm (it's not as scary as it sounds). +Muuri supports calculating the layout both synchronously and asynchronously. By default (if you use the default layout algorithm) Muuri will use two shared web workers to compute the layouts asynchronously. In browsers that do not support web workers Muuri will fallback to synchronous layout calculations. -* Default value: `{fillGaps: false, horizontal: false, alignRight: false, alignBottom: false}`. -* Accepted types: function, object. +- Default value: + ```javascript + { + fillGaps: false, + horizontal: false, + alignRight: false, + alignBottom: false, + rounding: false + } + ``` +- Accepted types: function, object. **Provide an _object_ to configure the default layout algorithm with the following properties** -* **fillGaps**  —  *boolean* - * Default value: `false`. - * When `true` the algorithm goes through every item in order and places each item to the first available free slot, even if the slot happens to be visually *before* the previous element's slot. Practically this means that the items might not end up visually in order, but there will be less gaps in the grid. By default this option is `false` which basically means that the following condition will be always true when calculating the layout (assuming `alignRight` and `alignBottom` are `false`): `nextItem.top > prevItem.top || (nextItem.top === prevItem.top && nextItem.left > prevItem.left)`. This also means that the items will be visually in order. -* **horizontal**  —  *boolean* - * Default value: `false`. - * When `true` the grid works in landscape mode (grid expands to the right). Use for horizontally scrolling sites. When `false` the grid works in "portrait" mode and expands downwards. -* **alignRight**  —  *boolean* - * Default value: `false`. - * When `true` the items are aligned from right to left. -* **alignBottom**  —  *boolean* - * Default value: `false`. - * When `true` the items are aligned from the bottom up. -* **rounding**  —  *boolean* - * Default value: `true`. - * When `true` the dimensions of the items will be automatically rounded for the layout calculations using `Math.round()`. Set to `false` to use accurate dimensions. In practice you would want disable this if you are using relative dimension values for items (%, em, rem, etc.). If you have defined item dimensions with pixel values (px) it is recommended that you leave this on. +- **fillGaps**  —  _boolean_ + - Default value: `false`. + - When `true` the algorithm goes through every item in order and places each item to the first available free slot, even if the slot happens to be visually _before_ the previous element's slot. Practically this means that the items might not end up visually in order, but there will be less gaps in the grid. +- **horizontal**  —  _boolean_ + - Default value: `false`. + - When `true` the grid works in landscape mode (grid expands to the right). Use for horizontally scrolling sites. When `false` the grid works in "portrait" mode and expands downwards. +- **alignRight**  —  _boolean_ + - Default value: `false`. + - When `true` the items are aligned from right to left. +- **alignBottom**  —  _boolean_ + - Default value: `false`. + - When `true` the items are aligned from the bottom up. +- **rounding**  —  _boolean_ + - Default value: `false`. + - When `true` the item dimensions are rounded to a precision of two decimals for the duration of layout calculations. This procedure stabilizes the layout calculations quite a lot, but also causes a hit on performance. Use only if you see your layout behaving badly, which might happen sometimes (hopefully never) when using relative dimension values. + +**Provide a _function_ to use a custom layout algorithm** + +When you provide a custom layout function Muuri calls it whenever calculation of layout is necessary. Before calling the layout function Muuri always calculates the current width and height of the grid element and also creates an array of all the items that are part of the layout currently (all _active_ items). + +The layout function always receives the following arguments: + +- **grid**  —  _Muuri_ + - The grid instance that requested the layout. +- **layoutId**  —  _number_ + - Automatically generated unique id for the layout which is used to keep track of the layout requests and to make sure that the correct layout gets applied at correct time. +- **items**  —  _array_ + - Array of `Muuri.Item` instances. A new array instance is created for each layout so there's no harm in manipulating this if you need to (or using it as is for the layout data object). +- **width**  —  _number_ + - Current width (in pixels) of the grid element (excluding borders, but including padding). +- **height**  —  _number_ + - Current height (in pixels) of the grid element (excluding borders, but including padding). +- **callback**  —  _function_ + - When the layout is calculated and ready to be applied you need to call this callback function and provide a _layout object_ as it's argument. Note that if another layout is requesteded while the current layout is still being calculated (asynchronously) this layout will be ignored. + +If the layout function's calculations are asynchronous you can optionally return a cancel function, which Muuri will call if there is a new layout request before the current layout has finished it's calculations. + +The layout object, which needs to be provided to the callback, must include the following properties. + +- **id**  —  _number_ + - The layout's unique id (must be the `layoutId` provided by Muuri). +- **items**  —  _array_ + - Array of the active item instances that are part of the layout. You can pass the same `items` array here which is provided by Muuri (in case you haven't mutated it). This array items must be identical to the array of items provided by Muuri. +- **slots**  —  _array_ + - Array of the item positions (numbers). E.g. if the items were `[a, b]` this should be `[aLeft, aTop, bLeft, bTop]`. You have to calculate the `left` and `top` position for each item in the provided _items_ array in the same order the items are provided. +- **styles**  —  _object / null_ + - Here you can optionally define all the layout related CSS styles that should be applied to the grid element _just_ before the `layoutStart` event is emitted. E.g. `{width: '100%', height: '200px', minWidth: '200px'}`. + - It's important to keep in mind here that if the grid element's `box-sizing` CSS property is set to `border-box` the element's borders are included in the dimensions. E.g. if you set `{width: '100px', width: '100px'}` here and the element has a `5px` border and `box-sizing` is set to `border-box`, then the _layout's_ effective `width` and `height` (as perceived by Muuri) will be `90px`. So remember to take that into account and add the borders to the dimensions when necessary. If this sounds complicated then just don't set borders directly to the grid element or make sure that grid element's `box-sizing` is set to `content-box` (which is the default value). + +Note that you can add additional properties to the layout object if you wish, e.g. the default layout algorithm also stores the layout's width and height (in pixels) to the layout object. + +**Examples** ```javascript // Customize the default layout algorithm. @@ -456,156 +555,150 @@ var grid = new Muuri(elem, { horizontal: true, alignRight: true, alignBottom: true, - rounding: false - } + rounding: true, + }, }); ``` -**Provide a _function_ to use a custom layout algorithm** - -When you provide a custom layout function Muuri calls it whenever calculation of layout is necessary. Before calling the layout function Muuri always calculates the current width and height of the grid's container element and also creates an array of all the items that are part of the layout currently (all _active_ items). - -* `customLayout( items, gridWidth, gridHeight )` - * **items**  —  *array* - * Array of `Muuri.Item` instances. - * **gridWidth**  —  *number* - * Current width of the grid's container element. - * **gridHeight**  —  *number* - * Current height of the grid's container element. - -The layout function's job is using this data, which is provided to the layout function as arguments (as detailed above), and calculating position for each item in the array. - -The layout function should _always_ return an object with following properties: - -* **slots**  —  *array* - * Array of the item positions (numbers). E.g. if the items were `[a, b]` this should be `[aLeft, aTop, bLeft, bTop]`. You have to calculate the `left` and `top` position for each item in the provided _items_ array in the same order the items are provided. -* **width**  —  *number* - * The width of the grid. -* **height**  —  *number* - * The height of the grid. -* **setWidth**  —  *boolean* - * Should Muuri set the provided _width_ as the grid element's width? -* **setHeight**  —  *boolean* - * Should Muuri set the provided _height_ as the grid element's height? - ```javascript // Build your own layout algorithm. var grid = new Muuri(elem, { - layout: function (items, gridWidth, gridHeight) { - // The layout data object. Muuri will read this data and position the items - // based on it. + layout: function (grid, layoutId, items, width, height, callback) { var layout = { - // The layout's item slots (left/top coordinates). + id: layoutId, + items: items, slots: [], - // The layout's total width. - width: 0, - // The layout's total height. - height: 0, - // Should Muuri set the grid's width after layout? - setWidth: true, - // Should Muuri set the grid's height after layout? - setHeight: true + styles: {}, }; - // Calculate the slots. - var item; - var m; - var x = 0; - var y = 0; - var w = 0; - var h = 0; - for (var i = 0; i < items.length; i++) { - item = items[i]; - x += w; - y += h; - m = item.getMargin(); - w = item.getWidth() + m.left + m.right; - h = item.getHeight() + m.top + m.bottom; - layout.slots.push(x, y); - } - - // Calculate the layout's total width and height. - layout.width = x + w; - layout.height = y + h; - - return layout; - } + // Calculate the slots asynchronously. Note that the timeout is here only + // as an example and does not help at all in the calculations. You should + // offload the calculations to web workers if you want real benefits. + // Also note that doing asynchronous layout is completely optional and you + // can call the callback function synchronously also. + var timerId = window.setTimeout(function () { + var item, + m, + x = 0, + y = 0, + w = 0, + h = 0; + + for (var i = 0; i < items.length; i++) { + item = items[i]; + x += w; + y += h; + m = item.getMargin(); + w = item.getWidth() + m.left + m.right; + h = item.getHeight() + m.top + m.bottom; + layout.slots.push(x, y); + } + + w += x; + h += y; + + // Set the CSS styles that should be applied to the grid element. + layout.styles.width = w + 'px'; + layout.styles.height = h + 'px'; + + // When the layout is fully computed let's call the callback function and + // provide the layout object as it's argument. + callback(layout); + }, 200); + + // If you are doing an async layout you _can_ (if you want to) return a + // function that cancels this specific layout calculations if it's still + // processing/queueing when the next layout is requested. + return function () { + window.clearTimeout(timerId); + }; + }, }); ``` -### layoutOnResize   +

option: layoutOnResize

+ +Should Muuri automatically trigger `layout` method on window resize? Set to `false` to disable. When a number or `true` is provided Muuri will automatically position the items every time window is resized. The provided number (`true` is transformed to `0`) equals to the amount of time (in milliseconds) that is waited before items are positioned after each window resize event. -Should Muuri automatically trigger `layout` method on window resize? Set to `false` to disable. When a number or `true` is provided Muuri will automatically lay out the items every time window is resized. The provided number (`true` is transformed to `0`) equals to the amount of time (in milliseconds) that is waited before items are laid out after each window resize event. +- Default value: `150`. +- Accepted types: boolean, number. -* Default value: `100`. -* Accepted types: boolean, number. +**Examples** ```javascript // No layout on resize. var grid = new Muuri(elem, { - layoutOnResize: false + layoutOnResize: false, }); ``` ```javascript // Layout on resize (instantly). var grid = new Muuri(elem, { - layoutOnResize: true + layoutOnResize: true, }); ``` ```javascript // Layout on resize (with 200ms debounce). var grid = new Muuri(elem, { - layoutOnResize: 200 + layoutOnResize: 200, }); ``` -### layoutOnInit   +

option: layoutOnInit

Should Muuri trigger `layout` method automatically on init? -* Default value: `true`. -* Accepted types: boolean. +- Default value: `true`. +- Accepted types: boolean. + +**Examples** ```javascript var grid = new Muuri(elem, { - layoutOnInit: false + layoutOnInit: false, }); ``` -### layoutDuration   +

option: layoutDuration

The duration for item's layout animation in milliseconds. Set to `0` to disable. -* Default value: `300`. -* Accepted types: number. +- Default value: `300`. +- Accepted types: number. + +**Examples** ```javascript var grid = new Muuri(elem, { - layoutDuration: 600 + layoutDuration: 600, }); ``` -### layoutEasing   +

option: layoutEasing

The easing for item's layout animation. Accepts any valid [Animation easing](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffectTimingProperties/easing) value. -* Default value: `'ease'`. -* Accepted types: string. +- Default value: `'ease'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - layoutEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)' + layoutEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)', }); ``` -### sortData   +

option: sortData

The sort data getter functions. Provide an object where the key is the name of the sortable attribute and the function returns a value (from the item) by which the items can be sorted. -* Default value: `null`. -* Accepted types: object, null. +- Default value: `null`. +- Accepted types: object, null. + +**Examples** ```javascript var grid = new Muuri(elem, { @@ -615,8 +708,8 @@ var grid = new Muuri(elem, { }, bar: function (item, element) { return element.getAttribute('data-bar').toUpperCase(); - } - } + }, + }, }); // Refresh sort data whenever an item's data-foo or data-bar changes grid.refreshSortData(); @@ -624,59 +717,83 @@ grid.refreshSortData(); grid.sort('foo bar'); ``` -### dragEnabled   +

option: dragEnabled

Should items be draggable? -* Default value: `false`. -* Accepted types: boolean. +- Default value: `false`. +- Accepted types: boolean. + +**Examples** ```javascript var grid = new Muuri(elem, { - dragEnabled: true + dragEnabled: true, }); ``` -### dragContainer   +

option: dragContainer

+ +The element the dragged item should be appended to for the duration of the drag. If set to `null` (which is also the default value) the grid element will be used. -The element the dragged item should be appended to for the duration of the drag. If set to `null` (which is also the default value) the grid's container element will be used. +- Default value: `null`. +- Accepted types: element, null. -* Default value: `null`. -* Accepted types: element, null. +**Examples** ```javascript var grid = new Muuri(elem, { - dragContainer: document.body + dragContainer: document.body, }); ``` -### dragStartPredicate   +

option: dragHandle

-A function that determines when the item should start to move when the item is being dragged. By default uses the built-in predicate which has some configurable options. +The element within the item element that should be used as the drag handle. This should be a CSS selector which will be fed to `element.querySelector()` as is to obtain the handle element when the item is instantiated. If no valid element is found or if this is `null` Muuri will use the item element as the handle. -* Default value: `{distance: 0, delay: 0, handle: false}`. -* Accepted types: function, object. +- Default value: `null`. +- Accepted types: string, null. -If an object is provided the default sort predicate handler will be used. You can define the following properties: +**Examples** + +```javascript +var grid = new Muuri(elem, { + dragHandle: '.handle', +}); +``` + +

option: dragStartPredicate

+ +A function that determines when the item should start moving when the item is being dragged. By default uses the built-in start predicate which has some configurable options. + +- Default value: + ```javascript + { + distance: 0, + delay: 0 + } + ``` +- Accepted types: function, object. + +If an object is provided the default start predicate handler will be used. You can define the following properties: -* **distance**  —  *number* - * Default value: `0`. - * How many pixels must be dragged before the dragging starts. -* **delay**  —  *number* - * Default value: `0`. - * How long (in milliseconds) the user must drag before the dragging starts. -* **handle**  —  *string / boolean* - * Default value: `false`. - * The selector(s) which much match the event target element for the dragging to start. Note that if the event target element is a descendant of the handle element(s) it is also considered a match, which should be pretty useful in most scenarios. +- **distance**  —  _number_ + - Default value: `0`. + - How many pixels the user must drag before the drag procedure starts and the item starts moving. +- **delay**  —  _number_ + - Default value: `0`. + - How long (in milliseconds) the user must drag before the dragging starts. -If you provide a function you can totally customize the drag start logic. When the user starts to drag an item this predicate function will be called until you return `true` or `false`. If you return `true` the item will begin to move whenever the item is dragged. If you return `false` the item will not be moved at all. Note that after you have returned `true` or `false` this function will not be called until the item is released and dragged again. +If you provide a function you can customize the drag start logic as you please. When the user starts to drag an item this predicate function will be called until you return `true` or `false`. If you return `true` the item will begin to move whenever the item is dragged. If you return `false` the item will not be moved at all. Note that after you have returned `true` or `false` this function will not be called until the item is released and dragged again. The predicate function receives two arguments: -* **item**  —  *Muuri.Item* - * The item that's being dragged. -* **event**  —  *object* - * The drag event (Muuri.Dragger event). +- **item**  —  _Muuri.Item_ + - The item that's being dragged. +- **event**  —  _object_ + - Muuri.Dragger event data. + +**Examples** ```javascript // Configure the default predicate @@ -684,8 +801,7 @@ var grid = new Muuri(elem, { dragStartPredicate: { distance: 10, delay: 100, - handle: '.foo, .bar' - } + }, }); ``` @@ -697,7 +813,7 @@ var grid = new Muuri(elem, { if (e.deltaTime > 1000) { return true; } - } + }, }); ``` @@ -715,84 +831,97 @@ var grid = new Muuri(elem, { return; } - // Prevent first item from being dragged. - if (grid.getItems().indexOf(item) === 0) { + // Prevent first item from being dragged. + if (grid.getItems()[0] === item) { return false; } // For other items use the default drag start predicate. return Muuri.ItemDrag.defaultStartPredicate(item, e); - } + }, }); ``` -### dragAxis   +

option: dragAxis

Force items to be moved only vertically or horizontally when dragged. Set to `'x'` for horizontal movement and to `'y'` for vertical movement. By default items can be dragged both vertically and horizontally. -* Default value: `null`. -* Accepted types: string. -* Allowed values: `'x'`, `'y'`. +- Default value: `'xy'`. +- Accepted types: string. +- Allowed values: `'x'`, `'y'`, `'xy'`. + +**Examples** ```javascript // Move items only horizontally when dragged. var grid = new Muuri(elem, { - dragAxis: 'x' + dragAxis: 'x', }); ``` ```javascript // Move items only vertically when dragged. var grid = new Muuri(elem, { - dragAxis: 'y' + dragAxis: 'y', }); ``` -### dragSort   +

option: dragSort

Should the items be sorted during drag? A simple boolean will do just fine here. -Alternatively you can do some advanced stuff and control within which grids a specific item can be sorted and dragged into. To do that you need to provide a function which receives the dragged item as its first argument and should return an array of grid instances. An important thing to note here is that you need to return *all* the grid instances you want the dragged item to sort within, even the current grid instance. If you return an empty array the dragged item will not cause sorting at all. +Alternatively you can do some advanced stuff and control within which grids a specific item can be sorted and dragged into. To do that you need to provide a function which receives the dragged item as its first argument and should return an array of grid instances. An important thing to note here is that you need to return _all_ the grid instances you want the dragged item to sort within, even the current grid instance. If you return an empty array the dragged item will not cause sorting at all. + +- Default value: `true`. +- Accepted types: boolean, function. -* Default value: `true`. -* Accepted types: boolean, function. +**Examples** ```javascript // Disable drag sorting. var grid = new Muuri(elem, { - dragSort: false + dragSort: false, }); ``` ```javascript // Multigrid drag sorting. -var gridA = new Muuri(elemA, {dragSort: getAllGrids}); -var gridB = new Muuri(elemB, {dragSort: getAllGrids}); -var gridC = new Muuri(elemC, {dragSort: getAllGrids}); +var gridA = new Muuri(elemA, { dragSort: getAllGrids }); +var gridB = new Muuri(elemB, { dragSort: getAllGrids }); +var gridC = new Muuri(elemC, { dragSort: getAllGrids }); function getAllGrids(item) { return [gridA, gridB, gridC]; } ``` -### dragSortHeuristics   +

option: dragSortHeuristics

Defines various heuristics so that sorting during drag would be smoother and faster. -* Default value: `{sortInterval: 100, minDragDistance: 10, minBounceBackAngle: 1}`. -* Accepted types: object. +- Default value: + ```javascript + { + sortInterval: 100, + minDragDistance: 10, + minBounceBackAngle: 1 + } + ``` +- Accepted types: object. You can define the following properties: -* **sortInterval**  —  *number* - * Default value: `100`. - * Defines the amount of time the dragged item must be still before `dragSortPredicate` function is called. -* **minDragDistance**  —  *number* - * Default value: `10`. - * Defines how much (in pixels) the item must be dragged before `dragSortPredicate` can be called. -* **minBounceBackAngle**  —  *number* - * Default value: `1`. - * Defines the minimum angle (in radians) of the delta vector between the last movement vector and the current movement vector that is required for the dragged item to be allowed to be sorted to it's previous index. The problem this heuristic is trying to solve is the scenario where you drag an item over a much bigger item and the bigger item moves, but it's still overlapping the dragged item after repositioning. Now when you move the dragged item again another sort is triggered and the bigger item moves back to it's previous position. This bouncing back and forth can go on for quite a while and it looks quite erratic. The fix we do here is that, by default, we disallow an item to be moved back to it's previous position, unless it's drag direction changes enough. And what is enough? That's what you can define here. Note that this option works in tandem with `minDragDistance` and needs it to be set to `3` at minimum to be enabled at all. +- **sortInterval**  —  _number_ + - Default value: `100`. + - Defines the amount of time the dragged item must be still before `dragSortPredicate` function is called. +- **minDragDistance**  —  _number_ + - Default value: `10`. + - Defines how much (in pixels) the item must be dragged before `dragSortPredicate` can be called. +- **minBounceBackAngle**  —  _number_ + - Default value: `1`. + - Defines the minimum angle (in radians) of the delta vector between the last movement vector and the current movement vector that is required for the dragged item to be allowed to be sorted to it's previous index. The problem this heuristic is trying to solve is the scenario where you drag an item over a much bigger item and the bigger item moves, but it's still overlapping the dragged item after repositioning. Now when you move the dragged item again another sort is triggered and the bigger item moves back to it's previous position. This bouncing back and forth can go on for quite a while and it looks quite erratic. The fix we do here is that, by default, we disallow an item to be moved back to it's previous position, unless it's drag direction changes enough. And what is enough? That's what you can define here. Note that this option works in tandem with `minDragDistance` and needs it to be set to `3` at minimum to be enabled at all. + +**Examples** ```javascript var grid = new Muuri(elem, { @@ -800,56 +929,81 @@ var grid = new Muuri(elem, { dragSortHeuristics: { sortInterval: 10, minDragDistance: 5, - minBounceBackAngle: Math.PI / 2 - } + minBounceBackAngle: Math.PI / 2, + }, }); ``` -### dragSortPredicate   +```javascript +// Pro tip: If you want drag sorting happening only on release set a really +// long sortInterval. A bit of a hack, but works. +var grid = new Muuri(elem, { + dragEnabled: true, + dragSortHeuristics: { + sortInterval: 3600000, // 1 hour + }, +}); +``` + +

option: dragSortPredicate

Defines the logic for the sort procedure during dragging an item. -* Default value: `{action: 'move', threshold: 50}`. -* Accepted types: function, object. +- Default value: + ```javascript + { + threshold: 50, + action: 'move', + migrateAction: 'move' + } + ``` +- Accepted types: function, object. If an object is provided the default sort predicate handler will be used. You can define the following properties: -* **action**  —  *string* - * Default value: `'move'`. - * Allowed values: `'move'`, `'swap'`. - * Should the dragged item be *moved* to the new position or should it *swap* places with the item it overlaps? -* **threshold**  —  *number* - * Default value: `50`. - * Allowed values: `1` - `100`. - * How many percent the intersection area between the dragged item and the compared item should be from the maximum potential intersection area between the items before sorting is triggered. + +- **threshold**  —  _number_ + - Default value: `50`. + - Allowed values: `1` - `100`. + - How many percent the intersection area between the dragged item and the compared item should be from the maximum potential intersection area between the items before sorting is triggered. +- **action**  —  _string_ + - Default value: `'move'`. + - Allowed values: `'move'`, `'swap'`. + - Should the dragged item be _moved_ to the new position or should it _swap_ places with the item it overlaps when the drag occurs within the same grid? +- **migrateAction**  —  _string_ + - Default value: `'move'`. + - Allowed values: `'move'`, `'swap'`. + - Should the dragged item be _moved_ to the new position or should it _swap_ places with the item it overlaps when the dragged item is moved to another grid? Alternatively you can provide your own callback function where you can define your own custom sort logic. The callback function receives two arguments: -* **item**  —  *Muuri.Item* - * The item that's being dragged. -* **event**  —  *object* - * The drag event (Muuri.Dragger event). +- **item**  —  _Muuri.Item_ + - The item that's being dragged. +- **event**  —  _object_ + - Muuri.Dragger event data. -The callback should return a *falsy* value if sorting should not occur. If, however, sorting should occur the callback should return an object containing the following properties: +The callback should return a _falsy_ value if sorting should not occur. If, however, sorting should occur the callback should return an object containing the following properties: -* **index**  —  *number* - * The index where the item should be moved to. -* **grid**  —  *Muuri* - * The grid where the item should be moved to. - * Defaults to the item's current grid. - * Optional. -* **action**  —  *string* - * The movement method. - * Default value: `'move'`. - * Allowed values: `'move'` or `'swap'`. - * Optional. +- **index**  —  _number_ + - The index where the item should be moved to. +- **grid**  —  _Muuri_ + - The grid where the item should be moved to. + - Defaults to the item's current grid. + - Optional. +- **action**  —  _string_ + - The movement method. + - Default value: `'move'`. + - Allowed values: `'move'` or `'swap'`. + - Optional. + +**Examples** ```javascript // Customize the default predicate. var grid = new Muuri(elem, { dragSortPredicate: { threshold: 90, - action: 'swap' - } + action: 'swap', + }, }); ``` @@ -860,9 +1014,9 @@ var grid = new Muuri(elem, { if (e.deltaTime < 1000) return false; return { index: Math.round(e.deltaTime / 1000) % 2 === 0 ? -1 : 0, - action: 'swap' + action: 'swap', }; - } + }, }); ``` @@ -873,70 +1027,92 @@ var grid = new Muuri(elem, { if (item.classList.contains('no-sort')) return false; return Muuri.ItemDrag.defaultSortPredicate(item, { action: 'swap', - threshold: 75 + threshold: 75, }); - } + }, }); ``` -### dragReleaseDuration   - -The duration for item's drag release animation. Set to `0` to disable. +

option: dragRelease

-* Default value: `300`. -* Accepted types: number. - -```javascript -var grid = new Muuri(elem, { - dragReleaseDuration: 600 -}); -``` +- Default value: + ```javascript + { + duration: 300, + easing: 'ease', + useDragContainer: true + } + ``` +- Accepted types: object. -### dragReleaseEasing   +You can define the following properties: -The easing for item's drag release animation. Accepts any valid [Animation easing](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffectTimingProperties/easing) value. +- **duration**  —  _number_ + - Default value: `300`. + - The duration for item's drag release animation. Set to `0` to disable. +- **easing**  —  _string_ + - Default value: `'ease'`. + - The easing for item's drag release animation. Accepts any valid [Animation easing](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffectTimingProperties/easing) value. +- **useDragContainer**  —  _boolean_ + - Default value: `true`. + - If `true` the item element will remain within the `dragContainer` for the duration of the release process. Otherwise the item element will be inserted within the grid element (if not already inside it) at the beginning of the release process. -* Default value: `'ease'`. -* Accepted types: string. +**Examples** ```javascript var grid = new Muuri(elem, { - dragReleaseEasing: 'ease-out' + dragRelease: { + duration: 600, + easing: 'ease-out', + useDragContainer: false, + }, }); ``` -### dragCssProps   +

option: dragCssProps

Drag specific CSS properties that Muuri sets to the draggable item elements. Muuri automatically prefixes the properties before applying them to the element. `touchAction` property is required to be always defined, but the other properties are optional and can be omitted by setting their value to an empty string if you want to e.g. define them via CSS only. -* Default value: `{touchAction: 'none', userSelect: 'none', userDrag: 'none', tapHighlightColor: 'rgba(0, 0, 0, 0)', touchCallout: 'none', contentZooming: 'none'}`. -* Accepted types: object. +- Default value: + ```javascript + { + touchAction: 'none', + userSelect: 'none', + userDrag: 'none', + tapHighlightColor: 'rgba(0, 0, 0, 0)', + touchCallout: 'none', + contentZooming: 'none' + } + ``` +- Accepted types: object. You can define the following properties: -* **touchAction**  —  *string* - * Default value: `'none'`. - * https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action -* **userSelect**  —  *string* - * Default value: `'none'`. - * https://developer.mozilla.org/en-US/docs/Web/CSS/user-select - * Optional. -* **userDrag**  —  *string* - * Default value: `'none'`. - * http://help.dottoro.com/lcbixvwm.php - * Optional. -* **tapHighlightColor**  —  *string* - * Default value: `'rgba(0, 0, 0, 0)'`. - * https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color - * Optional. -* **touchCallout**  —  *string* - * Default value: `'none'`. - * https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-touch-callout - * Optional. -* **contentZooming**  —  *string* - * Default value: `'none'`. - * https://developer.mozilla.org/en-US/docs/Web/CSS/-ms-content-zooming - * Optional. +- **touchAction**  —  _string_ + - Default value: `'none'`. + - https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action +- **userSelect**  —  _string_ + - Default value: `'none'`. + - https://developer.mozilla.org/en-US/docs/Web/CSS/user-select + - Optional. +- **userDrag**  —  _string_ + - Default value: `'none'`. + - http://help.dottoro.com/lcbixvwm.php + - Optional. +- **tapHighlightColor**  —  _string_ + - Default value: `'rgba(0, 0, 0, 0)'`. + - https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color + - Optional. +- **touchCallout**  —  _string_ + - Default value: `'none'`. + - https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-touch-callout + - Optional. +- **contentZooming**  —  _string_ + - Default value: `'none'`. + - https://developer.mozilla.org/en-US/docs/Web/CSS/-ms-content-zooming + - Optional. + +**Examples** ```javascript // Only set the required touch-action CSS property via the options if you for @@ -949,38 +1125,42 @@ var grid = new Muuri(elem, { userDrag: '', tapHighlightColor: '', touchCallout: '', - contentZooming: '' - } + contentZooming: '', + }, }); ``` -### dragPlaceholder   +

option: dragPlaceholder

-If you want a placeholder item to appear for the duration of an item's drag & drop procedure you can enable and configure it here. +If you want a placeholder item to appear for the duration of an item's drag & drop procedure you can enable and configure it here. The placeholder animation duration is fetched from the grid's `layoutDuration` option and easing from the grid's `layoutEasing` option. Note that a special placeholder class is given to all drag placeholders and is customizable via [itemPlaceholderClass](#itemplaceholderclass-) option. -* Default value: `{enabled: false, duration: 300, easing: 'ease', createElement: null, onCreate: null, onRemove: null}`. -* Accepted types: object. +- Default value: + ```javascript + { + enabled: false, + createElement: null, + onCreate: null, + onRemove: null + } + ``` +- Accepted types: object. You can define the following properties: -* **enabled**  —  *boolean* - * Default value: `false`. - * Is the placeholder enabled? -* **duration**  —  *number* - * Default value: `300`. - * The duration for placeholder's positioning animation. Set to `0` to disable. -* **easing**  —  *string* - * Default value: `'ease'`. - * The easing for placeholder's positioning animation. Accepts any valid [Animation easing](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffectTimingProperties/easing) value. -* **createElement**  —  *function / null* - * Default value: `null`. - * If defined, this method will be used to create the DOM element that is used for the placeholder. By default a new `div` element is created when a placeholder is summoned. -* **onCreate**  —  *function / null* - * Default value: `null`. - * An optional callback that will be called after a placeholder is created for an item. -* **onRemove**  —  *function / null* - * Default value: `null`. - * An optional callback that will be called after a placeholder is removed from the grid. +- **enabled**  —  _boolean_ + - Default value: `false`. + - Is the placeholder enabled? +- **createElement**  —  _function / null_ + - Default value: `null`. + - If defined, this method will be used to create the DOM element that is used for the placeholder. By default a new `div` element is created when a placeholder is summoned. +- **onCreate**  —  _function / null_ + - Default value: `null`. + - An optional callback that will be called after a placeholder is created for an item. +- **onRemove**  —  _function / null_ + - Default value: `null`. + - An optional callback that will be called after a placeholder is removed from the grid. + +**Examples** ```javascript // This example showcases how to pool placeholder elements @@ -992,8 +1172,6 @@ var grid = new Muuri(elem, { dragEnabled: true, dragPlaceholder: { enabled: true, - duration: 400, - easing: 'ease-out', createElement(item) { return phPool.pop() || phElem.cloneNode(); }, @@ -1002,160 +1180,369 @@ var grid = new Muuri(elem, { // placeholder is fully created, here's // the place to do it. }, - onRemove (item, element) { + onRemove(item, element) { phPool.push(element); - } + }, + }, +}); +``` + +

option: dragAutoScroll

+ +If you want to trigger scrolling on any element during dragging you can enable and configure it here. By default this feature is disabled. When you use this feature it is _highly_ recommended that you create a `fixed` positioned element right under `document.body` and use that as the `dragContainer` for all the dragged items. If you don't do this and a dragged item's parent is auto-scrolled, the dragged item will potentially grow the scrolled element's scroll area to infinity unintentionally. + +- Default value: + ```javascript + { + targets: [], + handle: null, + threshold: 50, + speed: Muuri.AutoScroller.smoothSpeed(1000, 2000, 2500), + sortDuringScroll: true, + smoothStop: false, + onStart: null, + onStop: null } + ``` +- Accepted types: object. + +You can define the following properties: + +- **targets**  —  _array / function_ + - Default value: `[]`. + - Define the DOM elements that should be scrolled during drag. As long as this array is empty there will be no scrolling during drag. To keep it simple you can just provide an array of elements here, in which case Muuri attempts to scroll the elements both vertically and horizontally when possible. If you want more fine-grained control, e.g. scroll an element only on specific axis or prioritize some element over another (handy for cases when there are overlapping elements), you can provide an array of scroll targets (objects). Finally, you can also provide a function which receives the dragged `item` instance as it's argument and which should return an array of scroll targets (elements and/or objects). This way you can provide different configurations for different items. + - **scrollTarget**  —  _object_ + - **element**  —  _element_ / _window_ + - The DOM element to scroll. + - Required. + - **axis**  —  _number_ + - Optional. Defaults to scrolling both axes: `Muuri.AutoScroller.AXIS_X | Muuri.AutoScroller.AXIS_Y`. + - To scroll only x-axis: `Muuri.AutoScroller.AXIS_X`. + - To scroll only y-axis: `Muuri.AutoScroller.AXIS_Y`. + - **priority**  —  _number_ + - Default: `0`. + - A dragged item can only scroll one element horizontally and one element vertically simultaneously. This is an artificial limit to fend off unnecesary complexity, and to avoid awkward situations. In the case where the dragged item overlaps multiple scrollable elements simultaneously and exceeds their scroll thresholds we pick the one that the dragged item overlaps most. However, that's not always the best choice. This is where `priority` comes in. Here you can manually tell Muuri which element to prefer over another in these scenarios. The element with highest priority _always_ wins the fight, in matches with equal priority we determine the winner by the amount of overlap. + - Optional. + - **threshold**  —  _number / null_ + - Default: `null`. + - If defined (a number is provided), this value will override the default threshold for _this scroll target_. Otherwise the default threshold will be used. + - Optional. +- **handle**  —  _function / null_ + - Default value: `null`. + - This property defines size and position of the handle (the rectangle that is compared against the scroll element's threshold). By default (when `null`) the dragged element's dimensions and offsets are used. However, you can provide a function which should return an object containing the handle's client offsets in pixels (`left` and `top`) and dimensions in pixels (`width` and `height`). The function receives the following arguments: + - **item**  —  _Muuri.Item_ + - **itemClientX**  —  _number_ + - **itemClientY**  —  _number_ + - **itemWidth**  —  _number_ + - **itemHeight**  —  _number_ + - **pointerClientX**  —  _number_ + - **pointerClientY**  —  _number_ + - Tip: Use `Muuri.AutoScroller.pointerHandle(pointerSize)` utility method if you want to use the pointer (instead of the element) as the handle. +- **threshold**  —  _number_ + - Default value: `50`. + - Defines the distance (in pixels) from the edge of the scrollable element when scrolling should start, in pixels. If this value is `0` the scrolling will start when the dragged element reaches the scrollable element's edge. Do note that Muuri dynamically adjusts the scroll element's _edge_ for the calculations (when needed). +- **safeZone**  —  _number_ + - Default value: `0.2`. + - Defines the size of the minimum "safe zone" space, an area in the center of the scrollable element that will be guaranteed not trigger scrolling regardless of threshold size and the dragged item's size. This value is a percentage of the scrollable element's size (width and/or height depending on the scroll axes), and should be something between `0` and `1`. So in practice, if you set this to e.g `0.5` the safe zone would be 50% of the scrollable element's width and/or height. +- **speed**  —  _number / function_ + - Default value: `Muuri.AutoScroller.smoothSpeed(1000, 2000, 2500)`. + - Defines the scrolling speed in pixels per second. You can provide either static speed with a `number` or dynamic speed with a `function`. The function is called before every scroll operation and should return the speed (`number`, pixels per second) for the next scroll operation. The function receives three arguments: + - **item**  —  _Muuri.Item_ + - The dragged `Muuri.Item` instance. + - **scrollElement**  —  _element_ / _window_ + - The scrolled element. + - **data**  —  _object_ + - **data.direction**  —  _number_ + - The direction of the scroll, one of the following: `Muuri.AutoScroller.LEFT`, `Muuri.AutoScroller.RIGHT`, `Muuri.AutoScroller.UP`, `Muuri.AutoScroller.DOWN`. + - **data.threshold**  —  _number_ + - The current threshold in pixels. + - **data.distance**  —  _number_ + - The handle rectangle's (as defined in `handle` option) current distance from the edge of the scroll element. E.g, if `direction` is `Muuri.AutoScroller.RIGHT` then distance is `scrollElement.getBoundingClientRect().right - handleRect.right`, and if `direction` is `Muuri.AutoScroller.LEFT` then distance is `handleRect.left - scrollElement.getBoundingClientRect().left`. Can be a negative value too. + - **data.value**  —  _number_ + - The scroll element's current scroll value on the scrolled axis. + - **data.maxValue**  —  _number_ + - The scroll element's maximum scroll value on the scrolled axis. + - **data.duration**  —  _number_ + - How long (in milliseconds) this specific auto-scroll operation has lasted so far. + - **data.speed**  —  _number_ + - The current speed as pixels per second. + - **data.deltaTime**  —  _number_ + - `requestAnimationFrame`'s delta time (in milliseconds). + - **data.isEnding**  —  _boolean_ + - Is the scroll process ending? When this is `true` it means that the associated drag item does not satisfy the threshold anymore. You should now start decreasing the speed towards `0` to allow the item to come to rest smoothly. + - Pro tip: Use `Muuri.AutoScroller.smoothSpeed()` for dynamic speed that provides a smooth scrolling experience. When executed it creates and returns a speed function which you can directly provide for `speed` option. The method _requires_ three arguments (in the following order): + - **maxSpeed**  —  _number_ + - The maximum speed (pixels per second) when the handle's distance to the scroll target's edge is `0` or less. + - **acceleration**  —  _number_ + - How fast the the speed may accelerate (pixels per second). + - **deceleration**  —  _number_ + - How fast the the speed may decelerate (pixels per second). +- **sortDuringScroll**  —  _boolean_ + - Default value: `true`. + - Should the grid items be sorted during auto-scroll or not? +- **smoothStop**  —  _boolean_ + - Default value: `false`. + - When a dragged item is moved out of the threshold area the scroll process is set to _ending_ state. However, it's up to you to decide if the actual scrolling motion is stopped gradually or instantly. By default, when this is `false`, scrolling will stop immediately. If you set this to `true` scrolling will continue until speed drops to `0`. When this option is `enabled` you _must_ handle decelerating the speed to `0` manually within `speed` function, so do not enable this option if you use a static speed value. The default `speed` function handles the deceleration automatically. +- **onStart**  —  _null / function_ + - Default value: `null`. + - Optionally, you can provide a callback that will be called when an item starts auto-scrolling a scroll target. The callback function will receive the following arguments: + - **item**  —  _Muuri.Item_ + - The dragged `Muuri.Item` instance. + - **scrollElement**  —  _element_ / _window_ + - The scrolled element. + - **direction**  —  _number_ + - The direction of the scroll, one of the following: `Muuri.AutoScroller.LEFT`, `Muuri.AutoScroller.RIGHT`, `Muuri.AutoScroller.UP`, `Muuri.AutoScroller.DOWN`. +- **onStop**  —  _null / function_ + - Default value: `null`. + - Optionally, you can provide a callback that will be called when an item stops auto-scrolling a scroll target. The callback function will receive the following arguments: + - **item**  —  _Muuri.Item_ + - The dragged `Muuri.Item` instance. + - **scrollElement**  —  _element_ / _window_ + - The scrolled element. + - **direction**  —  _number_ + - The direction of the scroll, one of the following: `Muuri.AutoScroller.LEFT`, `Muuri.AutoScroller.RIGHT`, `Muuri.AutoScroller.UP`, `Muuri.AutoScroller.DOWN`. + +**Examples** + +```javascript +// Create a fixed drag container for the dragged items, this is done with JS +// just for example's purposes. +var dragContainer = document.createElement('div'); +dragContainer.style.position = 'fixed'; +dragContainer.style.left = '0px'; +dragContainer.style.top = '0px'; +dragContainer.style.zIndex = 1000; +document.body.appendChild(dragContainer); + +var grid = new Muuri(elem, { + dragEnabled: true, + dragContainer: dragContainer, + dragAutoScroll: { + targets: [ + // Scroll window on both x-axis and y-axis. + { element: window, priority: 0 }, + // Scroll scrollElement (can be any scrollable element) on y-axis only, + // and prefer it over window in conflict scenarios. + { element: scrollElement, priority: 1, axis: Muuri.AutoScroller.AXIS_Y }, + ], + // Let's use the dragged item element as the handle. + handle: null, + // Start auto-scroll when the distance from scroll target's edge to dragged + // item is 40px or less. + threshold: 40, + // Make sure the inner 10% of the scroll target's area is always "safe zone" + // which does not trigger auto-scroll. + safeZone: 0.1, + // Let's define smooth dynamic speed. + // Max speed: 2000 pixels per second + // Acceleration: 2700 pixels per second + // Deceleration: 3200 pixels per second. + speed: Muuri.AutoScroller.smoothSpeed(2000, 2700, 3200), + // Let's not sort during scroll. + sortDuringScroll: false, + // Enable smooth stop. + smoothStop: true, + // Finally let's log some data when auto-scroll starts and stops. + onStart: function (item, scrollElement, direction) { + console.log('AUTOSCROLL STARTED', item, scrollElement, direction); + }, + onStop: function (item, scrollElement, direction) { + console.log('AUTOSCROLL STOPPED', item, scrollElement, direction); + }, + }, }); ``` -### containerClass   +

option: containerClass

+ +Grid element's class name. -Container element's class name. +- Default value: `'muuri'`. +- Accepted types: string. -* Default value: `'muuri'`. -* Accepted types: string. +**Examples** ```javascript var grid = new Muuri(elem, { - containerClass: 'foo' + containerClass: 'foo', }); ``` -### itemClass   +

option: itemClass

Item element's class name. -* Default value: `'muuri-item'`. -* Accepted types: string. +- Default value: `'muuri-item'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - itemClass: 'foo-item' + itemClass: 'foo-item', }); ``` -### itemVisibleClass   +

option: itemVisibleClass

Visible item's class name. -* Default value: `'muuri-item-shown'`. -* Accepted types: string. +- Default value: `'muuri-item-shown'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - itemVisibleClass: 'foo-item-shown' + itemVisibleClass: 'foo-item-shown', }); ``` -### itemHiddenClass   +

option: itemHiddenClass

Hidden item's class name. -* Default value: `'muuri-item-hidden'`. -* Accepted types: string. +- Default value: `'muuri-item-hidden'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - itemHiddenClass: 'foo-item-hidden' + itemHiddenClass: 'foo-item-hidden', }); ``` -### itemPositioningClass   +

option: itemPositioningClass

This class name will be added to the item element for the duration of positioning. -* Default value: `'muuri-item-positioning'`. -* Accepted types: string. +- Default value: `'muuri-item-positioning'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - itemPositioningClass: 'foo-item-positioning' + itemPositioningClass: 'foo-item-positioning', }); ``` -### itemDraggingClass   +

option: itemDraggingClass

This class name will be added to the item element for the duration of drag. -* Default value: `'muuri-item-dragging'`. -* Accepted types: string. +- Default value: `'muuri-item-dragging'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - itemDraggingClass: 'foo-item-dragging' + itemDraggingClass: 'foo-item-dragging', }); ``` -### itemReleasingClass   +

option: itemReleasingClass

This class name will be added to the item element for the duration of release. -* Default value: `'muuri-item-releasing'`. -* Accepted types: string. +- Default value: `'muuri-item-releasing'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - itemReleasingClass: 'foo-item-releasing' + itemReleasingClass: 'foo-item-releasing', }); ``` -### itemPlaceholderClass   +

option: itemPlaceholderClass

This class name will be added to the drag placeholder element. -* Default value: `'muuri-item-placeholder'`. -* Accepted types: string. +- Default value: `'muuri-item-placeholder'`. +- Accepted types: string. + +**Examples** ```javascript var grid = new Muuri(elem, { - itemPlaceholderClass: 'foo-item-placeholder' + itemPlaceholderClass: 'foo-item-placeholder', }); ``` -### Grid methods +

Grid methods

-* [grid.getElement()](#gridgetelement) -* [grid.getItems( [targets] )](#gridgetitems-targets-) -* [grid.refreshItems( [items] )](#gridrefreshitems-items-) -* [grid.refreshSortData( [items] )](#gridrefreshsortdata-items-) -* [grid.synchronize()](#gridsynchronize) -* [grid.layout( [instant], [callback] )](#gridlayout-instant-callback-) -* [grid.add( elements, [options] )](#gridadd-elements-options-) -* [grid.remove( items, [options] )](#gridremove-items-options-) -* [grid.show( items, [options] )](#gridshow-items-options-) -* [grid.hide( items, [options] )](#gridhide-items-options-) -* [grid.filter( predicate, [options] )](#gridfilter-predicate-options-) -* [grid.sort( comparer, [options] )](#gridsort-comparer-options-) -* [grid.move( item, position, [options] )](#gridmove-item-position-options-) -* [grid.send( item, grid, position, [options] )](#gridsend-item-grid-position-options-) -* [grid.on( event, listener )](#gridon-event-listener-) -* [grid.off( event, listener )](#gridoff-event-listener-) -* [grid.destroy( [removeElements] )](#griddestroy-removeelements-) +- [getElement](#grid-method-getelement) +- [getItem](#grid-method-getitem) +- [getItems](#grid-method-getitems) +- [refreshItems](#grid-method-refreshitems) +- [refreshSortData](#grid-method-refreshsortdata) +- [synchronize](#grid-method-synchronize) +- [layout](#grid-method-layout) +- [add](#grid-method-add) +- [remove](#grid-method-remove) +- [show](#grid-method-show) +- [hide](#grid-method-hide) +- [filter](#grid-method-filter) +- [sort](#grid-method-sort) +- [move](#grid-method-move) +- [send](#grid-method-send) +- [on](#grid-method-on) +- [off](#grid-method-off) +- [destroy](#grid-method-destroy) -### grid.getElement() +

grid.getElement()

-Get the instance element. +Get the grid element. -**Returns**  —  *element* +**Returns**  —  _element_ + +**Examples** ```javascript var elem = grid.getElement(); ``` -### grid.getItems( [targets] ) +

grid.getItem( target )

+ +Get a single grid item by element or by index. Target can also be a `Muuri.Item` instance in which case the function returns the item if it exists within related `Muuri` instance. If nothing is found with the provided target, `null` is returned. + +**Parameters** + +- **target**  —  _element / number / Muuri.Item_ + +**Returns**  —  _Muuri.Item / null_ + +- Returns the queried item or `null` if no item is found. + +**Examples** + +```javascript +// Get first item in grid. +var itemA = grid.getItem(0); + +// Get item by element reference. +var itemB = grid.getItem(someElement); +``` + +

grid.getItems( [targets] )

Get all items in the grid. Optionally you can provide specific targets (indices or elements). **Parameters** -* **targets**  —  *array / element / Muuri.Item / number* - * An array of item instances/elements/indices. - * Optional. +- **targets**  —  _array / element / Muuri.Item / number_ + - An array of item instances/elements/indices. + - Optional. + +**Returns**  —  _array_ -**Returns**  —  *array* +- Returns the queried items. -Returns the queried items. +**Examples** ```javascript // Get all items, both active and inactive. @@ -1178,33 +1565,53 @@ var firstItem = grid.getItems(0)[0]; var items = grid.getItems([elemA, elemB]); ``` -### grid.refreshItems( [items] ) +

grid.refreshItems( [items], [force] )

-Refresh the cached dimensions of the grid's items. When called without any arguments all active items are refreshed. Optionally you can provide specific the items which you want to refresh as the first argument. +Update the cached dimensions of the instance's items. By default all the items are refreshed, but you can also provide an array of target items as the first argument if you want to refresh specific items. Note that all hidden items are not refreshed by default since their `display` property is `'none'` and their dimensions are therefore not readable from the DOM. However, if you do want to force update hidden item dimensions too you can provide `true` as the second argument, which makes the elements temporarily visible while their dimensions are being read. **Parameters** -* **items**  —  *array / element / Muuri.Item / number / string* - * To target specific items provide an array of item instances/elements/indices. By default all active items are targeted. - * Optional. +- **items**  —  _array / element / Muuri.Item / number / string_ + - To target specific items provide an array of item instances/elements/indices. By default all items are targeted. + - Optional. +- **force**  —  _boolean_ + - Set to `true` to read dimensions of hidden items too (and make them visible for the duration of the reading). + - Default: `false`. + - Optional. + +**Returns**  —  _Muuri_ + +- Returns the grid instance. + +**Examples** ```javascript -// Refresh dimensions of all active item elements. +// Refresh dimensions of all items. grid.refreshItems(); -// Refresh dimensions of specific item elements. +// Refresh dimensions of specific items. grid.refreshItems([0, someElem, someItem]); + +// Refresh dimensions of specific items and force read their +// dimensions even if they are hidden. Note that this has performance cost. +grid.refreshItems([0, someElem, someHiddenItem], true); ``` -### grid.refreshSortData( [items] ) +

grid.refreshSortData( [items] )

-Refresh the sort data of the instance's items. +Refresh the sort data of the grid's items. **Parameters** -* **items**  —  *array / element / Muuri.Item / number* - * To target specific items provide an array of item instances/elements/indices. By default all items are targeted. - * Optional. +- **items**  —  _array / element / Muuri.Item / number_ + - To target specific items provide an array of item instances/elements/indices. By default all items are targeted. + - Optional. + +**Returns**  —  _Muuri_ + +- Returns the grid instance. + +**Examples** ```javascript // Refresh the sort data for every item. @@ -1214,32 +1621,48 @@ grid.refreshSortData(); grid.refreshSortData([0, someElem, someItem]); ``` -### grid.synchronize() +

grid.synchronize()

+ +Synchronize the item elements in the DOM to match the order of the items in the grid. This comes handy if you need to keep the DOM structure matched with the order of the items. Note that if an item's element is not currently a child of the grid element (if it is dragged for example) it is ignored and left untouched. The reason why item elements are not kept in sync automatically is that there's rarely a need for that as they are absolutely positioned elements. + +**Returns**  —  _Muuri_ -Synchronize the item elements to match the order of the items in the DOM. This comes handy if you need to keep the DOM structure matched with the order of the items. Note that if an item's element is not currently a child of the container element (if it is dragged for example) it is ignored and left untouched. +- Returns the grid instance. + +**Examples** ```javascript // Let's say we have to move the first item in the grid as the last. grid.move(0, -1); -// Now the DOM order of the items is not in sync anymore with the -// order of the items. We can sync the DOM with synchronize method. +// Now the order of the item elements in the DOM is not in sync anymore +// with the order of the items in the grid. We can sync the DOM with +// synchronize method. grid.synchronize(); ``` -### grid.layout( [instant], [callback] ) +

grid.layout( [instant], [callback] )

Calculate item positions and move items to their calculated positions, unless they are already positioned correctly. The grid's height/width (depends on the layout algorithm) is also adjusted according to the position of the items. **Parameters** -* **instant**  —  *boolean* - * Should the items be positioned instantly without any possible animation? - * Default value: `false`. - * Optional. -* **callback**  —  *function* - * A callback function that is called after the items have positioned. Receives one argument: an array of all the items that were successfully positioned without interruptions. - * Optional. +- **instant**  —  _boolean_ + - Should the items be positioned instantly without any possible animation? + - Default value: `false`. + - Optional. +- **callback**  —  _function_ + - A callback function that is called after every item in the layout has finished/aborted positioning. + - Receives two arguments: + - An array of all the items in the layout. + - A boolean indicating if the layout has changed or not. + - Optional. + +**Returns**  —  _Muuri_ + +- Returns the grid instance. + +**Examples** ```javascript // Layout items. @@ -1250,162 +1673,205 @@ grid.layout(true); // Layout all items and define a callback that will be called // after all items have been animated to their positions. -grid.layout(function (items) { +grid.layout(function (items, hasLayoutChanged) { + // If hasLayoutChanged is `true` it means that there has been another layout + // call before this layout had time to finish positioning all the items. console.log('layout done!'); }); ``` -### grid.add( elements, [options] ) +

grid.add( elements, [options] )

-Add new items by providing the elements you wish to add to the instance and optionally provide the index where you want the items to be inserted into. All elements that are not already children of the container element will be automatically appended to the container element. If an element has it's CSS display property set to none it will be marked as *inactive* during the initiation process. As long as the item is *inactive* it will not be part of the layout, but it will retain it's index. You can activate items at any point with `grid.show()` method. This method will automatically call `grid.layout()` if one or more of the added elements are visible. If only hidden items are added no layout will be called. All the new visible items are positioned without animation during their first layout. +Add new items by providing the elements you wish to add to the grid and optionally provide the index where you want the items to be inserted into. All elements that are not already children of the grid element will be automatically appended to the grid element. If an element has it's CSS display property set to none it will be marked as _inactive_ during the initiation process. As long as the item is _inactive_ it will not be part of the layout, but it will retain it's index. You can activate items at any point with `grid.show()` method. This method will automatically call `grid.layout()` if one or more of the added elements are visible. If only hidden items are added no layout will be called. All the new visible items are positioned without animation during their first layout. **Parameters** -* **elements**  —  *array / element* - * An array of DOM elements. -* **options.index**  —  *number* - * The index where you want the items to be inserted in. A value of `-1` will insert the items to the end of the list while `0` will insert the items to the beginning. Note that the DOM elements are always just appended to the instance container regardless of the index value. You can use the `grid.synchronize()` method to arrange the DOM elements to the same order as the items. - * Default value: `-1`. - * Optional. -* **options.layout**  —  *boolean / function / string* - * By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. - * Default value: `true`. - * Optional. +- **elements**  —  _array / element_ + - An array of DOM elements. +- **options.active**  —  _boolean / undefined_ + - By default (when this option is `undefined`) Muuri will automatically detect from each element's `display` style if the item should be active (visible) or inactive (hidden) on init. If the element's `display` style is `none` then the item will be inactive on init. However, you can also provide a boolean here to force the item to be active (`true`) or inactive (`false`) on init. + - Default value: `undefined`. + - Optional. +- **options.index**  —  _number_ + - The index where you want the items to be inserted in. A value of `-1` will insert the items to the end of the list while `0` will insert the items to the beginning. Note that the DOM elements are always just appended to the grid element regardless of the index value. You can use the `grid.synchronize()` method to arrange the DOM elements to the same order as the items. + - Default value: `-1`. + - Optional. +- **options.layout**  —  _boolean / function / string_ + - By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. + - Default value: `true`. + - Optional. -**Returns**  —  *array* +**Returns**  —  _array_ -Returns the added items. +- Returns the added items. + +**Examples** ```javascript // Add two new items to the end. -grid.add([elemA, elemB]); +var newItemsA = grid.add([elemA, elemB]); // Add two new items to the beginning. -grid.add([elemA, elemB], {index: 0}); +var newItemsB = grid.add([elemA, elemB], { index: 0 }); // Skip the automatic layout. -grid.add([elemA, elemB], {layout: false}); +var newItemsC = grid.add([elemA, elemB], { layout: false }); ``` -### grid.remove( items, [options] ) +

grid.remove( items, [options] )

-Remove items from the instance. +Remove items from the grid. **Parameters** -* **items**  —  *array / element / Muuri.Item / number* - * An array of item instances/elements/indices. -* **options.removeElements**  —  *boolean* - * Should the associated DOM element be removed from the DOM? - * Default value: `false`. - * Optional. -* **options.layout**  —  *boolean / function / string* - * By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. - * Default value: `true`. - * Optional. +- **items**  —  _array_ + - An array of item instances. +- **options.removeElements**  —  _boolean_ + - Should the associated DOM element be removed from the DOM? + - Default value: `false`. + - Optional. +- **options.layout**  —  _boolean / function / string_ + - By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. + - Default value: `true`. + - Optional. + +**Returns**  —  _array_ -**Returns**  —  *array* +- Returns the removed items. Note that the removal process also _destroys_ the items so they can not be reused e.g. in another grid. -Returns the destroyed items. +**Examples** ```javascript // Remove the first item, but keep the element in the DOM. -grid.remove(0); +var removedItemsA = grid.remove(grid.getItems(0)); // Remove items and the associated elements. -grid.remove([elemA, elemB], {removeElements: true}); +var removedItemsB = grid.remove([itemA, itemB], { removeElements: true }); // Skip the layout. -grid.remove([elemA, elemB], {layout: false}); +var removedItemsC = grid.remove([itemA, itemB], { layout: false }); ``` - -### grid.show( items, [options] ) +

grid.show( items, [options] )

Show the targeted items. **Parameters** -* **items**  —  *array / element / Muuri.Item / number* - * An array of item instances/elements/indices. -* **options.instant**  —  *boolean* - * Should the items be shown instantly without any possible animation? - * Default value: `false`. - * Optional. -* **options.onFinish**  —  *function* - * A callback function that is called after the items are shown. - * Optional. -* **options.layout**  —  *boolean / function / string* - * By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. - * Default value: `true`. - * Optional. +- **items**  —  _array_ + - An array of item instances. +- **options.instant**  —  _boolean_ + - Should the items be shown instantly without any possible animation? + - Default value: `false`. + - Optional. +- **options.syncWithLayout**  —  _boolean_ + - Should we wait for the next layout's calculations (which are potentially async) to finish before starting the show animations? By default this option is enabled so that the show animations are triggered in sync with the layout animations. If that's not needed set this to `false` and the show animations will begin immediately. + - Default value: `true`. + - Optional. +- **options.onFinish**  —  _function_ + - A callback function that is called after the items are shown. + - Optional. +- **options.layout**  —  _boolean / function / string_ + - By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. + - Default value: `true`. + - Optional. + +**Returns**  —  _Muuri_ + +- Returns the grid instance. + +**Examples** ```javascript // Show items with animation (if any). -grid.show([elemA, elemB]); +grid.show([itemA, itemB]); // Show items instantly without animations. -grid.show([elemA, elemB], {instant: true}); +grid.show([itemA, itemB], { instant: true }); // Show items with callback (and with animations if any). -grid.show([elemA, elemB], {onFinish: function (items) { - console.log('items shown!'); -}}); +grid.show([itemA, itemB], { + onFinish: function (items) { + console.log('items shown!'); + }, +}); ``` -### grid.hide( items, [options] ) +

grid.hide( items, [options] )

Hide the targeted items. **Parameters** -* **items**  —  *array / element / Muuri.Item / number* - * An array of item instances/elements/indices. -* **options.instant**  —  *boolean* - * Should the items be hidden instantly without any possible animation? - * Default value: `false`. - * Optional. -* **options.onFinish**  —  *function* - * A callback function that is called after the items are hidden. - * Optional. -* **options.layout**  —  *boolean / function / string* - * By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. - * Default value: `true`. - * Optional. +- **items**  —  _array_ + - An array of item instances. +- **options.instant**  —  _boolean_ + - Should the items be hidden instantly without any possible animation? + - Default value: `false`. + - Optional. +- **options.syncWithLayout**  —  _boolean_ + - Should we wait for the next layout's calculations (which are potentially async) to finish before starting the hide animations? By default this option is enabled so that the hide animations are triggered in sync with the layout animations. If that's not needed set this to `false` and the hide animations will begin immediately. + - Default value: `true`. + - Optional. +- **options.onFinish**  —  _function_ + - A callback function that is called after the items are hidden. + - Optional. +- **options.layout**  —  _boolean / function / string_ + - By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. + - Default value: `true`. + - Optional. + +**Returns**  —  _Muuri_ + +- Returns the grid instance. + +**Examples** ```javascript // Hide items with animation. -grid.hide([elemA, elemB]); +grid.hide([itemA, itemB]); // Hide items instantly without animations. -grid.hide([elemA, elemB], {instant: true}); +grid.hide([itemA, itemB], { instant: true }); // Hide items and call the callback function after // all items are hidden. -grid.hide([elemA, elemB], {onFinish: function (items) { - console.log('items hidden!'); -}}); +grid.hide([itemA, itemB], { + onFinish: function (items) { + console.log('items hidden!'); + }, +}); ``` -### grid.filter( predicate, [options] ) +

grid.filter( predicate, [options] )

-Filter items. Expects at least one argument, a predicate, which should be either a function or a string. The predicate callback is executed for every item in the instance. If the return value of the predicate is truthy the item in question will be shown and otherwise hidden. The predicate callback receives the item instance as it's argument. If the predicate is a string it is considered to be a selector and it is checked against every item element in the instance with the native element.matches() method. All the matching items will be shown and others hidden. +Filter items. Expects at least one argument, a predicate, which should be either a function or a string. The predicate callback is executed for every item in the grid. If the return value of the predicate is truthy the item in question will be shown and otherwise hidden. The predicate callback receives the item instance as it's argument. If the predicate is a string it is considered to be a selector and it is checked against every item element in the grid with the native element.matches() method. All the matching items will be shown and others hidden. **Parameters** -* **predicate**  —  *function / string* - * A predicate callback or a selector. -* **options.instant**  —  *boolean* - * Should the items be shown/hidden instantly without any possible animation? - * Default value: `false`. - * Optional. -* **options.onFinish**  —  *function* - * An optional callback function that is called after all the items are shown/hidden. - * Optional. -* **options.layout**  —  *boolean / function / string* - * By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. - * Default value: `true`. - * Optional. +- **predicate**  —  _function / string_ + - A predicate callback or a selector. +- **options.instant**  —  _boolean_ + - Should the items be shown/hidden instantly without any possible animation? + - Default value: `false`. + - Optional. +- **options.syncWithLayout**  —  _boolean_ + - Should we wait for the next layout's calculations (which are potentially async) to finish before starting the visibility animations? By default this option is enabled so that the visibility animations are triggered in sync with the layout animations. If that's not needed set this to `false` and the visibility animations will begin immediately. + - Default value: `true`. + - Optional. +- **options.onFinish**  —  _function_ + - An optional callback function that is called after all the items are shown/hidden. + - Optional. +- **options.layout**  —  _boolean / function / string_ + - By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. + - Default value: `true`. + - Optional. + +**Returns**  —  _Muuri_ + +- Returns the grid instance. + +**Examples** ```javascript // Show all items that have the attribute "data-foo". @@ -1420,22 +1886,28 @@ grid.filter('[data-foo]'); grid.filter('.foo'); ``` -### grid.sort( comparer, [options] ) +

grid.sort( comparer, [options] )

-Sort items. There are three ways to sort the items. The first is simply by providing a function as the comparer which works almost identically to [native array sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). The only difference is that the sort is always stable. Alternatively you can sort by the sort data you have provided in the instance's options. Just provide the sort data key(s) as a string (separated by space) and the items will be sorted based on the provided sort data keys. Lastly you have the opportunity to provide a presorted array of items which will be used to sync the internal items array in the same order. +Sort items. There are three ways to sort the items. The first is simply by providing a function as the comparer which works almost identically to [native array sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). The only difference is that the sort is always stable. Alternatively you can sort by the sort data you have provided in the grid's options. Just provide the sort data key(s) as a string (separated by space) and the items will be sorted based on the provided sort data keys. Lastly you have the opportunity to provide a presorted array of items which will be used to sync the internal items array in the same order. **Parameters** -* **comparer**  —  *array / function / string* - * Provide a comparer function, sort data keys as a string (separated with space) or a presorted array of items. It is recommended to use the sort data feature, because it allows you to cache the sort data and make the sorting faster. -* **options.descending**  —  *boolean* - * By default the items are sorted in ascending order. If you want to sort them in descending order set this to `true`. - * Default value: `false`. - * Optional. -* **options.layout**  —  *boolean / function / string* - * By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. - * Default value: `true`. - * Optional. +- **comparer**  —  _array / function / string_ + - Provide a comparer function, sort data keys as a string (separated with space) or a pre-sorted array of items. When you provide a pre-sorted array of items you _must_ make sure that it contains _exactly_ the same item instances as exists currently in `grid._items` (retrievable safely via `grid.getItems()`), only change the order of items. Muuri does not validate the array of items you provide due to performance reasons. +- **options.descending**  —  _boolean_ + - By default the items are sorted in ascending order. If you want to sort them in descending order set this to `true`. Note that this option has no effect when you provide a pre-sorted array of items. + - Default value: `false`. + - Optional. +- **options.layout**  —  _boolean / function / string_ + - By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. + - Default value: `true`. + - Optional. + +**Returns**  —  _Muuri_ + +- Returns the grid instance. + +**Examples** ```javascript // Sort items by data-id attribute value (ascending). @@ -1446,39 +1918,45 @@ grid.sort(function (itemA, itemB) { }); // Sort items with a presorted array of items. -grid.sort(presortedItems); +grid.sort(grid.getItems().reverse()); // Sort items using the sort data keys (ascending). grid.sort('foo bar'); // Sort items using the sort data keys (descending). -grid.sort('foo bar', {descending: true}); +grid.sort('foo bar', { descending: true }); // Sort items using the sort data keys. Sort some keys // ascending and some keys descending. grid.sort('foo bar:desc'); ``` -### grid.move( item, position, [options] ) +

grid.move( item, position, [options] )

Move an item to another position in the grid. **Parameters** -* **item**  —  *element / Muuri.Item / number* - * Item instance, element or index. -* **position**  —  *element / Muuri.Item / number* - * Item instance, element or index. -* **options.action**  —  *string* - * Accepts the following values: - * `'move'`: moves item in place of another item. - * `'swap'`: swaps position of items. - * Default value: `'move'`. - * Optional. -* **options.layout**  —  *boolean / function / string* - * By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. - * Default value: `true`. - * Optional. +- **item**  —  _element / Muuri.Item / number_ + - Item instance, element or index. +- **position**  —  _element / Muuri.Item / number_ + - Item instance, element or index. +- **options.action**  —  _string_ + - Accepts the following values: + - `'move'`: moves item in place of another item. + - `'swap'`: swaps position of items. + - Default value: `'move'`. + - Optional. +- **options.layout**  —  _boolean / function / string_ + - By default `grid.layout()` is called at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. + - Default value: `true`. + - Optional. + +**Returns**  —  _Muuri_ + +- Returns the grid instance. + +**Examples** ```javascript // Move elemA to the index of elemB. @@ -1488,35 +1966,41 @@ grid.move(elemA, elemB); grid.move(0, -1); // Swap positions of elemA and elemB. -grid.move(elemA, elemB, {action: 'swap'}); +grid.move(elemA, elemB, { action: 'swap' }); // Swap positions of the first and the last item. -grid.move(0, -1, {action: 'swap'}); +grid.move(0, -1, { action: 'swap' }); ``` -### grid.send( item, grid, position, [options] ) +

grid.send( item, grid, position, [options] )

Move an item into another grid. **Parameters** -* **item**  —  *element / Muuri.Item / number* - * The item that should be moved. You can define the item with an item instance, element or index. -* **grid**  —  *Muuri* - * The grid where the item should be moved to. -* **position**  —  *element / Muuri.Item / number* - * To which position should the item be placed to in the new grid? You can define the position with an item instance, element or index. -* **options.appendTo**  —  *element* - * Which element the item element should be appended to for the duration of the layout animation? - * Default value: `document.body`. -* **options.layoutSender**  —  *boolean / function / string* - * By default `grid.layout()` is called for the sending grid at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. - * Default value: `true`. - * Optional. -* **options.layoutReceiver**  —  *boolean / function / string* - * By default `grid.layout()` is called for the receiving grid at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. - * Default value: `true`. - * Optional. +- **item**  —  _element / Muuri.Item / number_ + - The item that should be moved. You can define the item with an item instance, element or index. +- **grid**  —  _Muuri_ + - The grid where the item should be moved to. +- **position**  —  _element / Muuri.Item / number_ + - To which position should the item be placed to in the new grid? You can define the position with an item instance, element or index. +- **options.appendTo**  —  _element_ + - Which element the item element should be appended to for the duration of the layout animation? + - Default value: `document.body`. +- **options.layoutSender**  —  _boolean / function / string_ + - By default `grid.layout()` is called for the sending grid at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. + - Default value: `true`. + - Optional. +- **options.layoutReceiver**  —  _boolean / function / string_ + - By default `grid.layout()` is called for the receiving grid at the end of this method. With this argument you can control the layout call. You can disable the layout completely with `false`, or provide a callback function for the layout method, or provide the string `'instant'` to make the layout happen instantly without any animations. + - Default value: `true`. + - Optional. + +**Returns**  —  _Muuri_ + +- Returns the grid instance. + +**Examples** ```javascript // Move the first item of gridA as the last item of gridB. @@ -1526,7 +2010,7 @@ gridA.send(0, gridB, -1); // Move the first item of gridA as the last item of gridB. // The sent item will be appended to someElem. gridA.send(0, gridB, -1, { - appendTo: someElem + appendTo: someElem, }); // Do something after the item has been sent and the layout @@ -1537,22 +2021,24 @@ gridA.send(0, gridB, -1, { }, layoutReceiver: function (isAborted, items) { // Do your other thing here... - } + }, }); ``` -### grid.on( event, listener ) +

grid.on( event, listener )

Bind an event listener. **Parameters** -* **event**  —  *string* -* **listener**  —  *function* +- **event**  —  _string_ +- **listener**  —  _function_ + +**Returns**  —  _Muuri_ -**Returns**  —  *object* +- Returns the grid instance. -Returns the instance. +**Examples** ```javascript grid.on('layoutEnd', function (items) { @@ -1560,81 +2046,95 @@ grid.on('layoutEnd', function (items) { }); ``` -### grid.off( event, listener ) +

grid.off( event, listener )

Unbind an event listener. **Parameters** -* **event**  —  *string* -* **listener**  —  *function* +- **event**  —  _string_ +- **listener**  —  _function_ -**Returns**  —  *object* +**Returns**  —  _Muuri_ -Returns the instance. +- Returns the grid instance. + +**Examples** ```javascript -var listener = function (items) { +function onLayoutEnd(items) { console.log(items); -}; +} -muuri -.on('layoutEnd', listener) -.off('layoutEnd', listener); +// Start listening to some event. +grid.on('layoutEnd', onLayoutEnd); + +/// ...sometime later -> unbind listener. +grid.off('layoutEnd', onLayoutEnd); ``` -### grid.destroy( [removeElements] ) +

grid.destroy( [removeElements] )

-Destroy the grid instance. +Destroy the grid. **Parameters** -* **removeElements**  —  *boolean* - * Should the item elements be removed or not? - * Default value: `false`. - * Optional. +- **removeElements**  —  _boolean_ + - Should the item elements be removed or not? + - Default value: `false`. + - Optional. + +**Returns**  —  _Muuri_ -**Returns**  —  *object* +- Returns the grid instance. -Returns the instance. +**Examples** ```javascript -// Destroy the instance. +// Destroy the instance, but keep +// item element in the DOM. grid.destroy(); -// Destroy the instance and remove item elements. +``` + +```javascript +// Destroy the instance and remove +// the item elements from the DOM. grid.destroy(true); ``` -### Grid events - -* [synchronize](#synchronize) -* [layoutStart](#layoutstart) -* [layoutEnd](#layoutend) -* [add](#add) -* [remove](#remove) -* [showStart](#showstart) -* [showEnd](#showend) -* [hideStart](#hidestart) -* [hideEnd](#hideend) -* [filter](#filter) -* [sort](#sort) -* [move](#move) -* [send](#send) -* [beforeSend](#beforesend) -* [receive](#receive) -* [beforeReceive](#beforereceive) -* [dragInit](#draginit) -* [dragStart](#dragstart) -* [dragMove](#dragmove) -* [dragScroll](#dragscroll) -* [dragEnd](#dragend) -* [dragReleaseStart](#dragreleasestart) -* [dragReleaseEnd](#dragreleaseend) -* [destroy](#destroy) - -### synchronize - -Triggered after `grid.synchronize()` is called. +

Grid events

+ +- [synchronize](#grid-event-synchronize) +- [layoutStart](#grid-event-layoutstart) +- [layoutEnd](#grid-event-layoutend) +- [layoutAbort](#grid-event-layoutabort) +- [add](#grid-event-add) +- [remove](#grid-event-remove) +- [showStart](#grid-event-showstart) +- [showEnd](#grid-event-showend) +- [hideStart](#grid-event-hidestart) +- [hideEnd](#grid-event-hideend) +- [filter](#grid-event-filter) +- [sort](#grid-event-sort) +- [move](#grid-event-move) +- [send](#grid-event-send) +- [beforeSend](#grid-event-beforesend) +- [receive](#grid-event-receive) +- [beforeReceive](#grid-event-beforereceive) +- [dragInit](#grid-event-draginit) +- [dragStart](#grid-event-dragstart) +- [dragMove](#grid-event-dragmove) +- [dragScroll](#grid-event-dragscroll) +- [dragEnd](#grid-event-dragend) +- [dragReleaseStart](#grid-event-dragreleasestart) +- [dragReleaseEnd](#grid-event-dragreleaseend) +- [destroy](#grid-event-destroy) + +

event: synchronize

+ +Triggered after item elements are synchronized via `grid.synchronize()`. + +**Examples** ```javascript grid.on('synchronize', function () { @@ -1642,44 +2142,81 @@ grid.on('synchronize', function () { }); ``` -### layoutStart +

event: layoutStart

-Triggered after `grid.layout()` is called, just before the items are positioned. +Triggered when the the layout procedure begins. More specifically, this event is emitted right after new _layout_ has been generated, internal item positions updated and grid element's dimensions updated. After this event is emitted the items in the layout will be positioned to their new positions. So if you e.g. want to react to grid element dimension changes this is a good place to do that. **Arguments** -* **items**  —  *array* - * The items that are about to be positioned. +- **items**  —  _array_ + - The items that are about to be positioned. +- **isInstant**  —  _boolean_ + - Was the layout called with `instant` flag or not. + +**Examples** ```javascript -grid.on('layoutStart', function (items) { - console.log(items); +grid.on('layoutStart', function (items, isInstant) { + console.log(items, isInstant); }); ``` -### layoutEnd +

event: layoutEnd

-Triggered after `grid.layout()` is called, after the items have positioned. Note that if `grid.layout()` is called during an ongoing layout animation the ongoing layout process will be aborted and it's _layoutEnd_ event will never be triggered. +Triggered after the layout procedure has finished, successfully. Note that if you abort a layout procedure by calling `grid.layout()` _before_ items have finished positioning, this event will not be emitted for the aborted layout procedure. In such a case `layoutAbort` will be emitted instead. **Arguments** -* **items**  —  *array* - * The items that were intended to be positioned. Note that these items are always identical to what the _layoutStart_ event's callback receives as it's argument. So if, for example, you destroy an item during the layout animation and don't do call another layout the destroyed item will still be included in this array of items. The original intention was to filter these items so that all items that were "interrupted" somehow during the layout process would be omitted from the results, but that solution was much more prone to errors and much more harder to explain/understand. +- **items**  —  _array_ + - The items that were positioned. Note that these items are always identical to what the _layoutStart_ event's callback receives as it's argument. + +**Examples** ```javascript grid.on('layoutEnd', function (items) { console.log(items); + // For good measure you might want to filter out all the non-active items, + // because it's techniclly possible that some of the items are + // destroyed/hidden when we receive this event. + var activeItems = items.filter(function (item) { + return item.isActive(); + }); }); ``` -### add +

event: layoutAbort

+ +Triggered if you start a new layout process (`grid.layout()`) while the current layout process is still busy positioning items. Note that this event is not triggered if you start a new layout process while the layout is being computed and the items have not yet started positioning. + +**Arguments** + +- **items**  —  _array_ + - The items that were attempted to be positioned. Note that these items are always identical to what the _layoutStart_ event's callback receives as it's argument. + +**Examples** + +```javascript +grid.on('layoutAbort', function (items) { + console.log(items); + // For good measure you might want to filter out all the non-active items, + // because it's techniclly possible that some of the items are destroyed or + // hidden when we receive this event. + var activeItems = items.filter(function (item) { + return item.isActive(); + }); +}); +``` + +

event: add

Triggered after `grid.add()` is called. **Arguments** -* **items**  —  *array* - * The items that were successfully added. +- **items**  —  _array_ + - The items that were successfully added. + +**Examples** ```javascript grid.on('add', function (items) { @@ -1687,16 +2224,18 @@ grid.on('add', function (items) { }); ``` -### remove +

event: remove

Triggered after `grid.remove()` is called. **Arguments** -* **items**  —  *array* - * The items that were successfully removed. -* **indices**  —  *array* - * Indices of the items that were successfully removed. +- **items**  —  _array_ + - The items that were successfully removed. +- **indices**  —  _array_ + - Indices of the items that were successfully removed. + +**Examples** ```javascript grid.on('remove', function (items, indices) { @@ -1704,14 +2243,16 @@ grid.on('remove', function (items, indices) { }); ``` -### showStart +

event: showStart

Triggered after `grid.show()` is called, just before the items are shown. **Arguments** -* **items**  —  *array* - * The items that are about to be shown. +- **items**  —  _array_ + - The items that are about to be shown. + +**Examples** ```javascript grid.on('showStart', function (items) { @@ -1719,14 +2260,16 @@ grid.on('showStart', function (items) { }); ``` -### showEnd +

event: showEnd

Triggered after `grid.show()` is called, after the items are shown. **Arguments** -* **items**  —  *array* - * The items that were successfully shown without interruptions. If you, for example, call `grid.hide()` to some of the items that are currently being shown, those items will be omitted from this argument. +- **items**  —  _array_ + - The items that were successfully shown without interruptions. If you, for example, call `grid.hide()` to some of the items that are currently being shown, those items will be omitted from this argument. + +**Examples** ```javascript grid.on('showEnd', function (items) { @@ -1734,14 +2277,16 @@ grid.on('showEnd', function (items) { }); ``` -### hideStart +

event: hideStart

Triggered after `grid.hide()` is called, just before the items are hidden. **Arguments** -* **items**  —  *array* - * The items that are about to be hidden. +- **items**  —  _array_ + - The items that are about to be hidden. + +**Examples** ```javascript grid.on('hideStart', function (items) { @@ -1749,14 +2294,16 @@ grid.on('hideStart', function (items) { }); ``` -### hideEnd +

event: hideEnd

Triggered after `grid.hide()` is called, after the items are hidden. **Arguments** -* **items**  —  *array* - * The items that were successfully hidden without interruptions. If you, for example, call `grid.show()` to some of the items that are currently being hidden, those items will be omitted from this argument. +- **items**  —  _array_ + - The items that were successfully hidden without interruptions. If you, for example, call `grid.show()` to some of the items that are currently being hidden, those items will be omitted from this argument. + +**Examples** ```javascript grid.on('hideEnd', function (items) { @@ -1764,16 +2311,18 @@ grid.on('hideEnd', function (items) { }); ``` -### filter +

event: filter

Triggered after `grid.filter()` is called. **Arguments** -* **shownItems**  —  *array* - * The items that are shown. -* **hiddenItems**  —  *array* - * The items that are hidden. +- **shownItems**  —  _array_ + - The items that are shown. +- **hiddenItems**  —  _array_ + - The items that are hidden. + +**Examples** ```javascript grid.on('filter', function (shownItems, hiddenItems) { @@ -1782,16 +2331,18 @@ grid.on('filter', function (shownItems, hiddenItems) { }); ``` -### sort +

event: sort

Triggered after `grid.sort()` is called. **Arguments** -* **currentOrder**  —  *array* - * All items in their current order. -* **previousOrder**  —  *array* - * All items in their previous order. +- **currentOrder**  —  _array_ + - All items in their current order. +- **previousOrder**  —  _array_ + - All items in their previous order. + +**Examples** ```javascript grid.on('sort', function (currentOrder, previousOrder) { @@ -1800,21 +2351,23 @@ grid.on('sort', function (currentOrder, previousOrder) { }); ``` -### move +

event: move

Triggered after `grid.move()` is called or when the grid is sorted during drag. Note that this is event not triggered when an item is dragged into another grid. **Arguments** -* **data**  —  *object* - * **data.item**  —  *Muuri.Item* - * The item that was moved. - * **data.fromIndex**  —  *number* - * The index the item was moved from. - * **data.toIndex**  —  *number* - * The index the item was moved to. - * **data.action**  —  *string* - * "move" or "swap". +- **data**  —  _object_ + - **data.item**  —  _Muuri.Item_ + - The item that was moved. + - **data.fromIndex**  —  _number_ + - The index the item was moved from. + - **data.toIndex**  —  _number_ + - The index the item was moved to. + - **data.action**  —  _string_ + - "move" or "swap". + +**Examples** ```javascript grid.on('move', function (data) { @@ -1822,23 +2375,25 @@ grid.on('move', function (data) { }); ``` -### send +

event: send

-Triggered for the originating grid in the end of the *send process* (after `grid.send()` is called or when an item is dragged into another grid). Note that this event is called *before* the item's layout starts. +Triggered for the originating grid in the end of the _send process_ (after `grid.send()` is called or when an item is dragged into another grid). Note that this event is called _before_ the item's layout starts. **Arguments** -* **data**  —  *object* - * **data.item**  —  *Muuri.Item* - * The item that was sent. - * **data.fromGrid**  —  *Muuri* - * The grid the item was sent from. - * **data.fromIndex**  —  *number* - * The index the item was moved from. - * **data.toGrid**  —  *Muuri* - * The grid the item was sent to. - * **data.toIndex**  —  *number* - * The index the item was moved to. +- **data**  —  _object_ + - **data.item**  —  _Muuri.Item_ + - The item that was sent. + - **data.fromGrid**  —  _Muuri_ + - The grid the item was sent from. + - **data.fromIndex**  —  _number_ + - The index the item was moved from. + - **data.toGrid**  —  _Muuri_ + - The grid the item was sent to. + - **data.toIndex**  —  _number_ + - The index the item was moved to. + +**Examples** ```javascript grid.on('send', function (data) { @@ -1846,23 +2401,25 @@ grid.on('send', function (data) { }); ``` -### beforeSend +

event: beforeSend

-Triggered for the originating grid in the beginning of the *send process* (after `grid.send()` is called or when an item is dragged into another grid). This event is highly useful in situations where you need to manipulate the sent item (freeze it's dimensions for example) before it is appended to it's temporary layout container as defined in [send method options](#gridsend-item-grid-position-options-). +Triggered for the originating grid in the beginning of the _send process_ (after `grid.send()` is called or when an item is dragged into another grid). This event is highly useful in situations where you need to manipulate the sent item (freeze it's dimensions for example) before it is appended to it's temporary layout container as defined in [send method options](#gridsend-item-grid-position-options-). **Arguments** -* **data**  —  *object* - * **data.item**  —  *Muuri.Item* - * The item that was sent. - * **data.fromGrid**  —  *Muuri* - * The grid the item was sent from. - * **data.fromIndex**  —  *number* - * The index the item was moved from. - * **data.toGrid**  —  *Muuri* - * The grid the item was sent to. - * **data.toIndex**  —  *number* - * The index the item was moved to. +- **data**  —  _object_ + - **data.item**  —  _Muuri.Item_ + - The item that was sent. + - **data.fromGrid**  —  _Muuri_ + - The grid the item was sent from. + - **data.fromIndex**  —  _number_ + - The index the item was moved from. + - **data.toGrid**  —  _Muuri_ + - The grid the item was sent to. + - **data.toIndex**  —  _number_ + - The index the item was moved to. + +**Examples** ```javascript grid.on('beforeSend', function (data) { @@ -1870,23 +2427,25 @@ grid.on('beforeSend', function (data) { }); ``` -### receive +

event: receive

-Triggered for the receiving grid in the end of the *send process* (after `grid.send()` is called or when an item is dragged into another grid). Note that this event is called *before* the item's layout starts. +Triggered for the receiving grid in the end of the _send process_ (after `grid.send()` is called or when an item is dragged into another grid). Note that this event is called _before_ the item's layout starts. **Arguments** -* **data**  —  *object* - * **data.item**  —  *Muuri.Item* - * The item that was sent. - * **data.fromGrid**  —  *Muuri* - * The grid the item was sent from. - * **data.fromIndex**  —  *number* - * The index the item was moved from. - * **data.toGrid**  —  *Muuri* - * The grid the item was sent to. - * **data.toIndex**  —  *number* - * The index the item was moved to. +- **data**  —  _object_ + - **data.item**  —  _Muuri.Item_ + - The item that was sent. + - **data.fromGrid**  —  _Muuri_ + - The grid the item was sent from. + - **data.fromIndex**  —  _number_ + - The index the item was moved from. + - **data.toGrid**  —  _Muuri_ + - The grid the item was sent to. + - **data.toIndex**  —  _number_ + - The index the item was moved to. + +**Examples** ```javascript grid.on('receive', function (data) { @@ -1894,23 +2453,25 @@ grid.on('receive', function (data) { }); ``` -### beforeReceive +

event: beforeReceive

-Triggered for the receiving grid in the beginning of the *send process* (after `grid.send()` is called or when an item is dragged into another grid). This event is highly useful in situations where you need to manipulate the received item (freeze it's dimensions for example) before it is appended to it's temporary layout container as defined in [send method options](#gridsend-item-grid-position-options-). +Triggered for the receiving grid in the beginning of the _send process_ (after `grid.send()` is called or when an item is dragged into another grid). This event is highly useful in situations where you need to manipulate the received item (freeze it's dimensions for example) before it is appended to it's temporary layout container as defined in [send method options](#gridsend-item-grid-position-options-). **Arguments** -* **data**  —  *object* - * **data.item**  —  *Muuri.Item* - * The item that was sent. - * **data.fromGrid**  —  *Muuri* - * The grid the item was sent from. - * **data.fromIndex**  —  *number* - * The index the item was moved from. - * **data.toGrid**  —  *Muuri* - * The grid the item was sent to. - * **data.toIndex**  —  *number* - * The index the item was moved to. +- **data**  —  _object_ + - **data.item**  —  _Muuri.Item_ + - The item that was sent. + - **data.fromGrid**  —  _Muuri_ + - The grid the item was sent from. + - **data.fromIndex**  —  _number_ + - The index the item was moved from. + - **data.toGrid**  —  _Muuri_ + - The grid the item was sent to. + - **data.toIndex**  —  _number_ + - The index the item was moved to. + +**Examples** ```javascript grid.on('beforeReceive', function (data) { @@ -1918,16 +2479,18 @@ grid.on('beforeReceive', function (data) { }); ``` -### dragInit +

event: dragInit

-Triggered in the beginning of the *drag start* process when dragging of an item begins. This event is highly useful in situations where you need to manipulate the dragged item (freeze it's dimensions for example) before it is appended to the [dragContainer](#dragcontainer-). +Triggered in the beginning of the _drag start_ process when dragging of an item begins. This event is highly useful in situations where you need to manipulate the dragged item (freeze it's dimensions for example) before it is appended to the [dragContainer](#dragcontainer-). **Arguments** -* **item**  —  *Muuri.Item* - * The dragged item. -* **event**  —  *object* - * Muuri.Dragger event data. +- **item**  —  _Muuri.Item_ + - The dragged item. +- **event**  —  _object_ + - Muuri.Dragger event data. + +**Examples** ```javascript grid.on('dragInit', function (item, event) { @@ -1936,16 +2499,18 @@ grid.on('dragInit', function (item, event) { }); ``` -### dragStart +

event: dragStart

-Triggered in the end of the *drag start* process when dragging of an item begins. +Triggered in the end of the _drag start_ process when dragging of an item begins. **Arguments** -* **item**  —  *Muuri.Item* - * The dragged item. -* **event**  —  *object* - * Muuri.Dragger event data. +- **item**  —  _Muuri.Item_ + - The dragged item. +- **event**  —  _object_ + - Muuri.Dragger event data. + +**Examples** ```javascript grid.on('dragStart', function (item, event) { @@ -1954,16 +2519,18 @@ grid.on('dragStart', function (item, event) { }); ``` -### dragMove +

event: dragMove

-Triggered when an item is dragged after the *drag start* process. +Triggered every time a dragged item is _moved_. Note that Muuri has an automatic throttling system which makes sure that this event is triggered at maximum once in an animation frame. **Arguments** -* **item**  —  *Muuri.Item* - * The dragged item. -* **event**  —  *object* - * Muuri.Dragger event data. +- **item**  —  _Muuri.Item_ + - The dragged item. +- **event**  —  _object_ + - Muuri.Dragger event data. + +**Examples** ```javascript grid.on('dragMove', function (item, event) { @@ -1972,16 +2539,18 @@ grid.on('dragMove', function (item, event) { }); ``` -### dragScroll +

event: dragScroll

Triggered when any of the scroll parents of a dragged item is scrolled. **Arguments** -* **item**  —  *Muuri.Item* - * The dragged item. -* **event**  —  *object* - * The scroll event data. +- **item**  —  _Muuri.Item_ + - The dragged item. +- **event**  —  _object_ + - Scroll event data. + +**Examples** ```javascript grid.on('dragScroll', function (item, event) { @@ -1990,16 +2559,18 @@ grid.on('dragScroll', function (item, event) { }); ``` -### dragEnd +

event: dragEnd

-Triggered when dragging of an item ends. +Triggered when dragged item is released and the drag process ends. **Arguments** -* **item**  —  *Muuri.Item* - * The dragged item. -* **event**  —  *object* - * Muuri.Dragger event data. +- **item**  —  _Muuri.Item_ + - The dragged item. +- **event**  —  _object_ + - Muuri.Dragger event data. + +**Examples** ```javascript grid.on('dragEnd', function (item, event) { @@ -2008,14 +2579,16 @@ grid.on('dragEnd', function (item, event) { }); ``` -### dragReleaseStart +

event: dragReleaseStart

-Triggered when a dragged item is released. +Triggered when a dragged item is released (always after `dragEnd` event). **Arguments** -* **item**  —  *Muuri.Item* - * The released item. +- **item**  —  _Muuri.Item_ + - The released item. + +**Examples** ```javascript grid.on('dragReleaseStart', function (item) { @@ -2023,14 +2596,16 @@ grid.on('dragReleaseStart', function (item) { }); ``` -### dragReleaseEnd +

event: dragReleaseEnd

-Triggered after released item has been animated to position. +Triggered after released item has finished it's position animation. **Arguments** -* **item**  —  *Muuri.Item* - * The released item. +- **item**  —  _Muuri.Item_ + - The released item. + +**Examples** ```javascript grid.on('dragReleaseEnd', function (item) { @@ -2038,9 +2613,11 @@ grid.on('dragReleaseEnd', function (item) { }); ``` -### destroy +

event: destroy

-Triggered after `grid.destroy()` is called. +Triggered after grid is destroyed. + +**Examples** ```javascript grid.on('destroy', function () { @@ -2048,180 +2625,208 @@ grid.on('destroy', function () { }); ``` -### Item methods +

Item methods

+ +- [getGrid](#item-method-getgrid) +- [getElement](#item-method-getelement) +- [getWidth](#item-method-getwidth) +- [getHeight](#item-method-getheight) +- [getMargin](#item-method-getmargin) +- [getPosition](#item-method-getposition) +- [isActive](#item-method-isactive) +- [isVisible](#item-method-isvisible) +- [isShowing](#item-method-isshowing) +- [isHiding](#item-method-ishiding) +- [isPositioning](#item-method-ispositioning) +- [isDragging](#item-method-isdragging) +- [isReleasing](#item-method-isreleasing) +- [isDestroyed](#item-method-isdestroyed) -* [item.getGrid()](#itemgetgrid) -* [item.getElement()](#itemgetelement) -* [item.getWidth()](#itemgetwidth) -* [item.getHeight()](#itemgetheight) -* [item.getMargin()](#itemgetmargin) -* [item.getPosition()](#itemgetposition) -* [item.isActive()](#itemisactive) -* [item.isVisible()](#itemisvisible) -* [item.isShowing()](#itemisshowing) -* [item.isHiding()](#itemishiding) -* [item.isPositioning()](#itemispositioning) -* [item.isDragging()](#itemisdragging) -* [item.isReleasing()](#itemisreleasing) -* [item.isDestroyed()](#itemisdestroyed) +

item.getGrid()

-### item.getGrid() +Get the grid instance the item belongs to. -Get the instance's grid instance. +**Returns**  —  _Muuri_ -**Returns**  —  *Muuri* +**Examples** ```javascript var grid = item.getGrid(); ``` -### item.getElement() +

item.getElement()

-Get the instance element. +Get the item element. -**Returns**  —  *element* +**Returns**  —  _element_ + +**Examples** ```javascript var elem = item.getElement(); ``` -### item.getWidth() +

item.getWidth()

+ +Get item element's cached width (in pixels). The returned value includes the element's paddings and borders. -Get instance element's cached width. The returned value includes the element's paddings and borders. +**Returns**  —  _number_ -**Returns**  —  *number* +**Examples** ```javascript var width = item.getWidth(); ``` -### item.getHeight() +

item.getHeight()

-Get instance element's cached height. The returned value includes the element's paddings and borders. +Get item element's cached height (in pixels). The returned value includes the element's paddings and borders. -**Returns**  —  *number* +**Returns**  —  _number_ + +**Examples** ```javascript var height = item.getHeight(); ``` -### item.getMargin() +

item.getMargin()

+ +Get item element's cached margins (in pixels). -Get instance element's cached margins. +**Returns**  —  _object_ -**Returns**  —  *object* +- **obj.left**  —  _number_ +- **obj.right**  —  _number_ +- **obj.top**  —  _number_ +- **obj.bottom**  —  _number_ -* **obj.left**  —  *number* -* **obj.right**  —  *number* -* **obj.top**  —  *number* -* **obj.bottom**  —  *number* +**Examples** ```javascript var margin = item.getMargin(); ``` -### item.getPosition() +

item.getPosition()

-Get instance element's cached position (relative to the container element). +Get item element's cached position (in pixels, relative to the grid element). -**Returns**  —  *object* +**Returns**  —  _object_ -* **obj.left**  —  *number* -* **obj.top**  —  *number* +- **obj.left**  —  _number_ +- **obj.top**  —  _number_ + +**Examples** ```javascript var position = item.getPosition(); ``` -### item.isActive() +

item.isActive()

+ +Check if the item is currently _active_. Only active items are considered to be part of the layout. -Check if the item is currently *active*. Only active items are considered to be part of the layout. +**Returns**  —  _boolean_ -**Returns**  —  *boolean* +**Examples** ```javascript var isActive = item.isActive(); ``` -### item.isVisible() +

item.isVisible()

+ +Check if the item is currently _visible_. -Check if the item is currently *visible*. +**Returns**  —  _boolean_ -**Returns**  —  *boolean* +**Examples** ```javascript var isVisible = item.isVisible(); ``` -### item.isShowing() +

item.isShowing()

Check if the item is currently animating to visible. -**Returns**  —  *boolean* +**Returns**  —  _boolean_ + +**Examples** ```javascript var isShowing = item.isShowing(); ``` -### item.isHiding() +

item.isHiding()

Check if the item is currently animating to hidden. -**Returns**  —  *boolean* +**Returns**  —  _boolean_ + +**Examples** ```javascript var isHiding = item.isHiding(); ``` -### item.isPositioning() +

item.isPositioning()

Check if the item is currently being positioned. -**Returns**  —  *boolean* +**Returns**  —  _boolean_ + +**Examples** ```javascript var isPositioning = item.isPositioning(); ``` -### item.isDragging() +

item.isDragging()

Check if the item is currently being dragged. -**Returns**  —  *boolean* +**Returns**  —  _boolean_ + +**Examples** ```javascript var isDragging = item.isDragging(); ``` -### item.isReleasing() +

item.isReleasing()

Check if the item is currently being released. -**Returns**  —  *boolean* +**Returns**  —  _boolean_ + +**Examples** ```javascript var isReleasing = item.isReleasing(); ``` -### item.isDestroyed() +

item.isDestroyed()

Check if the item is destroyed. -**Returns**  —  *boolean* +**Returns**  —  _boolean_ + +**Examples** ```javascript var isDestroyed = item.isDestroyed(); ``` -## Credits +

Credits

**Created and maintained by [Niklas Rämö](https://github.com/niklasramo).** -* This project owes much to David DeSandro's [Masonry](http://masonry.desandro.com/), [Packery](http://packery.metafizzy.co/) and [Isotope](https://isotope.metafizzy.co/) libraries. You should go ahead and check them out right now if you haven't yet. Thanks Dave! -* Jukka Jylänki's research [A Thousand Ways to Pack the Bin](http://clb.demon.fi/files/RectangleBinPack.pdf) came in handy when building Muuri's layout algorithms. Thanks Jukka! -* Big thanks to the people behind [Web Animations polyfill](https://github.com/web-animations/web-animations-js), Muuri would be much less cool without smooth animations. -* [Haltu Oy](http://www.haltu.fi/) was responsible for initiating this project in the first place and funded the initial development. Thanks Haltu! +- This project owes much to David DeSandro's [Masonry](http://masonry.desandro.com/), [Packery](http://packery.metafizzy.co/) and [Isotope](https://isotope.metafizzy.co/) libraries. You should go ahead and check them out right now if you haven't yet. Thanks Dave! +- Jukka Jylänki's [survey](https://github.com/juj/RectangleBinPack) "A Thousand Ways to Pack the Bin - A Practical Approach to Two-Dimensional Rectangle Bin Packing" came in handy when building Muuri's layout algorithms. Thanks Jukka! +- Big thanks to the people behind [Web Animations polyfill](https://github.com/web-animations/web-animations-js) for making it possible to use Web Animations API reliably across browsers today. +- [Haltu Oy](http://www.haltu.fi/) was responsible for initiating this project in the first place and funded the initial development. Thanks Haltu! -## License +

License

Copyright © 2015 Haltu Oy. Licensed under **[the MIT license](LICENSE.md)**. diff --git a/demos/basic.html b/demos/basic.html deleted file mode 100644 index 04c37614..00000000 --- a/demos/basic.html +++ /dev/null @@ -1,202 +0,0 @@ - - - - - Muuri Demo - - - -
- -
- -
- -
- - - - - - diff --git a/dist/muuri.js b/dist/muuri.js index f5d3066f..f6a14daa 100644 --- a/dist/muuri.js +++ b/dist/muuri.js @@ -1,6 +1,6 @@ /** -* Muuri v0.8.0 -* https://github.com/haltu/muuri +* Muuri v0.9.0 +* https://muuri.dev/ * Copyright (c) 2015-present, Haltu Oy * Released under the MIT license * https://github.com/haltu/muuri/blob/master/LICENSE.md @@ -10,47 +10,58 @@ * Copyright (c) 2016-present, Niklas Rämö * @license MIT * -* Muuri Ticker / Muuri Emitter / Muuri Queue +* Muuri Ticker / Muuri Emitter / Muuri Dragger * Copyright (c) 2018-present, Niklas Rämö * @license MIT +* +* Muuri AutoScroller +* Copyright (c) 2019-present, Niklas Rämö +* @license MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Muuri = factory()); -}(this, function () { 'use strict'; - - var namespace = 'Muuri'; - var gridInstances = {}; - - var actionSwap = 'swap'; - var actionMove = 'move'; - - var eventSynchronize = 'synchronize'; - var eventLayoutStart = 'layoutStart'; - var eventLayoutEnd = 'layoutEnd'; - var eventAdd = 'add'; - var eventRemove = 'remove'; - var eventShowStart = 'showStart'; - var eventShowEnd = 'showEnd'; - var eventHideStart = 'hideStart'; - var eventHideEnd = 'hideEnd'; - var eventFilter = 'filter'; - var eventSort = 'sort'; - var eventMove = 'move'; - var eventSend = 'send'; - var eventBeforeSend = 'beforeSend'; - var eventReceive = 'receive'; - var eventBeforeReceive = 'beforeReceive'; - var eventDragInit = 'dragInit'; - var eventDragStart = 'dragStart'; - var eventDragMove = 'dragMove'; - var eventDragScroll = 'dragScroll'; - var eventDragEnd = 'dragEnd'; - var eventDragReleaseStart = 'dragReleaseStart'; - var eventDragReleaseEnd = 'dragReleaseEnd'; - var eventDestroy = 'destroy'; +}(this, (function () { 'use strict'; + + var GRID_INSTANCES = {}; + var ITEM_ELEMENT_MAP = typeof Map === 'function' ? new Map() : null; + + var ACTION_SWAP = 'swap'; + var ACTION_MOVE = 'move'; + + var EVENT_SYNCHRONIZE = 'synchronize'; + var EVENT_LAYOUT_START = 'layoutStart'; + var EVENT_LAYOUT_END = 'layoutEnd'; + var EVENT_LAYOUT_ABORT = 'layoutAbort'; + var EVENT_ADD = 'add'; + var EVENT_REMOVE = 'remove'; + var EVENT_SHOW_START = 'showStart'; + var EVENT_SHOW_END = 'showEnd'; + var EVENT_HIDE_START = 'hideStart'; + var EVENT_HIDE_END = 'hideEnd'; + var EVENT_FILTER = 'filter'; + var EVENT_SORT = 'sort'; + var EVENT_MOVE = 'move'; + var EVENT_SEND = 'send'; + var EVENT_BEFORE_SEND = 'beforeSend'; + var EVENT_RECEIVE = 'receive'; + var EVENT_BEFORE_RECEIVE = 'beforeReceive'; + var EVENT_DRAG_INIT = 'dragInit'; + var EVENT_DRAG_START = 'dragStart'; + var EVENT_DRAG_MOVE = 'dragMove'; + var EVENT_DRAG_SCROLL = 'dragScroll'; + var EVENT_DRAG_END = 'dragEnd'; + var EVENT_DRAG_RELEASE_START = 'dragReleaseStart'; + var EVENT_DRAG_RELEASE_END = 'dragReleaseEnd'; + var EVENT_DESTROY = 'destroy'; + + var HAS_TOUCH_EVENTS = 'ontouchstart' in window; + var HAS_POINTER_EVENTS = !!window.PointerEvent; + var HAS_MS_POINTER_EVENTS = !!window.navigator.msPointerEnabled; + + var MAX_SAFE_FLOAT32_INTEGER = 16777216; /** * Event emitter constructor. @@ -61,7 +72,7 @@ this._events = {}; this._queue = []; this._counter = 0; - this._isDestroyed = false; + this._clearOnEmit = false; } /** @@ -73,13 +84,12 @@ * Bind an event listener. * * @public - * @memberof Emitter.prototype * @param {String} event * @param {Function} listener * @returns {Emitter} */ - Emitter.prototype.on = function(event, listener) { - if (this._isDestroyed) return this; + Emitter.prototype.on = function (event, listener) { + if (!this._events || !event || !listener) return this; // Get listeners queue and create it if it does not exist. var listeners = this._events[event]; @@ -95,28 +105,40 @@ * Unbind all event listeners that match the provided listener function. * * @public - * @memberof Emitter.prototype * @param {String} event - * @param {Function} [listener] + * @param {Function} listener * @returns {Emitter} */ - Emitter.prototype.off = function(event, listener) { - if (this._isDestroyed) return this; + Emitter.prototype.off = function (event, listener) { + if (!this._events || !event || !listener) return this; // Get listeners and return immediately if none is found. var listeners = this._events[event]; if (!listeners || !listeners.length) return this; - // If no specific listener is provided remove all listeners. - if (!listener) { - listeners.length = 0; - return this; + // Remove all matching listeners. + var index; + while ((index = listeners.indexOf(listener)) !== -1) { + listeners.splice(index, 1); } - // Remove all matching listeners. - var i = listeners.length; - while (i--) { - if (listener === listeners[i]) listeners.splice(i, 1); + return this; + }; + + /** + * Unbind all listeners of the provided event. + * + * @public + * @param {String} event + * @returns {Emitter} + */ + Emitter.prototype.clear = function (event) { + if (!this._events || !event) return this; + + var listeners = this._events[event]; + if (listeners) { + listeners.length = 0; + delete this._events[event]; } return this; @@ -126,31 +148,46 @@ * Emit all listeners in a specified event with the provided arguments. * * @public - * @memberof Emitter.prototype * @param {String} event - * @param {*} [arg1] - * @param {*} [arg2] - * @param {*} [arg3] + * @param {...*} [args] * @returns {Emitter} */ - Emitter.prototype.emit = function(event, arg1, arg2, arg3) { - if (this._isDestroyed) return this; + Emitter.prototype.emit = function (event) { + if (!this._events || !event) { + this._clearOnEmit = false; + return this; + } // Get event listeners and quit early if there's no listeners. var listeners = this._events[event]; - if (!listeners || !listeners.length) return this; + if (!listeners || !listeners.length) { + this._clearOnEmit = false; + return this; + } var queue = this._queue; - var qLength = queue.length; - var aLength = arguments.length - 1; - var i; + var startIndex = queue.length; + var argsLength = arguments.length - 1; + var args; + + // If we have more than 3 arguments let's put the arguments in an array and + // apply it to the listeners. + if (argsLength > 3) { + args = []; + args.push.apply(args, arguments); + args.shift(); + } // Add the current listeners to the callback queue before we process them. // This is necessary to guarantee that all of the listeners are called in // correct order even if new event listeners are removed/added during // processing and/or events are emitted during processing. - for (i = 0; i < listeners.length; i++) { - queue.push(listeners[i]); + queue.push.apply(queue, listeners); + + // Reset the event's listeners if need be. + if (this._clearOnEmit) { + listeners.length = 0; + this._clearOnEmit = false; } // Increment queue counter. This is needed for the scenarios where emit is @@ -160,15 +197,18 @@ ++this._counter; // Process the queue (the specific part of it for this emit). - for (i = qLength, qLength = queue.length; i < qLength; i++) { + var i = startIndex; + var endIndex = queue.length; + for (; i < endIndex; i++) { // prettier-ignore - aLength === 0 ? queue[i]() : - aLength === 1 ? queue[i](arg1) : - aLength === 2 ? queue[i](arg1, arg2) : - queue[i](arg1, arg2, arg3); + argsLength === 0 ? queue[i]() : + argsLength === 1 ? queue[i](arguments[1]) : + argsLength === 2 ? queue[i](arguments[1], arguments[2]) : + argsLength === 3 ? queue[i](arguments[1], arguments[2], arguments[3]) : + queue[i].apply(null, args); // Stop processing if the emitter is destroyed. - if (this._isDestroyed) return this; + if (!this._events) return this; } // Decrement queue process counter. @@ -181,365 +221,219 @@ }; /** - * Destroy emitter instance. Basically just removes all bound listeners. + * Emit all listeners in a specified event with the provided arguments and + * remove the event's listeners just before calling the them. This method allows + * the emitter to serve as a queue where all listeners are called only once. * * @public - * @memberof Emitter.prototype + * @param {String} event + * @param {...*} [args] * @returns {Emitter} */ - Emitter.prototype.destroy = function() { - if (this._isDestroyed) return this; - - var events = this._events; - var event; - - // Flag as destroyed. - this._isDestroyed = true; - - // Reset queue (if queue is currently processing this will also stop that). - this._queue.length = this._counter = 0; - - // Remove all listeners. - for (event in events) { - if (events[event]) { - events[event].length = 0; - events[event] = undefined; - } - } - + Emitter.prototype.burst = function () { + if (!this._events) return this; + this._clearOnEmit = true; + this.emit.apply(this, arguments); return this; }; - // Set up the default export values. - var transformStyle = 'transform'; - var transformProp = 'transform'; - - // Find the supported transform prop and style names. - var docElemStyle = window.document.documentElement.style; - var style = 'transform'; - var styleCap = 'Transform'; - var found = false; - ['', 'Webkit', 'Moz', 'O', 'ms'].forEach(function(prefix) { - if (found) return; - var propName = prefix ? prefix + styleCap : style; - if (docElemStyle[propName] !== undefined) { - prefix = prefix.toLowerCase(); - transformStyle = prefix ? '-' + prefix + '-' + style : style; - transformProp = propName; - found = true; - } - }); - - var stylesCache = typeof WeakMap === 'function' ? new WeakMap() : null; - /** - * Returns the computed value of an element's style property as a string. + * Check how many listeners there are for a specific event. * - * @param {HTMLElement} element - * @param {String} style - * @returns {String} + * @public + * @param {String} event + * @returns {Boolean} */ - function getStyle(element, style) { - var styles = stylesCache && stylesCache.get(element); - if (!styles) { - styles = window.getComputedStyle(element, null); - if (stylesCache) stylesCache.set(element, styles); - } - return styles.getPropertyValue(style === 'transform' ? transformStyle : style); - } - - var styleNameRegEx = /([A-Z])/g; + Emitter.prototype.countListeners = function (event) { + if (!this._events) return 0; + var listeners = this._events[event]; + return listeners ? listeners.length : 0; + }; /** - * Transforms a camel case style property to kebab case style property. + * Destroy emitter instance. Basically just removes all bound listeners. * - * @param {String} string - * @returns {String} + * @public + * @returns {Emitter} */ - function getStyleName(string) { - return string.replace(styleNameRegEx, '-$1').toLowerCase(); - } + Emitter.prototype.destroy = function () { + if (!this._events) return this; + this._queue.length = this._counter = 0; + this._events = null; + return this; + }; - var strFunction = 'function'; + var pointerout = HAS_POINTER_EVENTS ? 'pointerout' : HAS_MS_POINTER_EVENTS ? 'MSPointerOut' : ''; + var waitDuration = 100; /** - * Check if a value is a function. + * If you happen to use Edge or IE on a touch capable device there is a + * a specific case where pointercancel and pointerend events are never emitted, + * even though one them should always be emitted when you release your finger + * from the screen. The bug appears specifically when Muuri shifts the dragged + * element's position in the DOM after pointerdown event, IE and Edge don't like + * that behaviour and quite often forget to emit the pointerend/pointercancel + * event. But, they do emit pointerout event so we utilize that here. + * Specifically, if there has been no pointermove event within 100 milliseconds + * since the last pointerout event we force cancel the drag operation. This hack + * works surprisingly well 99% of the time. There is that 1% chance there still + * that dragged items get stuck but it is what it is. * - * @param {*} val - * @returns {Boolean} + * @class + * @param {Dragger} dragger */ - function isFunction(val) { - return typeof val === strFunction; - } + function EdgeHack(dragger) { + if (!pointerout) return; - var transformStyle$1 = 'transform'; + this._dragger = dragger; + this._timeout = null; + this._outEvent = null; + this._isActive = false; - /** - * Set inline styles to an element. - * - * @param {HTMLElement} element - * @param {Object} styles - */ - function setStyles(element, styles) { - for (var prop in styles) { - element.style[prop === transformStyle$1 ? transformProp : prop] = styles[prop]; - } - } + this._addBehaviour = this._addBehaviour.bind(this); + this._removeBehaviour = this._removeBehaviour.bind(this); + this._onTimeout = this._onTimeout.bind(this); + this._resetData = this._resetData.bind(this); + this._onStart = this._onStart.bind(this); + this._onOut = this._onOut.bind(this); - /** - * Item animation handler powered by Web Animations API. - * - * @class - * @param {HTMLElement} element - */ - function ItemAnimate(element) { - this._element = element; - this._animation = null; - this._callback = null; - this._props = []; - this._values = []; - this._keyframes = []; - this._options = {}; - this._isDestroyed = false; - this._onFinish = this._onFinish.bind(this); + this._dragger.on('start', this._onStart); } /** - * Public prototype methods - * ************************ + * @private */ + EdgeHack.prototype._addBehaviour = function () { + if (this._isActive) return; + this._isActive = true; + this._dragger.on('move', this._resetData); + this._dragger.on('cancel', this._removeBehaviour); + this._dragger.on('end', this._removeBehaviour); + window.addEventListener(pointerout, this._onOut); + }; /** - * Start instance's animation. Automatically stops current animation if it is - * running. - * - * @public - * @memberof ItemAnimate.prototype - * @param {Object} propsFrom - * @param {Object} propsTo - * @param {Object} [options] - * @param {Number} [options.duration=300] - * @param {String} [options.easing='ease'] - * @param {Function} [options.onFinish] + * @private */ - ItemAnimate.prototype.start = function(propsFrom, propsTo, options) { - if (this._isDestroyed) return; - - var animation = this._animation; - var currentProps = this._props; - var currentValues = this._values; - var opts = options || 0; - var cancelAnimation = false; - - // If we have an existing animation running, let's check if it needs to be - // cancelled or if it can continue running. - if (animation) { - var propCount = 0; - var propIndex; - - // Check if the requested animation target props and values match with the - // current props and values. - for (var propName in propsTo) { - ++propCount; - propIndex = currentProps.indexOf(propName); - if (propIndex === -1 || propsTo[propName] !== currentValues[propIndex]) { - cancelAnimation = true; - break; - } - } - - // Check if the target props count matches current props count. This is - // needed for the edge case scenario where target props contain the same - // styles as current props, but the current props have some additional - // props. - if (!cancelAnimation && propCount !== currentProps.length) { - cancelAnimation = true; - } - } - - // Cancel animation (if required). - if (cancelAnimation) animation.cancel(); - - // Store animation callback. - this._callback = isFunction(opts.onFinish) ? opts.onFinish : null; - - // If we have a running animation that does not need to be cancelled, let's - // call it a day here and let it run. - if (animation && !cancelAnimation) return; - - // Store target props and values to instance. - currentProps.length = currentValues.length = 0; - for (propName in propsTo) { - currentProps.push(propName); - currentValues.push(propsTo[propName]); - } - - // Set up keyframes. - var animKeyframes = this._keyframes; - animKeyframes[0] = propsFrom; - animKeyframes[1] = propsTo; - - // Set up options. - var animOptions = this._options; - animOptions.duration = opts.duration || 300; - animOptions.easing = opts.easing || 'ease'; - - // Start the animation - var element = this._element; - animation = element.animate(animKeyframes, animOptions); - animation.onfinish = this._onFinish; - this._animation = animation; - - // Set the end styles. This makes sure that the element stays at the end - // values after animation is finished. - setStyles(element, propsTo); + EdgeHack.prototype._removeBehaviour = function () { + if (!this._isActive) return; + this._dragger.off('move', this._resetData); + this._dragger.off('cancel', this._removeBehaviour); + this._dragger.off('end', this._removeBehaviour); + window.removeEventListener(pointerout, this._onOut); + this._resetData(); + this._isActive = false; }; /** - * Stop instance's current animation if running. - * - * @public - * @memberof ItemAnimate.prototype - * @param {Object} [styles] + * @private */ - ItemAnimate.prototype.stop = function(styles) { - if (this._isDestroyed || !this._animation) return; - - var element = this._element; - var currentProps = this._props; - var currentValues = this._values; - var propName; - var propValue; - var i; - - // Calculate (if not provided) and set styles. - if (!styles) { - for (i = 0; i < currentProps.length; i++) { - propName = currentProps[i]; - propValue = getStyle(element, getStyleName(propName)); - element.style[propName === 'transform' ? transformProp : propName] = propValue; - } - } else { - setStyles(element, styles); - } - - // Cancel animation. - this._animation.cancel(); - this._animation = this._callback = null; - - // Reset current props and values. - currentProps.length = currentValues.length = 0; + EdgeHack.prototype._resetData = function () { + window.clearTimeout(this._timeout); + this._timeout = null; + this._outEvent = null; }; /** - * Check if the item is being animated currently. - * - * @public - * @memberof ItemAnimate.prototype - * @return {Boolean} + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ - ItemAnimate.prototype.isAnimating = function() { - return !!this._animation; + EdgeHack.prototype._onStart = function (e) { + if (e.pointerType === 'mouse') return; + this._addBehaviour(); }; /** - * Destroy the instance and stop current animation if it is running. - * - * @public - * @memberof ItemAnimate.prototype + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ - ItemAnimate.prototype.destroy = function() { - if (this._isDestroyed) return; - this.stop(); - this._element = this._options = this._keyframes = null; - this._isDestroyed = true; + EdgeHack.prototype._onOut = function (e) { + if (!this._dragger._getTrackedTouch(e)) return; + this._resetData(); + this._outEvent = e; + this._timeout = window.setTimeout(this._onTimeout, waitDuration); }; /** - * Private prototype methods - * ************************* + * @private */ + EdgeHack.prototype._onTimeout = function () { + var e = this._outEvent; + this._resetData(); + if (this._dragger.isActive()) this._dragger._onCancel(e); + }; /** - * Animation end handler. - * - * @private - * @memberof ItemAnimate.prototype + * @public */ - ItemAnimate.prototype._onFinish = function() { - var callback = this._callback; - this._animation = this._callback = null; - this._props.length = this._values.length = 0; - callback && callback(); + EdgeHack.prototype.destroy = function () { + if (!pointerout) return; + this._dragger.off('start', this._onStart); + this._removeBehaviour(); }; + // Playing it safe here, test all potential prefixes capitalized and lowercase. var vendorPrefixes = ['', 'webkit', 'moz', 'ms', 'o', 'Webkit', 'Moz', 'MS', 'O']; + var cache = {}; /** * Get prefixed CSS property name when given a non-prefixed CSS property name. - * @param {Object} elemStyle - * @param {String} propName - * @returns {!String} + * Returns null if the property is not supported at all. + * + * @param {CSSStyleDeclaration} style + * @param {String} prop + * @returns {String} */ - function getPrefixedPropName(elemStyle, propName) { - var camelPropName = propName[0].toUpperCase() + propName.slice(1); - var i = 0; - var prefix; - var prefixedPropName; + function getPrefixedPropName(style, prop) { + var prefixedProp = cache[prop] || ''; + if (prefixedProp) return prefixedProp; + var camelProp = prop[0].toUpperCase() + prop.slice(1); + var i = 0; while (i < vendorPrefixes.length) { - prefix = vendorPrefixes[i]; - prefixedPropName = prefix ? prefix + camelPropName : propName; - if (prefixedPropName in elemStyle) return prefixedPropName; + prefixedProp = vendorPrefixes[i] ? vendorPrefixes[i] + camelProp : prop; + if (prefixedProp in style) { + cache[prop] = prefixedProp; + return prefixedProp; + } ++i; } - return null; + return ''; } - var dt = 1000 / 60; + /** + * Check if passive events are supported. + * https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection + * + * @returns {Boolean} + */ + function hasPassiveEvents() { + var isPassiveEventsSupported = false; - var raf = ( - window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - return this.setTimeout(function() { - callback(dt); - }, dt); - } - ).bind(window); + try { + var passiveOpts = Object.defineProperty({}, 'passive', { + get: function () { + isPassiveEventsSupported = true; + }, + }); + window.addEventListener('testPassive', null, passiveOpts); + window.removeEventListener('testPassive', null, passiveOpts); + } catch (e) {} - // Detect support for passive events: - // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection - var isPassiveEventsSupported = false; - try { - var passiveOpts = Object.defineProperty({}, 'passive', { - get: function() { - isPassiveEventsSupported = true; - } - }); - window.addEventListener('testPassive', null, passiveOpts); - window.removeEventListener('testPassive', null, passiveOpts); - } catch (e) {} + return isPassiveEventsSupported; + } - // Dragger events. - var events = { - start: 'start', - move: 'move', - end: 'end', - cancel: 'cancel' - }; + var ua = window.navigator.userAgent.toLowerCase(); + var isEdge = ua.indexOf('edge') > -1; + var isIE = ua.indexOf('trident') > -1; + var isFirefox = ua.indexOf('firefox') > -1; + var isAndroid = ua.indexOf('android') > -1; - var hasTouchEvents = !!('ontouchstart' in window || window.TouchEvent); - var hasPointerEvents = !!window.PointerEvent; - var hasMsPointerEvents = !!window.navigator.msPointerEnabled; - var isAndroid = /(android)/i.test(window.navigator.userAgent); - var listenerOptions = isPassiveEventsSupported ? { passive: true } : false; + var listenerOptions = hasPassiveEvents() ? { passive: true } : false; var taProp = 'touchAction'; - var taPropPrefixed = getPrefixedPropName(window.document.documentElement.style, taProp); + var taPropPrefixed = getPrefixedPropName(document.documentElement.style, taProp); var taDefaultValue = 'auto'; /** @@ -556,7 +450,7 @@ this._isDestroyed = false; this._cssProps = {}; this._touchAction = ''; - this._startEvent = null; + this._isActive = false; this._pointerId = null; this._startTime = 0; @@ -565,34 +459,31 @@ this._currentX = 0; this._currentY = 0; - this._preStartCheck = this._preStartCheck.bind(this); - this._abortNonCancelable = this._abortNonCancelable.bind(this); this._onStart = this._onStart.bind(this); this._onMove = this._onMove.bind(this); this._onCancel = this._onCancel.bind(this); this._onEnd = this._onEnd.bind(this); - // Apply initial css props. + // Can't believe had to build a freaking class for a hack! + this._edgeHack = null; + if ((isEdge || isIE) && (HAS_POINTER_EVENTS || HAS_MS_POINTER_EVENTS)) { + this._edgeHack = new EdgeHack(this); + } + + // Apply initial CSS props. this.setCssProps(cssProps); - // If touch action was not provided with initial css props let's assume it's + // If touch action was not provided with initial CSS props let's assume it's // auto. if (!this._touchAction) { this.setTouchAction(taDefaultValue); } - // Prevent native link/image dragging for the item and it's ancestors. + // Prevent native link/image dragging for the item and it's children. element.addEventListener('dragstart', Dragger._preventDefault, false); // Listen to start event. - element.addEventListener(Dragger._events.start, this._preStartCheck, listenerOptions); - - // If we have touch events, but no pointer events we need to also listen for - // mouse events in addition to touch events for devices which support both - // mouse and touch interaction. - if (hasTouchEvents && !hasPointerEvents && !hasMsPointerEvents) { - element.addEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions); - } + element.addEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions); } /** @@ -604,39 +495,46 @@ start: 'pointerdown', move: 'pointermove', cancel: 'pointercancel', - end: 'pointerup' + end: 'pointerup', }; Dragger._msPointerEvents = { start: 'MSPointerDown', move: 'MSPointerMove', cancel: 'MSPointerCancel', - end: 'MSPointerUp' + end: 'MSPointerUp', }; Dragger._touchEvents = { start: 'touchstart', move: 'touchmove', cancel: 'touchcancel', - end: 'touchend' + end: 'touchend', }; Dragger._mouseEvents = { start: 'mousedown', move: 'mousemove', cancel: '', - end: 'mouseup' + end: 'mouseup', }; - Dragger._events = (function() { - if (hasPointerEvents) return Dragger._pointerEvents; - if (hasMsPointerEvents) return Dragger._msPointerEvents; - if (hasTouchEvents) return Dragger._touchEvents; + Dragger._inputEvents = (function () { + if (HAS_TOUCH_EVENTS) return Dragger._touchEvents; + if (HAS_POINTER_EVENTS) return Dragger._pointerEvents; + if (HAS_MS_POINTER_EVENTS) return Dragger._msPointerEvents; return Dragger._mouseEvents; })(); Dragger._emitter = new Emitter(); + Dragger._emitterEvents = { + start: 'start', + move: 'move', + end: 'end', + cancel: 'cancel', + }; + Dragger._activeInstances = []; /** @@ -644,53 +542,55 @@ * ************************ */ - Dragger._preventDefault = function(e) { + Dragger._preventDefault = function (e) { if (e.preventDefault && e.cancelable !== false) e.preventDefault(); }; - Dragger._activateInstance = function(instance) { + Dragger._activateInstance = function (instance) { var index = Dragger._activeInstances.indexOf(instance); if (index > -1) return; Dragger._activeInstances.push(instance); - Dragger._emitter.on(events.move, instance._onMove); - Dragger._emitter.on(events.cancel, instance._onCancel); - Dragger._emitter.on(events.end, instance._onEnd); + Dragger._emitter.on(Dragger._emitterEvents.move, instance._onMove); + Dragger._emitter.on(Dragger._emitterEvents.cancel, instance._onCancel); + Dragger._emitter.on(Dragger._emitterEvents.end, instance._onEnd); if (Dragger._activeInstances.length === 1) { Dragger._bindListeners(); } }; - Dragger._deactivateInstance = function(instance) { + Dragger._deactivateInstance = function (instance) { var index = Dragger._activeInstances.indexOf(instance); if (index === -1) return; Dragger._activeInstances.splice(index, 1); - Dragger._emitter.off(events.move, instance._onMove); - Dragger._emitter.off(events.cancel, instance._onCancel); - Dragger._emitter.off(events.end, instance._onEnd); + Dragger._emitter.off(Dragger._emitterEvents.move, instance._onMove); + Dragger._emitter.off(Dragger._emitterEvents.cancel, instance._onCancel); + Dragger._emitter.off(Dragger._emitterEvents.end, instance._onEnd); if (!Dragger._activeInstances.length) { Dragger._unbindListeners(); } }; - Dragger._bindListeners = function() { - var events = Dragger._events; - window.addEventListener(events.move, Dragger._onMove, listenerOptions); - window.addEventListener(events.end, Dragger._onEnd, listenerOptions); - events.cancel && window.addEventListener(events.cancel, Dragger._onCancel, listenerOptions); + Dragger._bindListeners = function () { + window.addEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions); + window.addEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions); + if (Dragger._inputEvents.cancel) { + window.addEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions); + } }; - Dragger._unbindListeners = function() { - var events = Dragger._events; - window.removeEventListener(events.move, Dragger._onMove, listenerOptions); - window.removeEventListener(events.end, Dragger._onEnd, listenerOptions); - events.cancel && window.removeEventListener(events.cancel, Dragger._onCancel, listenerOptions); + Dragger._unbindListeners = function () { + window.removeEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions); + window.removeEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions); + if (Dragger._inputEvents.cancel) { + window.removeEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions); + } }; - Dragger._getEventPointerId = function(event) { + Dragger._getEventPointerId = function (event) { // If we have pointer id available let's use it. if (typeof event.pointerId === 'number') { return event.pointerId; @@ -705,7 +605,7 @@ return 1; }; - Dragger._getTouchById = function(event, id) { + Dragger._getTouchById = function (event, id) { // If we have a pointer event return the whole event if there's a match, and // null otherwise. if (typeof event.pointerId === 'number') { @@ -728,16 +628,16 @@ return event; }; - Dragger._onMove = function(e) { - Dragger._emitter.emit(events.move, e); + Dragger._onMove = function (e) { + Dragger._emitter.emit(Dragger._emitterEvents.move, e); }; - Dragger._onCancel = function(e) { - Dragger._emitter.emit(events.cancel, e); + Dragger._onCancel = function (e) { + Dragger._emitter.emit(Dragger._emitterEvents.cancel, e); }; - Dragger._onEnd = function(e) { - Dragger._emitter.emit(events.end, e); + Dragger._onEnd = function (e) { + Dragger._emitter.emit(Dragger._emitterEvents.end, e); }; /** @@ -749,25 +649,15 @@ * Reset current drag operation (if any). * * @private - * @memberof Dragger.prototype */ - Dragger.prototype._reset = function() { - if (this._isDestroyed) return; - + Dragger.prototype._reset = function () { this._pointerId = null; this._startTime = 0; this._startX = 0; this._startY = 0; this._currentX = 0; this._currentY = 0; - this._startEvent = null; - - this._element.removeEventListener( - Dragger._touchEvents.start, - this._abortNonCancelable, - listenerOptions - ); - + this._isActive = false; Dragger._deactivateInstance(this); }; @@ -775,12 +665,11 @@ * Create a custom dragger event from a raw event. * * @private - * @memberof Dragger.prototype * @param {String} type * @param {(PointerEvent|TouchEvent|MouseEvent)} e - * @returns {DraggerEvent} + * @returns {Object} */ - Dragger.prototype._createEvent = function(type, e) { + Dragger.prototype._createEvent = function (type, e) { var touch = this._getTrackedTouch(e); return { // Hammer.js compatibility interface. @@ -789,9 +678,10 @@ distance: this.getDistance(), deltaX: this.getDeltaX(), deltaY: this.getDeltaY(), - deltaTime: type === events.start ? 0 : this.getDeltaTime(), - isFirst: type === events.start, - isFinal: type === events.end || type === events.cancel, + deltaTime: type === Dragger._emitterEvents.start ? 0 : this.getDeltaTime(), + isFirst: type === Dragger._emitterEvents.start, + isFinal: type === Dragger._emitterEvents.end || type === Dragger._emitterEvents.cancel, + pointerType: e.pointerType || (e.touches ? 'touch' : 'mouse'), // Partial Touch API interface. identifier: this._pointerId, screenX: touch.screenX, @@ -800,7 +690,7 @@ clientY: touch.clientY, pageX: touch.pageX, pageY: touch.pageY, - target: touch.target + target: touch.target, }; }; @@ -808,11 +698,10 @@ * Emit a raw event as dragger event internally. * * @private - * @memberof Dragger.prototype * @param {String} type * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ - Dragger.prototype._emit = function(type, e) { + Dragger.prototype._emit = function (type, e) { this._emitter.emit(type, this._createEvent(type, e)); }; @@ -825,135 +714,68 @@ * it will be returned immediately. * * @private - * @memberof Dragger.prototype - * @param {(PointerEvent|TouchEvent|MouseEvent)} + * @param {(PointerEvent|TouchEvent|MouseEvent)} e * @returns {?(Touch|PointerEvent|MouseEvent)} */ - Dragger.prototype._getTrackedTouch = function(e) { - if (this._pointerId === null) { - return null; - } else { - return Dragger._getTouchById(e, this._pointerId); - } + Dragger.prototype._getTrackedTouch = function (e) { + if (this._pointerId === null) return null; + return Dragger._getTouchById(e, this._pointerId); }; /** - * A pre-handler for start event that checks if we can start dragging. + * Handler for start event. * * @private - * @memberof Dragger.prototype * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ - Dragger.prototype._preStartCheck = function(e) { + Dragger.prototype._onStart = function (e) { if (this._isDestroyed) return; - // Make sure the element is not being dragged currently. - if (this.isDragging()) return; - - // Special cancelable check for Android to prevent drag procedure from - // starting if native scrolling is in progress. Part 1. - if (isAndroid && e.cancelable === false) return; - - // Make sure left button is pressed on mouse. - if (e.button) return; + // If pointer id is already assigned let's return early. + if (this._pointerId !== null) return; // Get (and set) pointer id. this._pointerId = Dragger._getEventPointerId(e); if (this._pointerId === null) return; - // Store the start event and trigger start (async or sync). Pointer events - // are emitted before touch events if the browser supports both of them. And - // if you try to move an element before `touchstart` is emitted the pointer - // events for that element will be canceled. The fix is to delay the emitted - // pointer events in such a scenario by one frame so that `touchstart` has - // time to be emitted before the element is (potentially) moved. - this._startEvent = e; - if (hasTouchEvents && (hasPointerEvents || hasMsPointerEvents)) { - // Special cancelable check for Android to prevent drag procedure from - // starting if native scrolling is in progress. Part 2. - if (isAndroid) { - this._element.addEventListener( - Dragger._touchEvents.start, - this._abortNonCancelable, - listenerOptions - ); - } - raf(this._onStart); - } else { - this._onStart(); - } - }; - - /** - * Abort start event if it turns out to be non-cancelable. - * - * @private - * @memberof Dragger.prototype - * @param {(PointerEvent|TouchEvent|MouseEvent)} e - */ - Dragger.prototype._abortNonCancelable = function(e) { - this._element.removeEventListener( - Dragger._touchEvents.start, - this._abortNonCancelable, - listenerOptions - ); - - if (this._startEvent && e.cancelable === false) { - this._pointerId = null; - this._startEvent = null; - } - }; - - /** - * Start the drag procedure if possible. - * - * @private - * @memberof Dragger.prototype - */ - Dragger.prototype._onStart = function() { - var e = this._startEvent; - if (!e) return; - - this._startEvent = null; - + // Setup initial data and emit start event. var touch = this._getTrackedTouch(e); - if (!touch) return; - - // Set up init data and emit start event. this._startX = this._currentX = touch.clientX; this._startY = this._currentY = touch.clientY; this._startTime = Date.now(); - this._emit(events.start, e); - Dragger._activateInstance(this); + this._isActive = true; + this._emit(Dragger._emitterEvents.start, e); + + // If the drag procedure was not reset within the start procedure let's + // activate the instance (start listening to move/cancel/end events). + if (this._isActive) { + Dragger._activateInstance(this); + } }; /** * Handler for move event. * * @private - * @memberof Dragger.prototype * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ - Dragger.prototype._onMove = function(e) { + Dragger.prototype._onMove = function (e) { var touch = this._getTrackedTouch(e); if (!touch) return; - this._currentX = touch.clientX; this._currentY = touch.clientY; - this._emit(events.move, e); + this._emit(Dragger._emitterEvents.move, e); }; /** - * Handler for move cancel event. + * Handler for cancel event. * * @private - * @memberof Dragger.prototype * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ - Dragger.prototype._onCancel = function(e) { + Dragger.prototype._onCancel = function (e) { if (!this._getTrackedTouch(e)) return; - - this._emit(events.cancel, e); + this._emit(Dragger._emitterEvents.cancel, e); this._reset(); }; @@ -961,13 +783,11 @@ * Handler for end event. * * @private - * @memberof Dragger.prototype * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ - Dragger.prototype._onEnd = function(e) { + Dragger.prototype._onEnd = function (e) { if (!this._getTrackedTouch(e)) return; - - this._emit(events.end, e); + this._emit(Dragger._emitterEvents.end, e); this._reset(); }; @@ -980,21 +800,19 @@ * Check if the element is being dragged at the moment. * * @public - * @memberof Dragger.prototype * @returns {Boolean} */ - Dragger.prototype.isDragging = function() { - return this._pointerId !== null; + Dragger.prototype.isActive = function () { + return this._isActive; }; /** * Set element's touch-action CSS property. * * @public - * @memberof Dragger.prototype * @param {String} value */ - Dragger.prototype.setTouchAction = function(value) { + Dragger.prototype.setTouchAction = function (value) { // Store unmodified touch action value (we trust user input here). this._touchAction = value; @@ -1008,11 +826,13 @@ // that prevents default action on touch start event. A dirty hack, but best // we can do for now. The other options would be to somehow polyfill the // unsupported touch action behavior with custom heuristics which sounds like - // a can of worms. - if (hasTouchEvents) { - this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false); - if (this._element.style[taPropPrefixed] !== value) { - this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false); + // a can of worms. We do a special exception here for Firefox Android which's + // touch-action does not work properly if the dragged element is moved in the + // the DOM tree on touchstart. + if (HAS_TOUCH_EVENTS) { + this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); + if (this._element.style[taPropPrefixed] !== value || (isFirefox && isAndroid)) { + this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); } } }; @@ -1022,10 +842,9 @@ * props with value pairs as it's first argument. * * @public - * @memberof Dragger.prototype * @param {Object} [newProps] */ - Dragger.prototype.setCssProps = function(newProps) { + Dragger.prototype.setCssProps = function (newProps) { if (!newProps) return; var currentProps = this._cssProps; @@ -1065,10 +884,9 @@ * Positive value indicates movement from left to right. * * @public - * @memberof Dragger.prototype * @returns {Number} */ - Dragger.prototype.getDeltaX = function() { + Dragger.prototype.getDeltaX = function () { return this._currentX - this._startX; }; @@ -1077,10 +895,9 @@ * Positive value indicates movement from top to bottom. * * @public - * @memberof Dragger.prototype * @returns {Number} */ - Dragger.prototype.getDeltaY = function() { + Dragger.prototype.getDeltaY = function () { return this._currentY - this._startY; }; @@ -1088,10 +905,9 @@ * How far (in pixels) has pointer moved from start position. * * @public - * @memberof Dragger.prototype * @returns {Number} */ - Dragger.prototype.getDistance = function() { + Dragger.prototype.getDistance = function () { var x = this.getDeltaX(); var y = this.getDeltaY(); return Math.sqrt(x * x + y * y); @@ -1101,10 +917,9 @@ * How long has pointer been dragged. * * @public - * @memberof Dragger.prototype * @returns {Number} */ - Dragger.prototype.getDeltaTime = function() { + Dragger.prototype.getDeltaTime = function () { return this._startTime ? Date.now() - this._startTime : 0; }; @@ -1112,12 +927,11 @@ * Bind drag event listeners. * * @public - * @memberof Dragger.prototype * @param {String} eventName * - 'start', 'move', 'cancel' or 'end'. * @param {Function} listener */ - Dragger.prototype.on = function(eventName, listener) { + Dragger.prototype.on = function (eventName, listener) { this._emitter.on(eventName, listener); }; @@ -1125,12 +939,11 @@ * Unbind drag event listeners. * * @public - * @memberof Dragger.prototype * @param {String} eventName * - 'start', 'move', 'cancel' or 'end'. * @param {Function} listener */ - Dragger.prototype.off = function(events, listener) { + Dragger.prototype.off = function (eventName, listener) { this._emitter.off(eventName, listener); }; @@ -1138,13 +951,13 @@ * Destroy the instance and unbind all drag event listeners. * * @public - * @memberof Dragger.prototype */ - Dragger.prototype.destroy = function() { + Dragger.prototype.destroy = function () { if (this._isDestroyed) return; var element = this._element; - var events = Dragger._events; + + if (this._edgeHack) this._edgeHack.destroy(); // Reset data and deactivate the instance. this._reset(); @@ -1153,10 +966,9 @@ this._emitter.destroy(); // Unbind event handlers. - element.removeEventListener(events.start, this._preStartCheck, listenerOptions); - element.removeEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions); + element.removeEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions); element.removeEventListener('dragstart', Dragger._preventDefault, false); - element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false); + element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); // Reset styles. for (var prop in this._cssProps) { @@ -1171,380 +983,1559 @@ this._isDestroyed = true; }; + var dt = 1000 / 60; + + var raf = ( + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { + return this.setTimeout(function () { + callback(Date.now()); + }, dt); + } + ).bind(window); + /** * A ticker system for handling DOM reads and writes in an efficient way. - * Contains a read queue and a write queue that are processed on the next - * animation frame when needed. * * @class */ - function Ticker() { + function Ticker(numLanes) { this._nextStep = null; - - this._queue = []; - this._reads = {}; - this._writes = {}; - - this._batch = []; - this._batchReads = {}; - this._batchWrites = {}; - + this._lanes = []; + this._stepQueue = []; + this._stepCallbacks = {}; this._step = this._step.bind(this); + for (var i = 0; i < numLanes; i++) { + this._lanes.push(new TickerLane()); + } } - Ticker.prototype.add = function(id, readOperation, writeOperation, isPrioritized) { - // First, let's check if an item has been added to the queues with the same id - // and if so -> remove it. - var currentIndex = this._queue.indexOf(id); - if (currentIndex > -1) this._queue[currentIndex] = undefined; + Ticker.prototype._step = function (time) { + var lanes = this._lanes; + var stepQueue = this._stepQueue; + var stepCallbacks = this._stepCallbacks; + var i, j, id, laneQueue, laneCallbacks, laneIndices; - // Add entry. - isPrioritized ? this._queue.unshift(id) : this._queue.push(id); - this._reads[id] = readOperation; - this._writes[id] = writeOperation; + this._nextStep = null; - // Finally, let's kick-start the next tick if it is not running yet. - if (!this._nextStep) this._nextStep = raf(this._step); - }; + for (i = 0; i < lanes.length; i++) { + laneQueue = lanes[i].queue; + laneCallbacks = lanes[i].callbacks; + laneIndices = lanes[i].indices; + for (j = 0; j < laneQueue.length; j++) { + id = laneQueue[j]; + if (!id) continue; + stepQueue.push(id); + stepCallbacks[id] = laneCallbacks[id]; + delete laneCallbacks[id]; + delete laneIndices[id]; + } + laneQueue.length = 0; + } - Ticker.prototype.cancel = function(id) { - var currentIndex = this._queue.indexOf(id); - if (currentIndex > -1) { - this._queue[currentIndex] = undefined; - delete this._reads[id]; - delete this._writes[id]; + for (i = 0; i < stepQueue.length; i++) { + id = stepQueue[i]; + if (stepCallbacks[id]) stepCallbacks[id](time); + delete stepCallbacks[id]; } + + stepQueue.length = 0; }; - Ticker.prototype._step = function() { - var queue = this._queue; - var reads = this._reads; - var writes = this._writes; - var batch = this._batch; - var batchReads = this._batchReads; - var batchWrites = this._batchWrites; - var length = queue.length; - var id; - var i; + Ticker.prototype.add = function (laneIndex, id, callback) { + this._lanes[laneIndex].add(id, callback); + if (!this._nextStep) this._nextStep = raf(this._step); + }; - // Reset ticker. - this._nextStep = null; + Ticker.prototype.remove = function (laneIndex, id) { + this._lanes[laneIndex].remove(id); + }; - // Setup queues and callback placeholders. - for (i = 0; i < length; i++) { - id = queue[i]; - if (!id) continue; + /** + * A lane for ticker. + * + * @class + */ + function TickerLane() { + this.queue = []; + this.indices = {}; + this.callbacks = {}; + } - batch.push(id); + TickerLane.prototype.add = function (id, callback) { + var index = this.indices[id]; + if (index !== undefined) this.queue[index] = undefined; + this.queue.push(id); + this.callbacks[id] = callback; + this.indices[id] = this.queue.length - 1; + }; + + TickerLane.prototype.remove = function (id) { + var index = this.indices[id]; + if (index === undefined) return; + this.queue[index] = undefined; + delete this.callbacks[id]; + delete this.indices[id]; + }; + + var LAYOUT_READ = 'layoutRead'; + var LAYOUT_WRITE = 'layoutWrite'; + var VISIBILITY_READ = 'visibilityRead'; + var VISIBILITY_WRITE = 'visibilityWrite'; + var DRAG_START_READ = 'dragStartRead'; + var DRAG_START_WRITE = 'dragStartWrite'; + var DRAG_MOVE_READ = 'dragMoveRead'; + var DRAG_MOVE_WRITE = 'dragMoveWrite'; + var DRAG_SCROLL_READ = 'dragScrollRead'; + var DRAG_SCROLL_WRITE = 'dragScrollWrite'; + var DRAG_SORT_READ = 'dragSortRead'; + var PLACEHOLDER_LAYOUT_READ = 'placeholderLayoutRead'; + var PLACEHOLDER_LAYOUT_WRITE = 'placeholderLayoutWrite'; + var PLACEHOLDER_RESIZE_WRITE = 'placeholderResizeWrite'; + var AUTO_SCROLL_READ = 'autoScrollRead'; + var AUTO_SCROLL_WRITE = 'autoScrollWrite'; + var DEBOUNCE_READ = 'debounceRead'; + + var LANE_READ = 0; + var LANE_READ_TAIL = 1; + var LANE_WRITE = 2; + + var ticker = new Ticker(3); + + function addLayoutTick(itemId, read, write) { + ticker.add(LANE_READ, LAYOUT_READ + itemId, read); + ticker.add(LANE_WRITE, LAYOUT_WRITE + itemId, write); + } - batchReads[id] = reads[id]; - delete reads[id]; + function cancelLayoutTick(itemId) { + ticker.remove(LANE_READ, LAYOUT_READ + itemId); + ticker.remove(LANE_WRITE, LAYOUT_WRITE + itemId); + } - batchWrites[id] = writes[id]; - delete writes[id]; - } + function addVisibilityTick(itemId, read, write) { + ticker.add(LANE_READ, VISIBILITY_READ + itemId, read); + ticker.add(LANE_WRITE, VISIBILITY_WRITE + itemId, write); + } - // Reset queue. - queue.length = 0; + function cancelVisibilityTick(itemId) { + ticker.remove(LANE_READ, VISIBILITY_READ + itemId); + ticker.remove(LANE_WRITE, VISIBILITY_WRITE + itemId); + } - // Process read callbacks. - for (i = 0; i < length; i++) { - id = batch[i]; - if (batchReads[id]) { - batchReads[id](); - delete batchReads[id]; - } - } + function addDragStartTick(itemId, read, write) { + ticker.add(LANE_READ, DRAG_START_READ + itemId, read); + ticker.add(LANE_WRITE, DRAG_START_WRITE + itemId, write); + } - // Process write callbacks. - for (i = 0; i < length; i++) { - id = batch[i]; - if (batchWrites[id]) { - batchWrites[id](); - delete batchWrites[id]; - } - } + function cancelDragStartTick(itemId) { + ticker.remove(LANE_READ, DRAG_START_READ + itemId); + ticker.remove(LANE_WRITE, DRAG_START_WRITE + itemId); + } - // Reset batch. - batch.length = 0; + function addDragMoveTick(itemId, read, write) { + ticker.add(LANE_READ, DRAG_MOVE_READ + itemId, read); + ticker.add(LANE_WRITE, DRAG_MOVE_WRITE + itemId, write); + } - // Restart the ticker if needed. - if (!this._nextStep && queue.length) { - this._nextStep = raf(this._step); - } - }; + function cancelDragMoveTick(itemId) { + ticker.remove(LANE_READ, DRAG_MOVE_READ + itemId); + ticker.remove(LANE_WRITE, DRAG_MOVE_WRITE + itemId); + } - var ticker = new Ticker(); + function addDragScrollTick(itemId, read, write) { + ticker.add(LANE_READ, DRAG_SCROLL_READ + itemId, read); + ticker.add(LANE_WRITE, DRAG_SCROLL_WRITE + itemId, write); + } - var layoutTick = 'layout'; - var visibilityTick = 'visibility'; - var moveTick = 'move'; - var scrollTick = 'scroll'; - var placeholderTick = 'placeholder'; + function cancelDragScrollTick(itemId) { + ticker.remove(LANE_READ, DRAG_SCROLL_READ + itemId); + ticker.remove(LANE_WRITE, DRAG_SCROLL_WRITE + itemId); + } - function addLayoutTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + layoutTick, readCallback, writeCallback); + function addDragSortTick(itemId, read) { + ticker.add(LANE_READ_TAIL, DRAG_SORT_READ + itemId, read); } - function cancelLayoutTick(itemId) { - return ticker.cancel(itemId + layoutTick); + function cancelDragSortTick(itemId) { + ticker.remove(LANE_READ_TAIL, DRAG_SORT_READ + itemId); } - function addVisibilityTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + visibilityTick, readCallback, writeCallback); + function addPlaceholderLayoutTick(itemId, read, write) { + ticker.add(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId, read); + ticker.add(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId, write); } - function cancelVisibilityTick(itemId) { - return ticker.cancel(itemId + visibilityTick); + function cancelPlaceholderLayoutTick(itemId) { + ticker.remove(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId); + ticker.remove(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId); } - function addMoveTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + moveTick, readCallback, writeCallback, true); + function addPlaceholderResizeTick(itemId, write) { + ticker.add(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId, write); } - function cancelMoveTick(itemId) { - return ticker.cancel(itemId + moveTick); + function cancelPlaceholderResizeTick(itemId) { + ticker.remove(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId); } - function addScrollTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + scrollTick, readCallback, writeCallback, true); + function addAutoScrollTick(read, write) { + ticker.add(LANE_READ, AUTO_SCROLL_READ, read); + ticker.add(LANE_WRITE, AUTO_SCROLL_WRITE, write); } - function cancelScrollTick(itemId) { - return ticker.cancel(itemId + scrollTick); + function cancelAutoScrollTick() { + ticker.remove(LANE_READ, AUTO_SCROLL_READ); + ticker.remove(LANE_WRITE, AUTO_SCROLL_WRITE); } - function addPlaceholderTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + placeholderTick, readCallback, writeCallback); + function addDebounceTick(debounceId, read) { + ticker.add(LANE_READ, DEBOUNCE_READ + debounceId, read); } - function cancelPlaceholderTick(itemId) { - return ticker.cancel(itemId + placeholderTick); + function cancelDebounceTick(debounceId) { + ticker.remove(LANE_READ, DEBOUNCE_READ + debounceId); } - var ElProto = window.Element.prototype; - var matchesFn = - ElProto.matches || - ElProto.matchesSelector || - ElProto.webkitMatchesSelector || - ElProto.mozMatchesSelector || - ElProto.msMatchesSelector || - ElProto.oMatchesSelector || - function() { - return false; - }; + var AXIS_X = 1; + var AXIS_Y = 2; + var FORWARD = 4; + var BACKWARD = 8; + var LEFT = AXIS_X | BACKWARD; + var RIGHT = AXIS_X | FORWARD; + var UP = AXIS_Y | BACKWARD; + var DOWN = AXIS_Y | FORWARD; + + var functionType = 'function'; /** - * Check if element matches a CSS selector. + * Check if a value is a function. * - * @param {Element} el - * @param {String} selector + * @param {*} val * @returns {Boolean} */ - function elementMatches(el, selector) { - return matchesFn.call(el, selector); + function isFunction(val) { + return typeof val === functionType; } + var isWeakMapSupported = typeof WeakMap === 'function'; + var cache$1 = isWeakMapSupported ? new WeakMap() : null; + var cacheInterval = 3000; + var cacheTimer; + var canClearCache = true; + var clearCache = function () { + if (canClearCache) { + cacheTimer = window.clearInterval(cacheTimer); + cache$1 = isWeakMapSupported ? new WeakMap() : null; + } else { + canClearCache = true; + } + }; + /** - * Add class to an element. + * Returns the computed value of an element's style property as a string. * * @param {HTMLElement} element - * @param {String} className + * @param {String} style + * @returns {String} */ - function addClass(element, className) { - if (element.classList) { - element.classList.add(className); - } else { - if (!elementMatches(element, '.' + className)) { - element.className += ' ' + className; + function getStyle(element, style) { + var styles = cache$1 && cache$1.get(element); + + if (!styles) { + styles = window.getComputedStyle(element, null); + if (cache$1) cache$1.set(element, styles); + } + + if (cache$1) { + if (!cacheTimer) { + cacheTimer = window.setInterval(clearCache, cacheInterval); + } else { + canClearCache = false; } } - } - var tempArray = []; - var numberType = 'number'; + return styles.getPropertyValue(style); + } /** - * Insert an item or an array of items to array to a specified index. Mutates - * the array. The index can be negative in which case the items will be added - * to the end of the array. + * Returns the computed value of an element's style property transformed into + * a float value. * - * @param {Array} array - * @param {*} items - * @param {Number} [index=-1] + * @param {HTMLElement} el + * @param {String} style + * @returns {Number} */ - function arrayInsert(array, items, index) { - var startIndex = typeof index === numberType ? index : -1; - if (startIndex < 0) startIndex = array.length - startIndex + 1; - - array.splice.apply(array, tempArray.concat(startIndex, 0, items)); - tempArray.length = 0; + function getStyleAsFloat(el, style) { + return parseFloat(getStyle(el, style)) || 0; } + var DOC_ELEM = document.documentElement; + var BODY = document.body; + var THRESHOLD_DATA = { value: 0, offset: 0 }; + /** - * Normalize array index. Basically this function makes sure that the provided - * array index is within the bounds of the provided array and also transforms - * negative index to the matching positive index. - * - * @param {Array} array - * @param {Number} index - * @param {Boolean} isMigration + * @param {HTMLElement|Window} element + * @returns {HTMLElement|Window} */ - function normalizeArrayIndex(array, index, isMigration) { - var length = array.length; - var maxIndex = Math.max(0, isMigration ? length : length - 1); - return index > maxIndex ? maxIndex : index < 0 ? Math.max(maxIndex + index + 1, 0) : index; + function getScrollElement(element) { + if (element === window || element === DOC_ELEM || element === BODY) { + return window; + } else { + return element; + } } /** - * Move array item to another index. - * - * @param {Array} array - * @param {Number} fromIndex - * - Index (positive or negative) of the item that will be moved. - * @param {Number} toIndex - * - Index (positive or negative) where the item should be moved to. + * @param {HTMLElement|Window} element + * @returns {Number} */ - function arrayMove(array, fromIndex, toIndex) { - // Make sure the array has two or more items. - if (array.length < 2) return; - - // Normalize the indices. - var from = normalizeArrayIndex(array, fromIndex); - var to = normalizeArrayIndex(array, toIndex); - - // Add target item to the new position. - if (from !== to) { - array.splice(to, 0, array.splice(from, 1)[0]); - } + function getScrollLeft(element) { + return element === window ? element.pageXOffset : element.scrollLeft; } /** - * Swap array items. - * - * @param {Array} array - * @param {Number} index - * - Index (positive or negative) of the item that will be swapped. - * @param {Number} withIndex - * - Index (positive or negative) of the other item that will be swapped. + * @param {HTMLElement|Window} element + * @returns {Number} */ - function arraySwap(array, index, withIndex) { - // Make sure the array has two or more items. - if (array.length < 2) return; - - // Normalize the indices. - var indexA = normalizeArrayIndex(array, index); - var indexB = normalizeArrayIndex(array, withIndex); - var temp; + function getScrollTop(element) { + return element === window ? element.pageYOffset : element.scrollTop; + } - // Swap the items. - if (indexA !== indexB) { - temp = array[indexA]; - array[indexA] = array[indexB]; - array[indexB] = temp; + /** + * @param {HTMLElement|Window} element + * @returns {Number} + */ + function getScrollLeftMax(element) { + if (element === window) { + return DOC_ELEM.scrollWidth - DOC_ELEM.clientWidth; + } else { + return element.scrollWidth - element.clientWidth; } } - var actionCancel = 'cancel'; - var actionFinish = 'finish'; - var debounceTick = 'debounce'; - var debounceId = 0; - /** - * Returns a function, that, as long as it continues to be invoked, will not - * be triggered. The function will be called after it stops being called for - * N milliseconds. The returned function accepts one argument which, when - * being "finish", calls the debounce function immediately if it is currently - * waiting to be called, and when being "cancel" cancels the currently queued - * function call. - * - * @param {Function} fn - * @param {Number} wait - * @returns {Function} + * @param {HTMLElement|Window} element + * @returns {Number} */ - function debounce(fn, wait) { - var timeout; - var tickerId = ++debounceId + debounceTick; - - if (wait > 0) { - return function(action) { - if (timeout !== undefined) { - timeout = window.clearTimeout(timeout); - ticker.cancel(tickerId); - if (action === actionFinish) fn(); - } - - if (action !== actionCancel && action !== actionFinish) { - timeout = window.setTimeout(function() { - timeout = undefined; - ticker.add(tickerId, fn, null, true); - }, wait); - } - }; + function getScrollTopMax(element) { + if (element === window) { + return DOC_ELEM.scrollHeight - DOC_ELEM.clientHeight; + } else { + return element.scrollHeight - element.clientHeight; } - - return function(action) { - if (action !== actionCancel) fn(); - }; } /** - * Returns true if element is transformed, false if not. In practice the - * element's display value must be anything else than "none" or "inline" as - * well as have a valid transform value applied in order to be counted as a - * transformed element. + * Get window's or element's client rectangle data relative to the element's + * content dimensions (includes inner size + padding, excludes scrollbars, + * borders and margins). * - * Borrowed from Mezr (v0.6.1): - * https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L661 - * - * @param {HTMLElement} element - * @returns {Boolean} + * @param {HTMLElement|Window} element + * @returns {Rectangle} */ - function isTransformed(element) { - var transform = getStyle(element, 'transform'); - if (!transform || transform === 'none') return false; + function getContentRect(element, result) { + result = result || {}; - var display = getStyle(element, 'display'); - if (display === 'inline' || display === 'none') return false; - - return true; + if (element === window) { + result.width = DOC_ELEM.clientWidth; + result.height = DOC_ELEM.clientHeight; + result.left = 0; + result.right = result.width; + result.top = 0; + result.bottom = result.height; + } else { + var bcr = element.getBoundingClientRect(); + var borderLeft = element.clientLeft || getStyleAsFloat(element, 'border-left-width'); + var borderTop = element.clientTop || getStyleAsFloat(element, 'border-top-width'); + result.width = element.clientWidth; + result.height = element.clientHeight; + result.left = bcr.left + borderLeft; + result.right = result.left + result.width; + result.top = bcr.top + borderTop; + result.bottom = result.top + result.height; + } + + return result; } /** - * Returns an absolute positioned element's containing block, which is - * considered to be the closest ancestor element that the target element's - * positioning is relative to. Disclaimer: this only works as intended for + * @param {Item} item + * @returns {Object} + */ + function getItemAutoScrollSettings(item) { + return item._drag._getGrid()._settings.dragAutoScroll; + } + + /** + * @param {Item} item + */ + function prepareItemScrollSync(item) { + if (!item._drag) return; + item._drag._prepareScroll(); + } + + /** + * @param {Item} item + */ + function applyItemScrollSync(item) { + if (!item._drag || !item._isActive) return; + var drag = item._drag; + drag._scrollDiffX = drag._scrollDiffY = 0; + item._setTranslate(drag._left, drag._top); + } + + /** + * Compute threshold value and edge offset. + * + * @param {Number} threshold + * @param {Number} safeZone + * @param {Number} itemSize + * @param {Number} targetSize + * @returns {Object} + */ + function computeThreshold(threshold, safeZone, itemSize, targetSize) { + THRESHOLD_DATA.value = Math.min(targetSize / 2, threshold); + THRESHOLD_DATA.offset = + Math.max(0, itemSize + THRESHOLD_DATA.value * 2 + targetSize * safeZone - targetSize) / 2; + return THRESHOLD_DATA; + } + + function ScrollRequest() { + this.reset(); + } + + ScrollRequest.prototype.reset = function () { + if (this.isActive) this.onStop(); + this.item = null; + this.element = null; + this.isActive = false; + this.isEnding = false; + this.direction = null; + this.value = null; + this.maxValue = 0; + this.threshold = 0; + this.distance = 0; + this.speed = 0; + this.duration = 0; + this.action = null; + }; + + ScrollRequest.prototype.hasReachedEnd = function () { + return FORWARD & this.direction ? this.value >= this.maxValue : this.value <= 0; + }; + + ScrollRequest.prototype.computeCurrentScrollValue = function () { + if (this.value === null) { + return AXIS_X & this.direction ? getScrollLeft(this.element) : getScrollTop(this.element); + } + return Math.max(0, Math.min(this.value, this.maxValue)); + }; + + ScrollRequest.prototype.computeNextScrollValue = function (deltaTime) { + var delta = this.speed * (deltaTime / 1000); + var nextValue = FORWARD & this.direction ? this.value + delta : this.value - delta; + return Math.max(0, Math.min(nextValue, this.maxValue)); + }; + + ScrollRequest.prototype.computeSpeed = (function () { + var data = { + direction: null, + threshold: 0, + distance: 0, + value: 0, + maxValue: 0, + deltaTime: 0, + duration: 0, + isEnding: false, + }; + + return function (deltaTime) { + var item = this.item; + var speed = getItemAutoScrollSettings(item).speed; + + if (isFunction(speed)) { + data.direction = this.direction; + data.threshold = this.threshold; + data.distance = this.distance; + data.value = this.value; + data.maxValue = this.maxValue; + data.duration = this.duration; + data.speed = this.speed; + data.deltaTime = deltaTime; + data.isEnding = this.isEnding; + return speed(item, this.element, data); + } else { + return speed; + } + }; + })(); + + ScrollRequest.prototype.tick = function (deltaTime) { + if (!this.isActive) { + this.isActive = true; + this.onStart(); + } + this.value = this.computeCurrentScrollValue(); + this.speed = this.computeSpeed(deltaTime); + this.value = this.computeNextScrollValue(deltaTime); + this.duration += deltaTime; + return this.value; + }; + + ScrollRequest.prototype.onStart = function () { + var item = this.item; + var onStart = getItemAutoScrollSettings(item).onStart; + if (isFunction(onStart)) onStart(item, this.element, this.direction); + }; + + ScrollRequest.prototype.onStop = function () { + var item = this.item; + var onStop = getItemAutoScrollSettings(item).onStop; + if (isFunction(onStop)) onStop(item, this.element, this.direction); + // Manually nudge sort to happen. There's a good chance that the item is still + // after the scroll stops which means that the next sort will be triggered + // only after the item is moved or it's parent scrolled. + if (item._drag) item._drag.sort(); + }; + + function ScrollAction() { + this.element = null; + this.requestX = null; + this.requestY = null; + this.scrollLeft = 0; + this.scrollTop = 0; + } + + ScrollAction.prototype.reset = function () { + if (this.requestX) this.requestX.action = null; + if (this.requestY) this.requestY.action = null; + this.element = null; + this.requestX = null; + this.requestY = null; + this.scrollLeft = 0; + this.scrollTop = 0; + }; + + ScrollAction.prototype.addRequest = function (request) { + if (AXIS_X & request.direction) { + this.removeRequest(this.requestX); + this.requestX = request; + } else { + this.removeRequest(this.requestY); + this.requestY = request; + } + request.action = this; + }; + + ScrollAction.prototype.removeRequest = function (request) { + if (!request) return; + if (this.requestX === request) { + this.requestX = null; + request.action = null; + } else if (this.requestY === request) { + this.requestY = null; + request.action = null; + } + }; + + ScrollAction.prototype.computeScrollValues = function () { + this.scrollLeft = this.requestX ? this.requestX.value : getScrollLeft(this.element); + this.scrollTop = this.requestY ? this.requestY.value : getScrollTop(this.element); + }; + + ScrollAction.prototype.scroll = function () { + var element = this.element; + if (!element) return; + + if (element.scrollTo) { + element.scrollTo(this.scrollLeft, this.scrollTop); + } else { + element.scrollLeft = this.scrollLeft; + element.scrollTop = this.scrollTop; + } + }; + + function Pool(createItem, releaseItem) { + this.pool = []; + this.createItem = createItem; + this.releaseItem = releaseItem; + } + + Pool.prototype.pick = function () { + return this.pool.pop() || this.createItem(); + }; + + Pool.prototype.release = function (item) { + this.releaseItem(item); + if (this.pool.indexOf(item) !== -1) return; + this.pool.push(item); + }; + + Pool.prototype.reset = function () { + this.pool.length = 0; + }; + + /** + * Check if two rectangles are overlapping. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ + function isOverlapping(a, b) { + return !( + a.left + a.width <= b.left || + b.left + b.width <= a.left || + a.top + a.height <= b.top || + b.top + b.height <= a.top + ); + } + + /** + * Calculate intersection area between two rectangle. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ + function getIntersectionArea(a, b) { + if (!isOverlapping(a, b)) return 0; + var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left); + var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top); + return width * height; + } + + /** + * Calculate how many percent the intersection area of two rectangles is from + * the maximum potential intersection area between the rectangles. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ + function getIntersectionScore(a, b) { + var area = getIntersectionArea(a, b); + if (!area) return 0; + var maxArea = Math.min(a.width, b.width) * Math.min(a.height, b.height); + return (area / maxArea) * 100; + } + + var RECT_1 = { + width: 0, + height: 0, + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + + var RECT_2 = { + width: 0, + height: 0, + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + + function AutoScroller() { + this._isDestroyed = false; + this._isTicking = false; + this._tickTime = 0; + this._tickDeltaTime = 0; + this._items = []; + this._actions = []; + this._requests = {}; + this._requests[AXIS_X] = {}; + this._requests[AXIS_Y] = {}; + this._requestOverlapCheck = {}; + this._dragPositions = {}; + this._dragDirections = {}; + this._overlapCheckInterval = 150; + + this._requestPool = new Pool( + function () { + return new ScrollRequest(); + }, + function (request) { + request.reset(); + } + ); + + this._actionPool = new Pool( + function () { + return new ScrollAction(); + }, + function (action) { + action.reset(); + } + ); + + this._readTick = this._readTick.bind(this); + this._writeTick = this._writeTick.bind(this); + } + + AutoScroller.AXIS_X = AXIS_X; + AutoScroller.AXIS_Y = AXIS_Y; + AutoScroller.FORWARD = FORWARD; + AutoScroller.BACKWARD = BACKWARD; + AutoScroller.LEFT = LEFT; + AutoScroller.RIGHT = RIGHT; + AutoScroller.UP = UP; + AutoScroller.DOWN = DOWN; + + AutoScroller.smoothSpeed = function (maxSpeed, acceleration, deceleration) { + return function (item, element, data) { + var targetSpeed = 0; + if (!data.isEnding) { + if (data.threshold > 0) { + var factor = data.threshold - Math.max(0, data.distance); + targetSpeed = (maxSpeed / data.threshold) * factor; + } else { + targetSpeed = maxSpeed; + } + } + + var currentSpeed = data.speed; + var nextSpeed = targetSpeed; + + if (currentSpeed === targetSpeed) { + return nextSpeed; + } + + if (currentSpeed < targetSpeed) { + nextSpeed = currentSpeed + acceleration * (data.deltaTime / 1000); + return Math.min(targetSpeed, nextSpeed); + } else { + nextSpeed = currentSpeed - deceleration * (data.deltaTime / 1000); + return Math.max(targetSpeed, nextSpeed); + } + }; + }; + + AutoScroller.pointerHandle = function (pointerSize) { + var rect = { left: 0, top: 0, width: 0, height: 0 }; + var size = pointerSize || 1; + return function (item, x, y, w, h, pX, pY) { + rect.left = pX - size * 0.5; + rect.top = pY - size * 0.5; + rect.width = size; + rect.height = size; + return rect; + }; + }; + + AutoScroller.prototype._readTick = function (time) { + if (this._isDestroyed) return; + if (time && this._tickTime) { + this._tickDeltaTime = time - this._tickTime; + this._tickTime = time; + this._updateRequests(); + this._updateActions(); + } else { + this._tickTime = time; + this._tickDeltaTime = 0; + } + }; + + AutoScroller.prototype._writeTick = function () { + if (this._isDestroyed) return; + this._applyActions(); + addAutoScrollTick(this._readTick, this._writeTick); + }; + + AutoScroller.prototype._startTicking = function () { + this._isTicking = true; + addAutoScrollTick(this._readTick, this._writeTick); + }; + + AutoScroller.prototype._stopTicking = function () { + this._isTicking = false; + this._tickTime = 0; + this._tickDeltaTime = 0; + cancelAutoScrollTick(); + }; + + AutoScroller.prototype._getItemHandleRect = function (item, handle, rect) { + var itemDrag = item._drag; + + if (handle) { + var ev = itemDrag._dragMoveEvent || itemDrag._dragStartEvent; + var data = handle( + item, + itemDrag._clientX, + itemDrag._clientY, + item._width, + item._height, + ev.clientX, + ev.clientY + ); + rect.left = data.left; + rect.top = data.top; + rect.width = data.width; + rect.height = data.height; + } else { + rect.left = itemDrag._clientX; + rect.top = itemDrag._clientY; + rect.width = item._width; + rect.height = item._height; + } + + rect.right = rect.left + rect.width; + rect.bottom = rect.top + rect.height; + + return rect; + }; + + AutoScroller.prototype._requestItemScroll = function ( + item, + axis, + element, + direction, + threshold, + distance, + maxValue + ) { + var reqMap = this._requests[axis]; + var request = reqMap[item._id]; + + if (request) { + if (request.element !== element || request.direction !== direction) { + request.reset(); + } + } else { + request = this._requestPool.pick(); + } + + request.item = item; + request.element = element; + request.direction = direction; + request.threshold = threshold; + request.distance = distance; + request.maxValue = maxValue; + reqMap[item._id] = request; + }; + + AutoScroller.prototype._cancelItemScroll = function (item, axis) { + var reqMap = this._requests[axis]; + var request = reqMap[item._id]; + if (!request) return; + if (request.action) request.action.removeRequest(request); + this._requestPool.release(request); + delete reqMap[item._id]; + }; + + AutoScroller.prototype._checkItemOverlap = function (item, checkX, checkY) { + var settings = getItemAutoScrollSettings(item); + var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets; + var threshold = settings.threshold; + var safeZone = settings.safeZone; + + if (!targets || !targets.length) { + checkX && this._cancelItemScroll(item, AXIS_X); + checkY && this._cancelItemScroll(item, AXIS_Y); + return; + } + + var dragDirections = this._dragDirections[item._id]; + var dragDirectionX = dragDirections[0]; + var dragDirectionY = dragDirections[1]; + + if (!dragDirectionX && !dragDirectionY) { + checkX && this._cancelItemScroll(item, AXIS_X); + checkY && this._cancelItemScroll(item, AXIS_Y); + return; + } + + var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1); + var testRect = RECT_2; + + var target = null; + var testElement = null; + var testAxisX = true; + var testAxisY = true; + var testScore = 0; + var testPriority = 0; + var testThreshold = null; + var testDirection = null; + var testDistance = 0; + var testMaxScrollX = 0; + var testMaxScrollY = 0; + + var xElement = null; + var xPriority = -Infinity; + var xThreshold = 0; + var xScore = 0; + var xDirection = null; + var xDistance = 0; + var xMaxScroll = 0; + + var yElement = null; + var yPriority = -Infinity; + var yThreshold = 0; + var yScore = 0; + var yDirection = null; + var yDistance = 0; + var yMaxScroll = 0; + + for (var i = 0; i < targets.length; i++) { + target = targets[i]; + testAxisX = checkX && dragDirectionX && target.axis !== AXIS_Y; + testAxisY = checkY && dragDirectionY && target.axis !== AXIS_X; + testPriority = target.priority || 0; + + // Ignore this item if it's x-axis and y-axis priority is lower than + // the currently matching item's. + if ((!testAxisX || testPriority < xPriority) && (!testAxisY || testPriority < yPriority)) { + continue; + } + + testElement = getScrollElement(target.element || target); + testMaxScrollX = testAxisX ? getScrollLeftMax(testElement) : -1; + testMaxScrollY = testAxisY ? getScrollTopMax(testElement) : -1; + + // Ignore this item if there is no possibility to scroll. + if (!testMaxScrollX && !testMaxScrollY) continue; + + testRect = getContentRect(testElement, testRect); + testScore = getIntersectionScore(itemRect, testRect); + + // Ignore this item if it's not overlapping at all with the dragged item. + if (testScore <= 0) continue; + + // Test x-axis. + if ( + testAxisX && + testPriority >= xPriority && + testMaxScrollX > 0 && + (testPriority > xPriority || testScore > xScore) + ) { + testDirection = null; + testThreshold = computeThreshold( + typeof target.threshold === 'number' ? target.threshold : threshold, + safeZone, + itemRect.width, + testRect.width + ); + if (dragDirectionX === RIGHT) { + testDistance = testRect.right + testThreshold.offset - itemRect.right; + if (testDistance <= testThreshold.value && getScrollLeft(testElement) < testMaxScrollX) { + testDirection = RIGHT; + } + } else if (dragDirectionX === LEFT) { + testDistance = itemRect.left - (testRect.left - testThreshold.offset); + if (testDistance <= testThreshold.value && getScrollLeft(testElement) > 0) { + testDirection = LEFT; + } + } + + if (testDirection !== null) { + xElement = testElement; + xPriority = testPriority; + xThreshold = testThreshold.value; + xScore = testScore; + xDirection = testDirection; + xDistance = testDistance; + xMaxScroll = testMaxScrollX; + } + } + + // Test y-axis. + if ( + testAxisY && + testPriority >= yPriority && + testMaxScrollY > 0 && + (testPriority > yPriority || testScore > yScore) + ) { + testDirection = null; + testThreshold = computeThreshold( + typeof target.threshold === 'number' ? target.threshold : threshold, + safeZone, + itemRect.height, + testRect.height + ); + if (dragDirectionY === DOWN) { + testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom; + if (testDistance <= testThreshold.value && getScrollTop(testElement) < testMaxScrollY) { + testDirection = DOWN; + } + } else if (dragDirectionY === UP) { + testDistance = itemRect.top - (testRect.top - testThreshold.offset); + if (testDistance <= testThreshold.value && getScrollTop(testElement) > 0) { + testDirection = UP; + } + } + + if (testDirection !== null) { + yElement = testElement; + yPriority = testPriority; + yThreshold = testThreshold.value; + yScore = testScore; + yDirection = testDirection; + yDistance = testDistance; + yMaxScroll = testMaxScrollY; + } + } + } + + // Request or cancel x-axis scroll. + if (checkX) { + if (xElement) { + this._requestItemScroll( + item, + AXIS_X, + xElement, + xDirection, + xThreshold, + xDistance, + xMaxScroll + ); + } else { + this._cancelItemScroll(item, AXIS_X); + } + } + + // Request or cancel y-axis scroll. + if (checkY) { + if (yElement) { + this._requestItemScroll( + item, + AXIS_Y, + yElement, + yDirection, + yThreshold, + yDistance, + yMaxScroll + ); + } else { + this._cancelItemScroll(item, AXIS_Y); + } + } + }; + + AutoScroller.prototype._updateScrollRequest = function (scrollRequest) { + var item = scrollRequest.item; + var settings = getItemAutoScrollSettings(item); + var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets; + var targetCount = (targets && targets.length) || 0; + var threshold = settings.threshold; + var safeZone = settings.safeZone; + var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1); + var testRect = RECT_2; + var target = null; + var testElement = null; + var testIsAxisX = false; + var testScore = null; + var testThreshold = null; + var testDistance = null; + var testScroll = null; + var testMaxScroll = null; + var hasReachedEnd = null; + + for (var i = 0; i < targetCount; i++) { + target = targets[i]; + + // Make sure we have a matching element. + testElement = getScrollElement(target.element || target); + if (testElement !== scrollRequest.element) continue; + + // Make sure we have a matching axis. + testIsAxisX = !!(AXIS_X & scrollRequest.direction); + if (testIsAxisX) { + if (target.axis === AXIS_Y) continue; + } else { + if (target.axis === AXIS_X) continue; + } + + // Stop scrolling if there is no room to scroll anymore. + testMaxScroll = testIsAxisX ? getScrollLeftMax(testElement) : getScrollTopMax(testElement); + if (testMaxScroll <= 0) { + break; + } + + testRect = getContentRect(testElement, testRect); + testScore = getIntersectionScore(itemRect, testRect); + + // Stop scrolling if dragged item is not overlapping with the scroll + // element anymore. + if (testScore <= 0) { + break; + } + + // Compute threshold and edge offset. + testThreshold = computeThreshold( + typeof target.threshold === 'number' ? target.threshold : threshold, + safeZone, + testIsAxisX ? itemRect.width : itemRect.height, + testIsAxisX ? testRect.width : testRect.height + ); + + // Compute distance (based on current direction). + if (scrollRequest.direction === LEFT) { + testDistance = itemRect.left - (testRect.left - testThreshold.offset); + } else if (scrollRequest.direction === RIGHT) { + testDistance = testRect.right + testThreshold.offset - itemRect.right; + } else if (scrollRequest.direction === UP) { + testDistance = itemRect.top - (testRect.top - testThreshold.offset); + } else { + testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom; + } + + // Stop scrolling if threshold is not exceeded. + if (testDistance > testThreshold.value) { + break; + } + + // Stop scrolling if we have reached the end of the scroll value. + testScroll = testIsAxisX ? getScrollLeft(testElement) : getScrollTop(testElement); + hasReachedEnd = + FORWARD & scrollRequest.direction ? testScroll >= testMaxScroll : testScroll <= 0; + if (hasReachedEnd) { + break; + } + + // Scrolling can continue, let's update the values. + scrollRequest.maxValue = testMaxScroll; + scrollRequest.threshold = testThreshold.value; + scrollRequest.distance = testDistance; + scrollRequest.isEnding = false; + return true; + } + + // Before we end the request, let's see if we need to stop the scrolling + // smoothly or immediately. + if (settings.smoothStop === true && scrollRequest.speed > 0) { + if (hasReachedEnd === null) hasReachedEnd = scrollRequest.hasReachedEnd(); + scrollRequest.isEnding = hasReachedEnd ? false : true; + } else { + scrollRequest.isEnding = false; + } + + return scrollRequest.isEnding; + }; + + AutoScroller.prototype._updateRequests = function () { + var items = this._items; + var requestsX = this._requests[AXIS_X]; + var requestsY = this._requests[AXIS_Y]; + var item, reqX, reqY, checkTime, needsCheck, checkX, checkY; + + for (var i = 0; i < items.length; i++) { + item = items[i]; + checkTime = this._requestOverlapCheck[item._id]; + needsCheck = checkTime > 0 && this._tickTime - checkTime > this._overlapCheckInterval; + + checkX = true; + reqX = requestsX[item._id]; + if (reqX && reqX.isActive) { + checkX = !this._updateScrollRequest(reqX); + if (checkX) { + needsCheck = true; + this._cancelItemScroll(item, AXIS_X); + } + } + + checkY = true; + reqY = requestsY[item._id]; + if (reqY && reqY.isActive) { + checkY = !this._updateScrollRequest(reqY); + if (checkY) { + needsCheck = true; + this._cancelItemScroll(item, AXIS_Y); + } + } + + if (needsCheck) { + this._requestOverlapCheck[item._id] = 0; + this._checkItemOverlap(item, checkX, checkY); + } + } + }; + + AutoScroller.prototype._requestAction = function (request, axis) { + var actions = this._actions; + var isAxisX = axis === AXIS_X; + var action = null; + + for (var i = 0; i < actions.length; i++) { + action = actions[i]; + + // If the action's request does not match the request's -> skip. + if (request.element !== action.element) { + action = null; + continue; + } + + // If the request and action share the same element, but the request slot + // for the requested axis is already reserved let's ignore and cancel this + // request. + if (isAxisX ? action.requestX : action.requestY) { + this._cancelItemScroll(request.item, axis); + return; + } + + // Seems like we have found our action, let's break the loop. + break; + } + + if (!action) action = this._actionPool.pick(); + action.element = request.element; + action.addRequest(request); + + request.tick(this._tickDeltaTime); + actions.push(action); + }; + + AutoScroller.prototype._updateActions = function () { + var items = this._items; + var requests = this._requests; + var actions = this._actions; + var itemId; + var reqX; + var reqY; + var i; + + // Generate actions. + for (i = 0; i < items.length; i++) { + itemId = items[i]._id; + reqX = requests[AXIS_X][itemId]; + reqY = requests[AXIS_Y][itemId]; + if (reqX) this._requestAction(reqX, AXIS_X); + if (reqY) this._requestAction(reqY, AXIS_Y); + } + + // Compute actions' scroll values. + for (i = 0; i < actions.length; i++) { + actions[i].computeScrollValues(); + } + }; + + AutoScroller.prototype._applyActions = function () { + var actions = this._actions; + var items = this._items; + var i; + + // No actions -> no scrolling. + if (!actions.length) return; + + // Scroll all the required elements. + for (i = 0; i < actions.length; i++) { + actions[i].scroll(); + this._actionPool.release(actions[i]); + } + + // Reset actions. + actions.length = 0; + + // Sync the item position immediately after all the auto-scrolling business is + // finished. Without this procedure the items will jitter during auto-scroll + // (in some cases at least) since the drag scroll handler is async (bound to + // raf tick). Note that this procedure should not emit any dragScroll events, + // because otherwise they would be emitted twice for the same event. + for (i = 0; i < items.length; i++) prepareItemScrollSync(items[i]); + for (i = 0; i < items.length; i++) applyItemScrollSync(items[i]); + }; + + AutoScroller.prototype._updateDragDirection = function (item) { + var dragPositions = this._dragPositions[item._id]; + var dragDirections = this._dragDirections[item._id]; + var x1 = item._drag._left; + var y1 = item._drag._top; + if (dragPositions.length) { + var x2 = dragPositions[0]; + var y2 = dragPositions[1]; + dragDirections[0] = x1 > x2 ? RIGHT : x1 < x2 ? LEFT : dragDirections[0] || 0; + dragDirections[1] = y1 > y2 ? DOWN : y1 < y2 ? UP : dragDirections[1] || 0; + } + dragPositions[0] = x1; + dragPositions[1] = y1; + }; + + AutoScroller.prototype.addItem = function (item) { + if (this._isDestroyed) return; + var index = this._items.indexOf(item); + if (index === -1) { + this._items.push(item); + this._requestOverlapCheck[item._id] = this._tickTime; + this._dragDirections[item._id] = [0, 0]; + this._dragPositions[item._id] = []; + if (!this._isTicking) this._startTicking(); + } + }; + + AutoScroller.prototype.updateItem = function (item) { + if (this._isDestroyed) return; + this._updateDragDirection(item); + if (!this._requestOverlapCheck[item._id]) { + this._requestOverlapCheck[item._id] = this._tickTime; + } + }; + + AutoScroller.prototype.removeItem = function (item) { + if (this._isDestroyed) return; + + var index = this._items.indexOf(item); + if (index === -1) return; + + var itemId = item._id; + + var reqX = this._requests[AXIS_X][itemId]; + if (reqX) { + this._cancelItemScroll(item, AXIS_X); + delete this._requests[AXIS_X][itemId]; + } + + var reqY = this._requests[AXIS_Y][itemId]; + if (reqY) { + this._cancelItemScroll(item, AXIS_Y); + delete this._requests[AXIS_Y][itemId]; + } + + delete this._requestOverlapCheck[itemId]; + delete this._dragPositions[itemId]; + delete this._dragDirections[itemId]; + this._items.splice(index, 1); + + if (this._isTicking && !this._items.length) { + this._stopTicking(); + } + }; + + AutoScroller.prototype.isItemScrollingX = function (item) { + var reqX = this._requests[AXIS_X][item._id]; + return !!(reqX && reqX.isActive); + }; + + AutoScroller.prototype.isItemScrollingY = function (item) { + var reqY = this._requests[AXIS_Y][item._id]; + return !!(reqY && reqY.isActive); + }; + + AutoScroller.prototype.isItemScrolling = function (item) { + return this.isItemScrollingX(item) || this.isItemScrollingY(item); + }; + + AutoScroller.prototype.destroy = function () { + if (this._isDestroyed) return; + + var items = this._items.slice(0); + for (var i = 0; i < items.length; i++) { + this.removeItem(items[i]); + } + + this._actions.length = 0; + this._requestPool.reset(); + this._actionPool.reset(); + + this._isDestroyed = true; + }; + + var ElProto = window.Element.prototype; + var matchesFn = + ElProto.matches || + ElProto.matchesSelector || + ElProto.webkitMatchesSelector || + ElProto.mozMatchesSelector || + ElProto.msMatchesSelector || + ElProto.oMatchesSelector || + function () { + return false; + }; + + /** + * Check if element matches a CSS selector. + * + * @param {Element} el + * @param {String} selector + * @returns {Boolean} + */ + function elementMatches(el, selector) { + return matchesFn.call(el, selector); + } + + /** + * Add class to an element. + * + * @param {HTMLElement} element + * @param {String} className + */ + function addClass(element, className) { + if (!className) return; + + if (element.classList) { + element.classList.add(className); + } else { + if (!elementMatches(element, '.' + className)) { + element.className += ' ' + className; + } + } + } + + var tempArray = []; + var numberType = 'number'; + + /** + * Insert an item or an array of items to array to a specified index. Mutates + * the array. The index can be negative in which case the items will be added + * to the end of the array. + * + * @param {Array} array + * @param {*} items + * @param {Number} [index=-1] + */ + function arrayInsert(array, items, index) { + var startIndex = typeof index === numberType ? index : -1; + if (startIndex < 0) startIndex = array.length - startIndex + 1; + + array.splice.apply(array, tempArray.concat(startIndex, 0, items)); + tempArray.length = 0; + } + + /** + * Normalize array index. Basically this function makes sure that the provided + * array index is within the bounds of the provided array and also transforms + * negative index to the matching positive index. The third (optional) argument + * allows you to define offset for array's length in case you are adding items + * to the array or removing items from the array. + * + * @param {Array} array + * @param {Number} index + * @param {Number} [sizeOffset] + */ + function normalizeArrayIndex(array, index, sizeOffset) { + var maxIndex = Math.max(0, array.length - 1 + (sizeOffset || 0)); + return index > maxIndex ? maxIndex : index < 0 ? Math.max(maxIndex + index + 1, 0) : index; + } + + /** + * Move array item to another index. + * + * @param {Array} array + * @param {Number} fromIndex + * - Index (positive or negative) of the item that will be moved. + * @param {Number} toIndex + * - Index (positive or negative) where the item should be moved to. + */ + function arrayMove(array, fromIndex, toIndex) { + // Make sure the array has two or more items. + if (array.length < 2) return; + + // Normalize the indices. + var from = normalizeArrayIndex(array, fromIndex); + var to = normalizeArrayIndex(array, toIndex); + + // Add target item to the new position. + if (from !== to) { + array.splice(to, 0, array.splice(from, 1)[0]); + } + } + + /** + * Swap array items. + * + * @param {Array} array + * @param {Number} index + * - Index (positive or negative) of the item that will be swapped. + * @param {Number} withIndex + * - Index (positive or negative) of the other item that will be swapped. + */ + function arraySwap(array, index, withIndex) { + // Make sure the array has two or more items. + if (array.length < 2) return; + + // Normalize the indices. + var indexA = normalizeArrayIndex(array, index); + var indexB = normalizeArrayIndex(array, withIndex); + var temp; + + // Swap the items. + if (indexA !== indexB) { + temp = array[indexA]; + array[indexA] = array[indexB]; + array[indexB] = temp; + } + } + + var transformProp = getPrefixedPropName(document.documentElement.style, 'transform') || 'transform'; + + var styleNameRegEx = /([A-Z])/g; + var prefixRegex = /^(webkit-|moz-|ms-|o-)/; + var msPrefixRegex = /^(-m-s-)/; + + /** + * Transforms a camel case style property to kebab case style property. Handles + * vendor prefixed properties elegantly as well, e.g. "WebkitTransform" and + * "webkitTransform" are both transformed into "-webkit-transform". + * + * @param {String} property + * @returns {String} + */ + function getStyleName(property) { + // Initial slicing, turns "fooBarProp" into "foo-bar-prop". + var styleName = property.replace(styleNameRegEx, '-$1').toLowerCase(); + + // Handle properties that start with "webkit", "moz", "ms" or "o" prefix (we + // need to add an extra '-' to the beginnig). + styleName = styleName.replace(prefixRegex, '-$1'); + + // Handle properties that start with "MS" prefix (we need to transform the + // "-m-s-" into "-ms-"). + styleName = styleName.replace(msPrefixRegex, '-ms-'); + + return styleName; + } + + var transformStyle = getStyleName(transformProp); + + var transformNone = 'none'; + var displayInline = 'inline'; + var displayNone = 'none'; + var displayStyle = 'display'; + + /** + * Returns true if element is transformed, false if not. In practice the + * element's display value must be anything else than "none" or "inline" as + * well as have a valid transform value applied in order to be counted as a + * transformed element. + * + * Borrowed from Mezr (v0.6.1): + * https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L661 + * + * @param {HTMLElement} element + * @returns {Boolean} + */ + function isTransformed(element) { + var transform = getStyle(element, transformStyle); + if (!transform || transform === transformNone) return false; + + var display = getStyle(element, displayStyle); + if (display === displayInline || display === displayNone) return false; + + return true; + } + + /** + * Returns an absolute positioned element's containing block, which is + * considered to be the closest ancestor element that the target element's + * positioning is relative to. Disclaimer: this only works as intended for * absolute positioned elements. * * @param {HTMLElement} element - * @param {Boolean} [includeSelf=false] - * - When this is set to true the containing block checking is started from - * the provided element. Otherwise the checking is started from the - * provided element's parent element. * @returns {(Document|Element)} */ - function getContainingBlock(element, includeSelf) { + function getContainingBlock(element) { // As long as the containing block is an element, static and not // transformed, try to get the element's parent element and fallback to // document. https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L339 - var document = window.document; - var ret = (includeSelf ? element : element.parentElement) || document; - while (ret && ret !== document && getStyle(ret, 'position') === 'static' && !isTransformed(ret)) { - ret = ret.parentElement || document; + var doc = document; + var res = element || doc; + while (res && res !== doc && getStyle(res, 'position') === 'static' && !isTransformed(res)) { + res = res.parentElement || doc; } - return ret; - } - - /** - * Returns the computed value of an element's style property transformed into - * a float value. - * - * @param {HTMLElement} el - * @param {String} style - * @returns {Number} - */ - function getStyleAsFloat(el, style) { - return parseFloat(getStyle(el, style)) || 0; + return res; } var offsetA = {}; @@ -1564,33 +2555,33 @@ * @returns {Object} */ function getOffset(element, offsetData) { - var ret = offsetData || {}; + var offset = offsetData || {}; var rect; // Set up return data. - ret.left = 0; - ret.top = 0; + offset.left = 0; + offset.top = 0; // Document's offsets are always 0. - if (element === document) return ret; + if (element === document) return offset; // Add viewport scroll left/top to the respective offsets. - ret.left = window.pageXOffset || 0; - ret.top = window.pageYOffset || 0; + offset.left = window.pageXOffset || 0; + offset.top = window.pageYOffset || 0; // Window's offsets are the viewport scroll left/top values. - if (element.self === window.self) return ret; + if (element.self === window.self) return offset; // Add element's client rects to the offsets. rect = element.getBoundingClientRect(); - ret.left += rect.left; - ret.top += rect.top; + offset.left += rect.left; + offset.top += rect.top; // Exclude element's borders from the offset. - ret.left += getStyleAsFloat(element, 'border-left-width'); - ret.top += getStyleAsFloat(element, 'border-top-width'); + offset.left += getStyleAsFloat(element, 'border-left-width'); + offset.top += getStyleAsFloat(element, 'border-top-width'); - return ret; + return offset; } /** @@ -1613,8 +2604,8 @@ // Compare containing blocks if necessary. if (compareContainingBlocks) { - elemA = getContainingBlock(elemA, true); - elemB = getContainingBlock(elemB, true); + elemA = getContainingBlock(elemA); + elemB = getContainingBlock(elemB); // If containing blocks are identical, let's return early. if (elemA === elemB) return offsetDiff; @@ -1629,11 +2620,15 @@ return offsetDiff; } - var styleOverflow = 'overflow'; - var styleOverflowX = 'overflow-x'; - var styleOverflowY = 'overflow-y'; - var overflowAuto = 'auto'; - var overflowScroll = 'scroll'; + /** + * Check if overflow style value is scrollable. + * + * @param {String} value + * @returns {Boolean} + */ + function isScrollableOverflow(value) { + return value === 'auto' || value === 'scroll' || value === 'overlay'; + } /** * Check if an element is scrollable. @@ -1642,56 +2637,50 @@ * @returns {Boolean} */ function isScrollable(element) { - var overflow = getStyle(element, styleOverflow); - if (overflow === overflowAuto || overflow === overflowScroll) return true; - - overflow = getStyle(element, styleOverflowX); - if (overflow === overflowAuto || overflow === overflowScroll) return true; - - overflow = getStyle(element, styleOverflowY); - if (overflow === overflowAuto || overflow === overflowScroll) return true; - - return false; + return ( + isScrollableOverflow(getStyle(element, 'overflow')) || + isScrollableOverflow(getStyle(element, 'overflow-x')) || + isScrollableOverflow(getStyle(element, 'overflow-y')) + ); } /** - * Collect element's ancestors that are potentially scrollable elements. + * Collect element's ancestors that are potentially scrollable elements. The + * provided element is also also included in the check, meaning that if it is + * scrollable it is added to the result array. * * @param {HTMLElement} element - * @param {Boolean} [includeSelf=false] - * @param {Array} [data] + * @param {Array} [result] * @returns {Array} */ - function getScrollableAncestors(element, includeSelf, data) { - var ret = data || []; - var parent = includeSelf ? element : element.parentNode; + function getScrollableAncestors(element, result) { + result = result || []; // Find scroll parents. - while (parent && parent !== document) { + while (element && element !== document) { // If element is inside ShadowDOM let's get it's host node from the real // DOM and continue looping. - if (parent.getRootNode && parent instanceof DocumentFragment) { - parent = parent.getRootNode().host; + if (element.getRootNode && element instanceof DocumentFragment) { + element = element.getRootNode().host; continue; } // If element is scrollable let's add it to the scrollable list. - if (isScrollable(parent)) { - ret.push(parent); + if (isScrollable(element)) { + result.push(element); } - parent = parent.parentNode; + element = element.parentNode; } // Always add window to the results. - ret.push(window); + result.push(window); - return ret; + return result; } var translateValue = {}; - var transformStyle$2 = 'transform'; - var transformNone = 'none'; + var transformNone$1 = 'none'; var rxMat3d = /^matrix3d/; var rxMatTx = /([^,]*,){4}/; var rxMat3dTx = /([^,]*,){12}/; @@ -1709,8 +2698,8 @@ translateValue.x = 0; translateValue.y = 0; - var transform = getStyle(element, transformStyle$2); - if (!transform || transform === transformNone) { + var transform = getStyle(element, transformStyle); + if (!transform || transform === transformNone$1) { return translateValue; } @@ -1725,18 +2714,6 @@ return translateValue; } - /** - * Transform translateX and translateY value into CSS transform style - * property's value. - * - * @param {Number} x - * @param {Number} y - * @returns {String} - */ - function getTranslateString(x, y) { - return 'translateX(' + x + 'px) translateY(' + y + 'px)'; - } - /** * Remove class from an element. * @@ -1744,6 +2721,8 @@ * @param {String} className */ function removeClass(element, className) { + if (!className) return; + if (element.classList) { element.classList.remove(className); } else { @@ -1755,11 +2734,10 @@ } } - // Drag start predicate states. - var startPredicateInactive = 0; - var startPredicatePending = 1; - var startPredicateResolved = 2; - var startPredicateRejected = 3; + var START_PREDICATE_INACTIVE = 0; + var START_PREDICATE_PENDING = 1; + var START_PREDICATE_RESOLVED = 2; + var SCROLL_LISTENER_OPTIONS = hasPassiveEvents() ? { passive: true } : false; /** * Bind touch interaction to an item. @@ -1781,15 +2759,17 @@ this._startPredicate = isFunction(settings.dragStartPredicate) ? settings.dragStartPredicate : ItemDrag.defaultStartPredicate; - this._startPredicateState = startPredicateInactive; + this._startPredicateState = START_PREDICATE_INACTIVE; this._startPredicateResult = undefined; // Data for drag sort predicate heuristics. - this._hBlockedIndex = null; - this._hX1 = 0; - this._hX2 = 0; - this._hY1 = 0; - this._hY2 = 0; + this._isSortNeeded = false; + this._sortTimer = undefined; + this._blockedSortIndex = null; + this._sortX1 = 0; + this._sortX2 = 0; + this._sortY1 = 0; + this._sortY2 = 0; // Setup item's initial drag data. this._reset(); @@ -1798,24 +2778,38 @@ this._preStartCheck = this._preStartCheck.bind(this); this._preEndCheck = this._preEndCheck.bind(this); this._onScroll = this._onScroll.bind(this); + this._prepareStart = this._prepareStart.bind(this); + this._applyStart = this._applyStart.bind(this); this._prepareMove = this._prepareMove.bind(this); this._applyMove = this._applyMove.bind(this); this._prepareScroll = this._prepareScroll.bind(this); this._applyScroll = this._applyScroll.bind(this); - this._checkOverlap = this._checkOverlap.bind(this); + this._handleSort = this._handleSort.bind(this); + this._handleSortDelayed = this._handleSortDelayed.bind(this); - // Create debounce overlap checker function. - var sortInterval = settings.dragSortHeuristics.sortInterval; - this._checkOverlapDebounce = debounce(this._checkOverlap, sortInterval); + // Get drag handle element. + this._handle = (settings.dragHandle && element.querySelector(settings.dragHandle)) || element; // Init dragger. - this._dragger = new Dragger(element, settings.dragCssProps); + this._dragger = new Dragger(this._handle, settings.dragCssProps); this._dragger.on('start', this._preStartCheck); this._dragger.on('move', this._preStartCheck); this._dragger.on('cancel', this._preEndCheck); this._dragger.on('end', this._preEndCheck); } + /** + * Public properties + * ***************** + */ + + /** + * @public + * @static + * @type {AutoScroller} + */ + ItemDrag.autoScroller = new AutoScroller(); + /** * Public static methods * ********************* @@ -1829,18 +2823,35 @@ * the predicate will be called again on the next drag movement. * * @public - * @memberof ItemDrag + * @static * @param {Item} item - * @param {DraggerEvent} event + * @param {Object} event * @param {Object} [options] * - An optional options object which can be used to pass the predicate * it's options manually. By default the predicate retrieves the options * from the grid's settings. - * @returns {Boolean} + * @returns {(Boolean|undefined)} */ - ItemDrag.defaultStartPredicate = function(item, event, options) { + ItemDrag.defaultStartPredicate = function (item, event, options) { var drag = item._drag; - var predicate = drag._startPredicateData || drag._setupStartPredicate(options); + + // Make sure left button is pressed on mouse. + if (event.isFirst && event.srcEvent.button) { + return false; + } + + // If the start event is trusted, non-cancelable and it's default action has + // not been prevented it is in most cases a sign that the gesture would be + // cancelled anyways right after it has started (e.g. starting drag while + // the page is scrolling). + if ( + event.isFirst && + event.srcEvent.isTrusted === true && + event.srcEvent.defaultPrevented === false && + event.srcEvent.cancelable === false + ) { + return false; + } // Final event logic. At this stage return value does not matter anymore, // the predicate is either resolved or it's not and there's nothing to do @@ -1851,13 +2862,14 @@ return; } - // Find and store the handle element so we can check later on if the - // cursor is within the handle. If we have a handle selector let's find - // the corresponding element. Otherwise let's use the item element as the - // handle. - if (!predicate.handleElement) { - predicate.handleElement = drag._getStartPredicateHandle(event); - if (!predicate.handleElement) return false; + // Setup predicate data from options if not already set. + var predicate = drag._startPredicateData; + if (!predicate) { + var config = options || drag._getGrid()._settings.dragStartPredicate || {}; + drag._startPredicateData = predicate = { + distance: Math.max(config.distance, 0) || 0, + delay: Math.max(config.delay, 0) || 0, + }; } // If delay is defined let's keep track of the latest event and initiate @@ -1865,7 +2877,7 @@ if (predicate.delay) { predicate.event = event; if (!predicate.delayTimer) { - predicate.delayTimer = window.setTimeout(function() { + predicate.delayTimer = window.setTimeout(function () { predicate.delay = 0; if (drag._resolveStartPredicate(predicate.event)) { drag._forceResolveStartPredicate(predicate.event); @@ -1882,20 +2894,22 @@ * Default drag sort predicate. * * @public - * @memberof ItemDrag + * @static * @param {Item} item * @param {Object} [options] * @param {Number} [options.threshold=50] * @param {String} [options.action='move'] - * @returns {(Boolean|DragSortCommand)} - * - Returns false if no valid index was found. Otherwise returns drag sort + * @returns {?Object} + * - Returns `null` if no valid index was found. Otherwise returns drag sort * command. */ - ItemDrag.defaultSortPredicate = (function() { + ItemDrag.defaultSortPredicate = (function () { var itemRect = {}; var targetRect = {}; var returnData = {}; - var rootGridArray = []; + var gridsArray = []; + var minThreshold = 1; + var maxThreshold = 100; function getTargetGrid(item, rootGrid, threshold) { var target = null; @@ -1904,18 +2918,26 @@ var gridScore; var grids; var grid; + var container; + var containerRect; + var left; + var top; + var right; + var bottom; var i; // Get potential target grids. if (dragSort === true) { - rootGridArray[0] = rootGrid; - grids = rootGridArray; - } else { + gridsArray[0] = rootGrid; + grids = gridsArray; + } else if (isFunction(dragSort)) { grids = dragSort.call(rootGrid, item); } // Return immediately if there are no grids. - if (!Array.isArray(grids)) return target; + if (!grids || !Array.isArray(grids) || !grids.length) { + return target; + } // Loop through the grids and get the best match. for (i = 0; i < grids.length; i++) { @@ -1924,16 +2946,53 @@ // Filter out all destroyed grids. if (grid._isDestroyed) continue; - // We need to update the grid's offsets and dimensions since they might - // have changed (e.g during scrolling). + // Compute the grid's client rect an clamp the initial boundaries to + // viewport dimensions. grid._updateBoundingRect(); + left = Math.max(0, grid._left); + top = Math.max(0, grid._top); + right = Math.min(window.innerWidth, grid._right); + bottom = Math.min(window.innerHeight, grid._bottom); + + // The grid might be inside one or more elements that clip it's visibility + // (e.g overflow scroll/hidden) so we want to find out the visible portion + // of the grid in the viewport and use that in our calculations. + container = grid._element.parentNode; + while ( + container && + container !== document && + container !== document.documentElement && + container !== document.body + ) { + if (container.getRootNode && container instanceof DocumentFragment) { + container = container.getRootNode().host; + continue; + } + + if (getStyle(container, 'overflow') !== 'visible') { + containerRect = container.getBoundingClientRect(); + left = Math.max(left, containerRect.left); + top = Math.max(top, containerRect.top); + right = Math.min(right, containerRect.right); + bottom = Math.min(bottom, containerRect.bottom); + } + + if (getStyle(container, 'position') === 'fixed') { + break; + } + + container = container.parentNode; + } + + // No need to go further if target rect does not have visible area. + if (left >= right || top >= bottom) continue; // Check how much dragged element overlaps the container element. - targetRect.width = grid._width; - targetRect.height = grid._height; - targetRect.left = grid._left; - targetRect.top = grid._top; - gridScore = getRectOverlapScore(itemRect, targetRect); + targetRect.left = left; + targetRect.top = top; + targetRect.width = right - left; + targetRect.height = bottom - top; + gridScore = getIntersectionScore(itemRect, targetRect); // Check if this grid is the best match so far. if (gridScore > threshold && gridScore > bestScore) { @@ -1942,38 +3001,46 @@ } } - // Always reset root grid array. - rootGridArray.length = 0; + // Always reset grids array. + gridsArray.length = 0; return target; } - return function(item, options) { + return function (item, options) { var drag = item._drag; var rootGrid = drag._getGrid(); // Get drag sort predicate settings. var sortThreshold = options && typeof options.threshold === 'number' ? options.threshold : 50; - var sortAction = options && options.action === actionSwap ? actionSwap : actionMove; + var sortAction = options && options.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; + var migrateAction = + options && options.migrateAction === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; + + // Sort threshold must be a positive number capped to a max value of 100. If + // that's not the case this function will not work correctly. So let's clamp + // the threshold just in case. + sortThreshold = Math.min(Math.max(sortThreshold, minThreshold), maxThreshold); // Populate item rect data. itemRect.width = item._width; itemRect.height = item._height; - itemRect.left = drag._elementClientX; - itemRect.top = drag._elementClientY; + itemRect.left = drag._clientX; + itemRect.top = drag._clientY; // Calculate the target grid. var grid = getTargetGrid(item, rootGrid, sortThreshold); // Return early if we found no grid container element that overlaps the // dragged item enough. - if (!grid) return false; + if (!grid) return null; + var isMigration = item.getGrid() !== grid; var gridOffsetLeft = 0; var gridOffsetTop = 0; - var matchScore = -1; - var matchIndex; - var hasValidTargets; + var matchScore = 0; + var matchIndex = -1; + var hasValidTargets = false; var target; var score; var i; @@ -2008,7 +3075,7 @@ targetRect.height = target._height; targetRect.left = target._left + target._marginLeft + gridOffsetLeft; targetRect.top = target._top + target._marginTop + gridOffsetTop; - score = getRectOverlapScore(itemRect, targetRect); + score = getIntersectionScore(itemRect, targetRect); // Update best match index and score if the target's overlap score with // the dragged item is higher than the current best match score. @@ -2018,22 +3085,29 @@ } } - // If there is no valid match and the item is being moved into another - // grid. - if (matchScore < sortThreshold && item.getGrid() !== grid) { - matchIndex = hasValidTargets ? -1 : 0; - matchScore = Infinity; + // If there is no valid match and the dragged item is being moved into + // another grid we need to do some guess work here. If there simply are no + // valid targets (which means that the dragged item will be the only active + // item in the new grid) we can just add it as the first item. If we have + // valid items in the new grid and the dragged item is overlapping one or + // more of the items in the new grid let's make an exception with the + // threshold and just pick the item which the dragged item is overlapping + // most. However, if the dragged item is not overlapping any of the valid + // items in the new grid let's position it as the last item in the grid. + if (isMigration && matchScore < sortThreshold) { + matchIndex = hasValidTargets ? matchIndex : 0; + matchScore = sortThreshold; } // Check if the best match overlaps enough to justify a placement switch. if (matchScore >= sortThreshold) { returnData.grid = grid; returnData.index = matchIndex; - returnData.action = sortAction; + returnData.action = isMigration ? migrateAction : sortAction; return returnData; } - return false; + return null; }; })(); @@ -2046,62 +3120,84 @@ * Abort dragging and reset drag data. * * @public - * @memberof ItemDrag.prototype - * @returns {ItemDrag} */ - ItemDrag.prototype.stop = function() { - var item = this._item; - var element = item._element; - var grid = this._getGrid(); - - if (!this._isActive) return this; + ItemDrag.prototype.stop = function () { + if (!this._isActive) return; // If the item is being dropped into another grid, finish it up and return // immediately. if (this._isMigrating) { this._finishMigration(); - return this; + return; } - // Cancel queued move and scroll ticks. - cancelMoveTick(item._id); - cancelScrollTick(item._id); + // Cancel queued ticks. + var itemId = this._item._id; + cancelDragStartTick(itemId); + cancelDragMoveTick(itemId); + cancelDragScrollTick(itemId); - // Remove scroll listeners. - this._unbindScrollListeners(); + // Cancel sort procedure. + this._cancelSort(); - // Cancel overlap check. - this._checkOverlapDebounce('cancel'); + if (this._isStarted) { + // Remove scroll listeners. + this._unbindScrollListeners(); - // Append item element to the container if it's not it's child. Also make - // sure the translate values are adjusted to account for the DOM shift. - if (element.parentNode !== grid._element) { - grid._element.appendChild(element); - element.style[transformProp] = getTranslateString(this._gridX, this._gridY); - } + var element = item._element; + var grid = this._getGrid(); + var draggingClass = grid._settings.itemDraggingClass; + + // Append item element to the container if it's not it's child. Also make + // sure the translate values are adjusted to account for the DOM shift. + if (element.parentNode !== grid._element) { + grid._element.appendChild(element); + item._setTranslate(this._gridX, this._gridY); + + // We need to do forced reflow to make sure the dragging class is removed + // gracefully. + // eslint-disable-next-line + if (draggingClass) element.clientWidth; + } - // Remove dragging class. - removeClass(element, grid._settings.itemDraggingClass); + // Remove dragging class. + removeClass(element, draggingClass); + } // Reset drag data. this._reset(); + }; - return this; + /** + * Manually trigger drag sort. This is only needed for special edge cases where + * e.g. you have disabled sort and want to trigger a sort right after enabling + * it (and don't want to wait for the next move/scroll event). + * + * @private + * @param {Boolean} [force=false] + */ + ItemDrag.prototype.sort = function (force) { + var item = this._item; + if (item._isActive && this._dragMoveEvent) { + if (force === true) { + this._handleSort(); + } else { + addDragSortTick(item._id, this._handleSort); + } + } }; /** * Destroy instance. * * @public - * @memberof ItemDrag.prototype - * @returns {ItemDrag} */ - ItemDrag.prototype.destroy = function() { - if (this._isDestroyed) return this; + ItemDrag.prototype.destroy = function () { + if (this._isDestroyed) return; this.stop(); this._dragger.destroy(); + ItemDrag.autoScroller.removeItem(this._item); this._isDestroyed = true; - return this; }; /** @@ -2113,22 +3209,20 @@ * Get Grid instance. * * @private - * @memberof ItemDrag.prototype * @returns {?Grid} */ - ItemDrag.prototype._getGrid = function() { - return gridInstances[this._gridId] || null; + ItemDrag.prototype._getGrid = function () { + return GRID_INSTANCES[this._gridId] || null; }; /** * Setup/reset drag data. * * @private - * @memberof ItemDrag.prototype */ - ItemDrag.prototype._reset = function() { - // Is item being dragged? + ItemDrag.prototype._reset = function () { this._isActive = false; + this._isStarted = false; // The dragged item's container element. this._container = null; @@ -2137,7 +3231,9 @@ this._containingBlock = null; // Drag/scroll event data. - this._dragEvent = null; + this._dragStartEvent = null; + this._dragMoveEvent = null; + this._dragPrevMoveEvent = null; this._scrollEvent = null; // All the elements which need to be listened for scroll events during @@ -2154,8 +3250,16 @@ // Dragged element's current offset from window's northwest corner. Does // not account for element's margins. - this._elementClientX = 0; - this._elementClientY = 0; + this._clientX = 0; + this._clientY = 0; + + // Keep track of the clientX/Y diff for scrolling. + this._scrollDiffX = 0; + this._scrollDiffY = 0; + + // Keep track of the clientX/Y diff for moving. + this._moveDiffX = 0; + this._moveDiffY = 0; // Offset difference between the dragged element's temporary drag // container and it's original container. @@ -2168,9 +3272,8 @@ * dragged element and the drag container element. * * @private - * @memberof ItemDrag.prototype */ - ItemDrag.prototype._bindScrollListeners = function() { + ItemDrag.prototype._bindScrollListeners = function () { var gridContainer = this._getGrid()._element; var dragContainer = this._container; var scrollers = this._scrollers; @@ -2179,14 +3282,14 @@ // Get dragged element's scrolling parents. scrollers.length = 0; - getScrollableAncestors(this._item._element, false, scrollers); + getScrollableAncestors(this._item._element.parentNode, scrollers); // If drag container is defined and it's not the same element as grid // container then we need to add the grid container and it's scroll parents // to the elements which are going to be listener for scroll events. if (dragContainer !== gridContainer) { gridScrollers = []; - getScrollableAncestors(gridContainer, true, gridScrollers); + getScrollableAncestors(gridContainer, gridScrollers); for (i = 0; i < gridScrollers.length; i++) { if (scrollers.indexOf(gridScrollers[i]) < 0) { scrollers.push(gridScrollers[i]); @@ -2196,7 +3299,7 @@ // Bind scroll listeners. for (i = 0; i < scrollers.length; i++) { - scrollers[i].addEventListener('scroll', this._onScroll); + scrollers[i].addEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS); } }; @@ -2205,107 +3308,42 @@ * elements of the dragged element and the drag container element. * * @private - * @memberof ItemDrag.prototype */ - ItemDrag.prototype._unbindScrollListeners = function() { + ItemDrag.prototype._unbindScrollListeners = function () { var scrollers = this._scrollers; var i; for (i = 0; i < scrollers.length; i++) { - scrollers[i].removeEventListener('scroll', this._onScroll); + scrollers[i].removeEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS); } scrollers.length = 0; }; - /** - * Setup default start predicate. - * - * @private - * @memberof ItemDrag.prototype - * @param {Object} [options] - * @returns {Object} - */ - ItemDrag.prototype._setupStartPredicate = function(options) { - var config = options || this._getGrid()._settings.dragStartPredicate || 0; - return (this._startPredicateData = { - distance: Math.abs(config.distance) || 0, - delay: Math.max(config.delay, 0) || 0, - handle: typeof config.handle === 'string' ? config.handle : false - }); - }; - - /** - * Setup default start predicate handle. - * - * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event - * @returns {?HTMLElement} - */ - ItemDrag.prototype._getStartPredicateHandle = function(event) { - var predicate = this._startPredicateData; - var element = this._item._element; - var handleElement = element; - - // No handle, no hassle -> let's use the item element as the handle. - if (!predicate.handle) return handleElement; - - // If there is a specific predicate handle defined, let's try to get it. - handleElement = event.target; - while (handleElement && !elementMatches(handleElement, predicate.handle)) { - handleElement = handleElement !== element ? handleElement.parentElement : null; - } - return handleElement || null; - }; - /** * Unbind currently bound drag scroll handlers from all scrollable ancestor * elements of the dragged element and the drag container element. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event * @returns {Boolean} */ - ItemDrag.prototype._resolveStartPredicate = function(event) { + ItemDrag.prototype._resolveStartPredicate = function (event) { var predicate = this._startPredicateData; - - // If the moved distance is smaller than the threshold distance or there is - // some delay left, ignore this predicate cycle. if (event.distance < predicate.distance || predicate.delay) return; - - // Get handle rect data. - var handleRect = predicate.handleElement.getBoundingClientRect(); - var handleLeft = handleRect.left + (window.pageXOffset || 0); - var handleTop = handleRect.top + (window.pageYOffset || 0); - var handleWidth = handleRect.width; - var handleHeight = handleRect.height; - - // Reset predicate data. this._resetStartPredicate(); - - // If the cursor is still within the handle let's start the drag. - return ( - handleWidth && - handleHeight && - event.pageX >= handleLeft && - event.pageX < handleLeft + handleWidth && - event.pageY >= handleTop && - event.pageY < handleTop + handleHeight - ); + return true; }; /** * Forcefully resolve drag start predicate. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ - ItemDrag.prototype._forceResolveStartPredicate = function(event) { - if (!this._isDestroyed && this._startPredicateState === startPredicatePending) { - this._startPredicateState = startPredicateResolved; + ItemDrag.prototype._forceResolveStartPredicate = function (event) { + if (!this._isDestroyed && this._startPredicateState === START_PREDICATE_PENDING) { + this._startPredicateState = START_PREDICATE_RESOLVED; this._onStart(event); } }; @@ -2314,10 +3352,9 @@ * Finalize start predicate. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ - ItemDrag.prototype._finishStartPredicate = function(event) { + ItemDrag.prototype._finishStartPredicate = function (event) { var element = this._item._element; // Check if this is a click (very subjective heuristics). @@ -2335,13 +3372,13 @@ * Reset drag sort heuristics. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Number} x + * @param {Number} y */ - ItemDrag.prototype._resetHeuristics = function(event) { - this._hBlockedIndex = null; - this._hX1 = this._hX2 = event.clientX; - this._hY1 = this._hY2 = event.clientY; + ItemDrag.prototype._resetHeuristics = function (x, y) { + this._blockedSortIndex = null; + this._sortX1 = this._sortX2 = x; + this._sortY1 = this._sortY2 = y; }; /** @@ -2349,30 +3386,28 @@ * if it can not. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Number} x + * @param {Number} y * @returns {Boolean} */ - ItemDrag.prototype._checkHeuristics = function(event) { + ItemDrag.prototype._checkHeuristics = function (x, y) { var settings = this._getGrid()._settings.dragSortHeuristics; var minDist = settings.minDragDistance; // Skip heuristics if not needed. if (minDist <= 0) { - this._hBlockedIndex = null; + this._blockedSortIndex = null; return true; } - var x = event.clientX; - var y = event.clientY; - var diffX = x - this._hX2; - var diffY = y - this._hY2; + var diffX = x - this._sortX2; + var diffY = y - this._sortY2; // If we can't do proper bounce back check make sure that the blocked index // is not set. var canCheckBounceBack = minDist > 3 && settings.minBounceBackAngle > 0; if (!canCheckBounceBack) { - this._hBlockedIndex = null; + this._blockedSortIndex = null; } if (Math.abs(diffX) > minDist || Math.abs(diffY) > minDist) { @@ -2380,18 +3415,18 @@ // minimum value of 3 for minDragDistance to function properly. if (canCheckBounceBack) { var angle = Math.atan2(diffX, diffY); - var prevAngle = Math.atan2(this._hX2 - this._hX1, this._hY2 - this._hY1); + var prevAngle = Math.atan2(this._sortX2 - this._sortX1, this._sortY2 - this._sortY1); var deltaAngle = Math.atan2(Math.sin(angle - prevAngle), Math.cos(angle - prevAngle)); if (Math.abs(deltaAngle) > settings.minBounceBackAngle) { - this._hBlockedIndex = null; + this._blockedSortIndex = null; } } // Update points. - this._hX1 = this._hX2; - this._hY1 = this._hY2; - this._hX2 = x; - this._hY2 = y; + this._sortX1 = this._sortX2; + this._sortY1 = this._sortY2; + this._sortX2 = x; + this._sortY2 = y; return true; } @@ -2403,9 +3438,8 @@ * Reset for default drag start predicate function. * * @private - * @memberof ItemDrag.prototype */ - ItemDrag.prototype._resetStartPredicate = function() { + ItemDrag.prototype._resetStartPredicate = function () { var predicate = this._startPredicateData; if (predicate) { if (predicate.delayTimer) { @@ -2415,14 +3449,97 @@ } }; + /** + * Handle the sorting procedure. Manage drag sort heuristics/interval and + * check overlap when necessary. + * + * @private + */ + ItemDrag.prototype._handleSort = function () { + var settings = this._getGrid()._settings; + + // No sorting when drag sort is disabled. Also, account for the scenario where + // dragSort is temporarily disabled during drag procedure so we need to reset + // sort timer heuristics state too. + if ( + !settings.dragSort || + (!settings.dragAutoScroll.sortDuringScroll && ItemDrag.autoScroller.isItemScrolling(this._item)) + ) { + this._sortX1 = this._sortX2 = this._gridX; + this._sortY1 = this._sortY2 = this._gridY; + // We set this to true intentionally so that overlap check would be + // triggered as soon as possible after sort becomes enabled again. + this._isSortNeeded = true; + if (this._sortTimer !== undefined) { + this._sortTimer = window.clearTimeout(this._sortTimer); + } + return; + } + + // If sorting is enabled we always need to run the heuristics check to keep + // the tracked coordinates updated. We also allow an exception when the sort + // timer is finished because the heuristics are intended to prevent overlap + // checks based on the dragged element's immediate movement and a delayed + // overlap check is valid if it comes through, because it was valid when it + // was invoked. + var shouldSort = this._checkHeuristics(this._gridX, this._gridY); + if (!this._isSortNeeded && !shouldSort) return; + + var sortInterval = settings.dragSortHeuristics.sortInterval; + if (sortInterval <= 0 || this._isSortNeeded) { + this._isSortNeeded = false; + if (this._sortTimer !== undefined) { + this._sortTimer = window.clearTimeout(this._sortTimer); + } + this._checkOverlap(); + } else if (this._sortTimer === undefined) { + this._sortTimer = window.setTimeout(this._handleSortDelayed, sortInterval); + } + }; + + /** + * Delayed sort handler. + * + * @private + */ + ItemDrag.prototype._handleSortDelayed = function () { + this._isSortNeeded = true; + this._sortTimer = undefined; + addDragSortTick(this._item._id, this._handleSort); + }; + + /** + * Cancel and reset sort procedure. + * + * @private + */ + ItemDrag.prototype._cancelSort = function () { + this._isSortNeeded = false; + if (this._sortTimer !== undefined) { + this._sortTimer = window.clearTimeout(this._sortTimer); + } + cancelDragSortTick(this._item._id); + }; + + /** + * Handle the ending of the drag procedure for sorting. + * + * @private + */ + ItemDrag.prototype._finishSort = function () { + var isSortEnabled = this._getGrid()._settings.dragSort; + var needsFinalCheck = isSortEnabled && (this._isSortNeeded || this._sortTimer !== undefined); + this._cancelSort(); + if (needsFinalCheck) this._checkOverlap(); + }; + /** * Check (during drag) if an item is overlapping other items and based on * the configuration layout the items. * * @private - * @memberof ItemDrag.prototype */ - ItemDrag.prototype._checkOverlap = function() { + ItemDrag.prototype._checkOverlap = function () { if (!this._isActive) return; var item = this._item; @@ -2432,12 +3549,13 @@ var currentIndex; var targetGrid; var targetIndex; + var targetItem; var sortAction; var isMigration; // Get overlap check result. if (isFunction(settings.dragSortPredicate)) { - result = settings.dragSortPredicate(item, this._dragEvent); + result = settings.dragSortPredicate(item, this._dragMoveEvent); } else { result = ItemDrag.defaultSortPredicate(item, settings.dragSortPredicate); } @@ -2445,15 +3563,19 @@ // Let's make sure the result object has a valid index before going further. if (!result || typeof result.index !== 'number') return; + sortAction = result.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; currentGrid = item.getGrid(); targetGrid = result.grid || currentGrid; isMigration = currentGrid !== targetGrid; currentIndex = currentGrid._items.indexOf(item); - targetIndex = normalizeArrayIndex(targetGrid._items, result.index, isMigration); - sortAction = result.action === actionSwap ? actionSwap : actionMove; + targetIndex = normalizeArrayIndex( + targetGrid._items, + result.index, + isMigration && sortAction === ACTION_MOVE ? 1 : 0 + ); // Prevent position bounce. - if (!isMigration && targetIndex === this._hBlockedIndex) { + if (!isMigration && targetIndex === this._blockedSortIndex) { return; } @@ -2461,22 +3583,22 @@ if (!isMigration) { // Make sure the target index is not the current index. if (currentIndex !== targetIndex) { - this._hBlockedIndex = currentIndex; + this._blockedSortIndex = currentIndex; // Do the sort. - (sortAction === actionSwap ? arraySwap : arrayMove)( + (sortAction === ACTION_SWAP ? arraySwap : arrayMove)( currentGrid._items, currentIndex, targetIndex ); // Emit move event. - if (currentGrid._hasListeners(eventMove)) { - currentGrid._emit(eventMove, { + if (currentGrid._hasListeners(EVENT_MOVE)) { + currentGrid._emit(EVENT_MOVE, { item: item, fromIndex: currentIndex, toIndex: targetIndex, - action: sortAction + action: sortAction, }); } @@ -2487,27 +3609,30 @@ // If the item was moved to another grid. else { - this._hBlockedIndex = null; + this._blockedSortIndex = null; + + // Let's fetch the target item when it's still in it's original index. + targetItem = targetGrid._items[targetIndex]; // Emit beforeSend event. - if (currentGrid._hasListeners(eventBeforeSend)) { - currentGrid._emit(eventBeforeSend, { + if (currentGrid._hasListeners(EVENT_BEFORE_SEND)) { + currentGrid._emit(EVENT_BEFORE_SEND, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } // Emit beforeReceive event. - if (targetGrid._hasListeners(eventBeforeReceive)) { - targetGrid._emit(eventBeforeReceive, { + if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) { + targetGrid._emit(EVENT_BEFORE_RECEIVE, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } @@ -2521,33 +3646,48 @@ currentGrid._items.splice(currentIndex, 1); arrayInsert(targetGrid._items, item, targetIndex); - // Set sort data as null, which is an indicator for the item comparison - // function that the sort data of this specific item should be fetched - // lazily. + // Reset sort data. item._sortData = null; // Emit send event. - if (currentGrid._hasListeners(eventSend)) { - currentGrid._emit(eventSend, { + if (currentGrid._hasListeners(EVENT_SEND)) { + currentGrid._emit(EVENT_SEND, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } // Emit receive event. - if (targetGrid._hasListeners(eventReceive)) { - targetGrid._emit(eventReceive, { + if (targetGrid._hasListeners(EVENT_RECEIVE)) { + targetGrid._emit(EVENT_RECEIVE, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } + // If the sort action is "swap" let's respect it and send the target item + // (if it exists) from the target grid to the originating grid. This process + // is done on purpose after the dragged item placed within the target grid + // so that we can keep this implementation as simple as possible utilizing + // the existing API. + if (sortAction === ACTION_SWAP && targetItem && targetItem.isActive()) { + // Sanity check to make sure that the target item is still part of the + // target grid. It could have been manipulated in the event handlers. + if (targetGrid._items.indexOf(targetItem) > -1) { + targetGrid.send(targetItem, currentGrid, currentIndex, { + appendTo: this._container || document.body, + layoutSender: false, + layoutReceiver: false, + }); + } + } + // Layout both grids. currentGrid.layout(); targetGrid.layout(); @@ -2559,11 +3699,10 @@ * gracefully. * * @private - * @memberof ItemDrag.prototype */ - ItemDrag.prototype._finishMigration = function() { + ItemDrag.prototype._finishMigration = function () { var item = this._item; - var release = item._release; + var release = item._dragRelease; var element = item._element; var isActive = item._isActive; var targetGrid = item.getGrid(); @@ -2572,6 +3711,10 @@ var targetContainer = targetSettings.dragContainer || targetGridElement; var currentSettings = this._getGrid()._settings; var currentContainer = element.parentNode; + var currentVisClass = isActive + ? currentSettings.itemVisibleClass + : currentSettings.itemHiddenClass; + var nextVisClass = isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass; var translate; var offsetDiff; @@ -2581,14 +3724,17 @@ this._isMigrating = false; this.destroy(); - // Remove current classnames. - removeClass(element, currentSettings.itemClass); - removeClass(element, currentSettings.itemVisibleClass); - removeClass(element, currentSettings.itemHiddenClass); + // Update item class. + if (currentSettings.itemClass !== targetSettings.itemClass) { + removeClass(element, currentSettings.itemClass); + addClass(element, targetSettings.itemClass); + } - // Add new classnames. - addClass(element, targetSettings.itemClass); - addClass(element, isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass); + // Update visibility class. + if (currentVisClass !== nextVisClass) { + removeClass(element, currentVisClass); + addClass(element, nextVisClass); + } // Move the item inside the target container if it's different than the // current container. @@ -2600,9 +3746,8 @@ translate.y -= offsetDiff.top; } - // Update item's cached dimensions and sort data. + // Update item's cached dimensions. item._refreshDimensions(); - item._refreshSortData(); // Calculate the offset difference between target's drag container (if any) // and actual grid container element. We save it later for the release @@ -2617,12 +3762,11 @@ // Adjust the position of the item element if it was moved from a container // to another. if (targetContainer !== currentContainer) { - element.style[transformProp] = getTranslateString(translate.x, translate.y); + item._setTranslate(translate.x, translate.y); } // Update child element's styles to reflect the current visibility state. - item._child.removeAttribute('style'); - setStyles(item._child, isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles); + item._visibility.setStyles(isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles); // Start the release. release.start(); @@ -2632,28 +3776,29 @@ * Drag pre-start handler. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ - ItemDrag.prototype._preStartCheck = function(event) { + ItemDrag.prototype._preStartCheck = function (event) { // Let's activate drag start predicate state. - if (this._startPredicateState === startPredicateInactive) { - this._startPredicateState = startPredicatePending; + if (this._startPredicateState === START_PREDICATE_INACTIVE) { + this._startPredicateState = START_PREDICATE_PENDING; } // If predicate is pending try to resolve it. - if (this._startPredicateState === startPredicatePending) { + if (this._startPredicateState === START_PREDICATE_PENDING) { this._startPredicateResult = this._startPredicate(this._item, event); if (this._startPredicateResult === true) { - this._startPredicateState = startPredicateResolved; + this._startPredicateState = START_PREDICATE_RESOLVED; this._onStart(event); } else if (this._startPredicateResult === false) { - this._startPredicateState = startPredicateRejected; + this._resetStartPredicate(event); + this._dragger._reset(); + this._startPredicateState = START_PREDICATE_INACTIVE; } } // Otherwise if predicate is resolved and drag is active, move the item. - else if (this._startPredicateState === startPredicateResolved && this._isActive) { + else if (this._startPredicateState === START_PREDICATE_RESOLVED && this._isActive) { this._onMove(event); } }; @@ -2662,217 +3807,306 @@ * Drag pre-end handler. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ - ItemDrag.prototype._preEndCheck = function(event) { - // Check if the start predicate was resolved during drag. - var isResolved = this._startPredicateState === startPredicateResolved; + ItemDrag.prototype._preEndCheck = function (event) { + var isResolved = this._startPredicateState === START_PREDICATE_RESOLVED; // Do final predicate check to allow user to unbind stuff for the current // drag procedure within the predicate callback. The return value of this // check will have no effect to the state of the predicate. this._startPredicate(this._item, event); - // Reset start predicate state. - this._startPredicateState = startPredicateInactive; + this._startPredicateState = START_PREDICATE_INACTIVE; - // If predicate is resolved and dragging is active, call the end handler. - if (isResolved && this._isActive) this._onEnd(event); + if (!isResolved || !this._isActive) return; + + if (this._isStarted) { + this._onEnd(event); + } else { + this.stop(); + } }; /** * Drag start handler. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event + */ + ItemDrag.prototype._onStart = function (event) { + var item = this._item; + if (!item._isActive) return; + + this._isActive = true; + this._dragStartEvent = event; + ItemDrag.autoScroller.addItem(item); + + addDragStartTick(item._id, this._prepareStart, this._applyStart); + }; + + /** + * Prepare item to be dragged. + * + * @private + * ItemDrag.prototype + */ + ItemDrag.prototype._prepareStart = function () { + var item = this._item; + if (!item._isActive) return; + + var element = item._element; + var grid = this._getGrid(); + var settings = grid._settings; + var gridContainer = grid._element; + var dragContainer = settings.dragContainer || gridContainer; + var containingBlock = getContainingBlock(dragContainer); + var translate = getTranslate(element); + var elementRect = element.getBoundingClientRect(); + var hasDragContainer = dragContainer !== gridContainer; + + this._container = dragContainer; + this._containingBlock = containingBlock; + this._clientX = elementRect.left; + this._clientY = elementRect.top; + this._left = this._gridX = translate.x; + this._top = this._gridY = translate.y; + this._scrollDiffX = this._scrollDiffY = 0; + this._moveDiffX = this._moveDiffY = 0; + + this._resetHeuristics(this._gridX, this._gridY); + + // If a specific drag container is set and it is different from the + // grid's container element we store the offset between containers. + if (hasDragContainer) { + var offsetDiff = getOffsetDiff(containingBlock, gridContainer); + this._containerDiffX = offsetDiff.left; + this._containerDiffY = offsetDiff.top; + } + }; + + /** + * Start drag for the item. + * + * @private */ - ItemDrag.prototype._onStart = function(event) { + ItemDrag.prototype._applyStart = function () { var item = this._item; - - // If item is not active, don't start the drag. if (!item._isActive) return; - var element = item._element; var grid = this._getGrid(); - var settings = grid._settings; - var release = item._release; + var element = item._element; + var release = item._dragRelease; var migrate = item._migrate; - var gridContainer = grid._element; - var dragContainer = settings.dragContainer || gridContainer; - var containingBlock = getContainingBlock(dragContainer, true); - var translate = getTranslate(element); - var currentLeft = translate.x; - var currentTop = translate.y; - var elementRect = element.getBoundingClientRect(); - var hasDragContainer = dragContainer !== gridContainer; - var offsetDiff; - - // Reset heuristics data. - this._resetHeuristics(event); - - // If grid container is not the drag container, we need to calculate the - // offset difference between grid container and drag container's containing - // element. - if (hasDragContainer) { - offsetDiff = getOffsetDiff(containingBlock, gridContainer); - } + var hasDragContainer = this._container !== grid._element; - // Stop current positioning animation. if (item.isPositioning()) { - item._layout.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); + item._layout.stop(true, this._left, this._top); } - // Stop current migration animation. if (migrate._isActive) { - currentLeft -= migrate._containerDiffX; - currentTop -= migrate._containerDiffY; - migrate.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); + this._left -= migrate._containerDiffX; + this._top -= migrate._containerDiffY; + this._gridX -= migrate._containerDiffX; + this._gridY -= migrate._containerDiffY; + migrate.stop(true, this._left, this._top); } - // If item is being released reset release data. - if (item.isReleasing()) release._reset(); - - // Setup drag data. - this._isActive = true; - this._dragEvent = event; - this._container = dragContainer; - this._containingBlock = containingBlock; - this._elementClientX = elementRect.left; - this._elementClientY = elementRect.top; - this._left = this._gridX = currentLeft; - this._top = this._gridY = currentTop; + if (item.isReleasing()) { + release._reset(); + } - // Create placeholder (if necessary). - if (settings.dragPlaceholder.enabled) { + if (grid._settings.dragPlaceholder.enabled) { item._dragPlaceholder.create(); } - // Emit dragInit event. - grid._emit(eventDragInit, item, event); + this._isStarted = true; - // If a specific drag container is set and it is different from the - // grid's container element we need to cast some extra spells. - if (hasDragContainer) { - // Store the container offset diffs to drag data. - this._containerDiffX = offsetDiff.left; - this._containerDiffY = offsetDiff.top; + grid._emit(EVENT_DRAG_INIT, item, this._dragStartEvent); + if (hasDragContainer) { // If the dragged element is a child of the drag container all we need to // do is setup the relative drag position data. - if (element.parentNode === dragContainer) { - this._gridX = currentLeft - this._containerDiffX; - this._gridY = currentTop - this._containerDiffY; + if (element.parentNode === this._container) { + this._gridX -= this._containerDiffX; + this._gridY -= this._containerDiffY; } - // Otherwise we need to append the element inside the correct container, // setup the actual drag position data and adjust the element's translate // values to account for the DOM position shift. else { - this._left = currentLeft + this._containerDiffX; - this._top = currentTop + this._containerDiffY; - dragContainer.appendChild(element); - element.style[transformProp] = getTranslateString(this._left, this._top); + this._left += this._containerDiffX; + this._top += this._containerDiffY; + this._container.appendChild(element); + item._setTranslate(this._left, this._top); } } - // Set drag class and bind scrollers. - addClass(element, settings.itemDraggingClass); + addClass(element, grid._settings.itemDraggingClass); this._bindScrollListeners(); - - // Emit dragStart event. - grid._emit(eventDragStart, item, event); + grid._emit(EVENT_DRAG_START, item, this._dragStartEvent); }; /** * Drag move handler. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ - ItemDrag.prototype._onMove = function(event) { + ItemDrag.prototype._onMove = function (event) { var item = this._item; - // If item is not active, reset drag. if (!item._isActive) { this.stop(); return; } + this._dragMoveEvent = event; + addDragMoveTick(item._id, this._prepareMove, this._applyMove); + addDragSortTick(item._id, this._handleSort); + }; + + /** + * Prepare dragged item for moving. + * + * @private + */ + ItemDrag.prototype._prepareMove = function () { + var item = this._item; + + if (!item._isActive) return; + var settings = this._getGrid()._settings; var axis = settings.dragAxis; + var nextEvent = this._dragMoveEvent; + var prevEvent = this._dragPrevMoveEvent || this._dragStartEvent || nextEvent; // Update horizontal position data. if (axis !== 'y') { - var xDiff = event.clientX - this._dragEvent.clientX; - this._left += xDiff; - this._gridX += xDiff; - this._elementClientX += xDiff; + var moveDiffX = nextEvent.clientX - prevEvent.clientX; + this._left = this._left - this._moveDiffX + moveDiffX; + this._gridX = this._gridX - this._moveDiffX + moveDiffX; + this._clientX = this._clientX - this._moveDiffX + moveDiffX; + this._moveDiffX = moveDiffX; } // Update vertical position data. if (axis !== 'x') { - var yDiff = event.clientY - this._dragEvent.clientY; - this._top += yDiff; - this._gridY += yDiff; - this._elementClientY += yDiff; + var moveDiffY = nextEvent.clientY - prevEvent.clientY; + this._top = this._top - this._moveDiffY + moveDiffY; + this._gridY = this._gridY - this._moveDiffY + moveDiffY; + this._clientY = this._clientY - this._moveDiffY + moveDiffY; + this._moveDiffY = moveDiffY; } - // Update event data. - this._dragEvent = event; + this._dragPrevMoveEvent = nextEvent; + }; + + /** + * Apply movement to dragged item. + * + * @private + */ + ItemDrag.prototype._applyMove = function () { + var item = this._item; + if (!item._isActive) return; - // Do move prepare/apply handling in the next tick. - addMoveTick(item._id, this._prepareMove, this._applyMove); + this._moveDiffX = this._moveDiffY = 0; + item._setTranslate(this._left, this._top); + this._getGrid()._emit(EVENT_DRAG_MOVE, item, this._dragMoveEvent); + ItemDrag.autoScroller.updateItem(item); }; /** - * Prepare dragged item for moving. + * Drag scroll handler. * * @private - * @memberof ItemDrag.prototype + * @param {Object} event */ - ItemDrag.prototype._prepareMove = function() { - // Do nothing if item is not active. - if (!this._item._isActive) return; + ItemDrag.prototype._onScroll = function (event) { + var item = this._item; - // If drag sort is enabled -> check overlap. - if (this._getGrid()._settings.dragSort) { - if (this._checkHeuristics(this._dragEvent)) { - this._checkOverlapDebounce(); - } + if (!item._isActive) { + this.stop(); + return; } + + this._scrollEvent = event; + addDragScrollTick(item._id, this._prepareScroll, this._applyScroll); + addDragSortTick(item._id, this._handleSort); }; /** - * Apply movement to dragged item. + * Prepare dragged item for scrolling. * * @private - * @memberof ItemDrag.prototype */ - ItemDrag.prototype._applyMove = function() { + ItemDrag.prototype._prepareScroll = function () { var item = this._item; - // Do nothing if item is not active. + // If item is not active do nothing. if (!item._isActive) return; - // Update element's translateX/Y values. - item._element.style[transformProp] = getTranslateString(this._left, this._top); + var element = item._element; + var grid = this._getGrid(); + var gridContainer = grid._element; + var axis = grid._settings.dragAxis; + var moveX = axis !== 'y'; + var moveY = axis !== 'x'; + var rect = element.getBoundingClientRect(); + + // Update container diff. + if (this._container !== gridContainer) { + var offsetDiff = getOffsetDiff(this._containingBlock, gridContainer); + this._containerDiffX = offsetDiff.left; + this._containerDiffY = offsetDiff.top; + } + + // Update horizontal position data. + if (moveX) { + var scrollDiffX = this._clientX - this._moveDiffX - this._scrollDiffX - rect.left; + this._left = this._left - this._scrollDiffX + scrollDiffX; + this._scrollDiffX = scrollDiffX; + } + + // Update vertical position data. + if (moveY) { + var scrollDiffY = this._clientY - this._moveDiffY - this._scrollDiffY - rect.top; + this._top = this._top - this._scrollDiffY + scrollDiffY; + this._scrollDiffY = scrollDiffY; + } - // Emit dragMove event. - this._getGrid()._emit(eventDragMove, item, this._dragEvent); + // Update grid position. + this._gridX = this._left - this._containerDiffX; + this._gridY = this._top - this._containerDiffY; }; /** - * Drag scroll handler. + * Apply scroll to dragged item. + * + * @private + */ + ItemDrag.prototype._applyScroll = function () { + var item = this._item; + if (!item._isActive) return; + + this._scrollDiffX = this._scrollDiffY = 0; + item._setTranslate(this._left, this._top); + this._getGrid()._emit(EVENT_DRAG_SCROLL, item, this._scrollEvent); + }; + + /** + * Drag end handler. * * @private - * @memberof ItemDrag.prototype - * @param {Event} event + * @param {Object} event */ - ItemDrag.prototype._onScroll = function(event) { + ItemDrag.prototype._onEnd = function (event) { var item = this._item; + var element = item._element; + var grid = this._getGrid(); + var settings = grid._settings; + var release = item._dragRelease; // If item is not active, reset drag. if (!item._isActive) { @@ -2880,180 +4114,359 @@ return; } - // Update last scroll event. - this._scrollEvent = event; + // Cancel queued ticks. + cancelDragStartTick(item._id); + cancelDragMoveTick(item._id); + cancelDragScrollTick(item._id); + + // Finish sort procedure (does final overlap check if needed). + this._finishSort(); + + // Remove scroll listeners. + this._unbindScrollListeners(); - // Do scroll prepare/apply handling in the next tick. - addScrollTick(item._id, this._prepareScroll, this._applyScroll); + // Setup release data. + release._containerDiffX = this._containerDiffX; + release._containerDiffY = this._containerDiffY; + + // Reset drag data. + this._reset(); + + // Remove drag class name from element. + removeClass(element, settings.itemDraggingClass); + + // Stop auto-scroll. + ItemDrag.autoScroller.removeItem(item); + + // Emit dragEnd event. + grid._emit(EVENT_DRAG_END, item, event); + + // Finish up the migration process or start the release process. + this._isMigrating ? this._finishMigration() : release.start(); }; /** - * Prepare dragged item for scrolling. + * Private helpers + * *************** + */ + + /** + * Check if an element is an anchor element and open the href url if possible. * - * @private - * @memberof ItemDrag.prototype + * @param {HTMLElement} element */ - ItemDrag.prototype._prepareScroll = function() { - var item = this._item; + function openAnchorHref(element) { + // Make sure the element is anchor element. + if (element.tagName.toLowerCase() !== 'a') return; - // If item is not active do nothing. - if (!item._isActive) return; + // Get href and make sure it exists. + var href = element.getAttribute('href'); + if (!href) return; - var element = item._element; - var grid = this._getGrid(); - var settings = grid._settings; - var axis = settings.dragAxis; - var gridContainer = grid._element; - var offsetDiff; + // Finally let's navigate to the link href. + var target = element.getAttribute('target'); + if (target && target !== '_self') { + window.open(href, target); + } else { + window.location.href = href; + } + } - // Calculate element's rect and x/y diff. - var rect = element.getBoundingClientRect(); - var xDiff = this._elementClientX - rect.left; - var yDiff = this._elementClientY - rect.top; + /** + * Get current values of the provided styles definition object or array. + * + * @param {HTMLElement} element + * @param {(Object|Array} styles + * @return {Object} + */ + function getCurrentStyles(element, styles) { + var result = {}; + var prop, i; + + if (Array.isArray(styles)) { + for (i = 0; i < styles.length; i++) { + prop = styles[i]; + result[prop] = getStyle(element, getStyleName(prop)); + } + } else { + for (prop in styles) { + result[prop] = getStyle(element, getStyleName(prop)); + } + } + + return result; + } + + var unprefixRegEx = /^(webkit|moz|ms|o|Webkit|Moz|MS|O)(?=[A-Z])/; + var cache$2 = {}; + + /** + * Remove any potential vendor prefixes from a property name. + * + * @param {String} prop + * @returns {String} + */ + function getUnprefixedPropName(prop) { + var result = cache$2[prop]; + if (result) return result; + + result = prop.replace(unprefixRegEx, ''); + + if (result !== prop) { + result = result[0].toLowerCase() + result.slice(1); + } + + cache$2[prop] = result; + + return result; + } + + var nativeCode = '[native code]'; + + /** + * Check if a value (e.g. a method or constructor) is native code. Good for + * detecting when a polyfill is used and when not. + * + * @param {*} feat + * @returns {Boolean} + */ + function isNative(feat) { + var S = window.Symbol; + return !!( + feat && + isFunction(S) && + isFunction(S.toString) && + S(feat).toString().indexOf(nativeCode) > -1 + ); + } + + /** + * Set inline styles to an element. + * + * @param {HTMLElement} element + * @param {Object} styles + */ + function setStyles(element, styles) { + for (var prop in styles) { + element.style[prop] = styles[prop]; + } + } + + var HAS_WEB_ANIMATIONS = !!(Element && isFunction(Element.prototype.animate)); + var HAS_NATIVE_WEB_ANIMATIONS = !!(Element && isNative(Element.prototype.animate)); + + /** + * Item animation handler powered by Web Animations API. + * + * @class + * @param {HTMLElement} element + */ + function Animator(element) { + this._element = element; + this._animation = null; + this._duration = 0; + this._easing = ''; + this._callback = null; + this._props = []; + this._values = []; + this._isDestroyed = false; + this._onFinish = this._onFinish.bind(this); + } + + /** + * Public prototype methods + * ************************ + */ + + /** + * Start instance's animation. Automatically stops current animation if it is + * running. + * + * @public + * @param {Object} propsFrom + * @param {Object} propsTo + * @param {Object} [options] + * @param {Number} [options.duration=300] + * @param {String} [options.easing='ease'] + * @param {Function} [options.onFinish] + */ + Animator.prototype.start = function (propsFrom, propsTo, options) { + if (this._isDestroyed) return; + + var element = this._element; + var opts = options || {}; + + // If we don't have web animations available let's not animate. + if (!HAS_WEB_ANIMATIONS) { + setStyles(element, propsTo); + this._callback = isFunction(opts.onFinish) ? opts.onFinish : null; + this._onFinish(); + return; + } + + var animation = this._animation; + var currentProps = this._props; + var currentValues = this._values; + var duration = opts.duration || 300; + var easing = opts.easing || 'ease'; + var cancelAnimation = false; + var propName, propCount, propIndex; + + // If we have an existing animation running, let's check if it needs to be + // cancelled or if it can continue running. + if (animation) { + propCount = 0; + + // Cancel animation if duration or easing has changed. + if (duration !== this._duration || easing !== this._easing) { + cancelAnimation = true; + } + + // Check if the requested animation target props and values match with the + // current props and values. + if (!cancelAnimation) { + for (propName in propsTo) { + ++propCount; + propIndex = currentProps.indexOf(propName); + if (propIndex === -1 || propsTo[propName] !== currentValues[propIndex]) { + cancelAnimation = true; + break; + } + } + + // Check if the target props count matches current props count. This is + // needed for the edge case scenario where target props contain the same + // styles as current props, but the current props have some additional + // props. + if (propCount !== currentProps.length) { + cancelAnimation = true; + } + } + } + + // Cancel animation (if required). + if (cancelAnimation) animation.cancel(); + + // Store animation callback. + this._callback = isFunction(opts.onFinish) ? opts.onFinish : null; - // Update container diff. - if (this._container !== gridContainer) { - offsetDiff = getOffsetDiff(this._containingBlock, gridContainer); - this._containerDiffX = offsetDiff.left; - this._containerDiffY = offsetDiff.top; - } + // If we have a running animation that does not need to be cancelled, let's + // call it a day here and let it run. + if (animation && !cancelAnimation) return; - // Update horizontal position data. - if (axis !== 'y') { - this._left += xDiff; - this._gridX = this._left - this._containerDiffX; + // Store target props and values to instance. + currentProps.length = currentValues.length = 0; + for (propName in propsTo) { + currentProps.push(propName); + currentValues.push(propsTo[propName]); } - // Update vertical position data. - if (axis !== 'x') { - this._top += yDiff; - this._gridY = this._top - this._containerDiffY; - } + // Start the animation. We need to provide unprefixed property names to the + // Web Animations polyfill if it is being used. If we have native Web + // Animations available we need to provide prefixed properties instead. + this._duration = duration; + this._easing = easing; + this._animation = element.animate( + [ + createFrame(propsFrom, HAS_NATIVE_WEB_ANIMATIONS), + createFrame(propsTo, HAS_NATIVE_WEB_ANIMATIONS), + ], + { + duration: duration, + easing: easing, + } + ); + this._animation.onfinish = this._onFinish; - // Overlap handling. - if (settings.dragSort) this._checkOverlapDebounce(); + // Set the end styles. This makes sure that the element stays at the end + // values after animation is finished. + setStyles(element, propsTo); }; /** - * Apply scroll to dragged item. + * Stop instance's current animation if running. * - * @private - * @memberof ItemDrag.prototype + * @public */ - ItemDrag.prototype._applyScroll = function() { - var item = this._item; - - // If item is not active do nothing. - if (!item._isActive) return; - - // Update element's translateX/Y values. - item._element.style[transformProp] = getTranslateString(this._left, this._top); - - // Emit dragScroll event. - this._getGrid()._emit(eventDragScroll, item, this._scrollEvent); + Animator.prototype.stop = function () { + if (this._isDestroyed || !this._animation) return; + this._animation.cancel(); + this._animation = this._callback = null; + this._props.length = this._values.length = 0; }; /** - * Drag end handler. + * Read the current values of the element's animated styles from the DOM. * - * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @public + * @return {Object} */ - ItemDrag.prototype._onEnd = function(event) { - var item = this._item; - var element = item._element; - var grid = this._getGrid(); - var settings = grid._settings; - var release = item._release; - - // If item is not active, reset drag. - if (!item._isActive) { - this.stop(); - return; - } - - // Cancel queued move and scroll ticks. - cancelMoveTick(item._id); - cancelScrollTick(item._id); - - // Finish currently queued overlap check. - settings.dragSort && this._checkOverlapDebounce('finish'); - - // Remove scroll listeners. - this._unbindScrollListeners(); - - // Setup release data. - release._containerDiffX = this._containerDiffX; - release._containerDiffY = this._containerDiffY; - - // Reset drag data. - this._reset(); - - // Remove drag class name from element. - removeClass(element, settings.itemDraggingClass); + Animator.prototype.getCurrentStyles = function () { + return getCurrentStyles(element, currentProps); + }; - // Emit dragEnd event. - grid._emit(eventDragEnd, item, event); + /** + * Check if the item is being animated currently. + * + * @public + * @return {Boolean} + */ + Animator.prototype.isAnimating = function () { + return !!this._animation; + }; - // Finish up the migration process or start the release process. - this._isMigrating ? this._finishMigration() : release.start(); + /** + * Destroy the instance and stop current animation if it is running. + * + * @public + */ + Animator.prototype.destroy = function () { + if (this._isDestroyed) return; + this.stop(); + this._element = null; + this._isDestroyed = true; }; /** - * Private helpers - * *************** + * Private prototype methods + * ************************* */ /** - * Calculate how many percent the intersection area of two rectangles is from - * the maximum potential intersection area between the rectangles. + * Animation end handler. * - * @param {Rectangle} a - * @param {Rectangle} b - * @returns {Number} - * - A number between 0-100. + * @private */ - function getRectOverlapScore(a, b) { - // Return 0 immediately if the rectangles do not overlap. - if ( - a.left + a.width <= b.left || - b.left + b.width <= a.left || - a.top + a.height <= b.top || - b.top + b.height <= a.top - ) { - return 0; - } + Animator.prototype._onFinish = function () { + var callback = this._callback; + this._animation = this._callback = null; + this._props.length = this._values.length = 0; + callback && callback(); + }; - // Calculate intersection area's width, height, max height and max width. - var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left); - var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top); - var maxWidth = Math.min(a.width, b.width); - var maxHeight = Math.min(a.height, b.height); + /** + * Private helpers + * *************** + */ - return ((width * height) / (maxWidth * maxHeight)) * 100; + function createFrame(props, prefix) { + var frame = {}; + for (var prop in props) { + frame[prefix ? prop : getUnprefixedPropName(prop)] = props[prop]; + } + return frame; } /** - * Check if an element is an anchor element and open the href url if possible. + * Transform translateX and translateY value into CSS transform style + * property's value. * - * @param {HTMLElement} element + * @param {Number} x + * @param {Number} y + * @returns {String} */ - function openAnchorHref(element) { - // Make sure the element is anchor element. - if (element.tagName.toLowerCase() !== 'a') return; - - // Get href and make sure it exists. - var href = element.getAttribute('href'); - if (!href) return; - - // Finally let's navigate to the link href. - var target = element.getAttribute('target'); - if (target && target !== '_self') { - window.open(href, target); - } else { - window.location.href = href; - } + function getTranslateString(x, y) { + return 'translateX(' + x + 'px) translateY(' + y + 'px)'; } /** @@ -3064,25 +4477,29 @@ */ function ItemDragPlaceholder(item) { this._item = item; - this._animate = new ItemAnimate(); + this._animation = new Animator(); this._element = null; this._className = ''; this._didMigrate = false; this._resetAfterLayout = false; - this._currentLeft = 0; - this._currentTop = 0; - this._nextLeft = 0; - this._nextTop = 0; + this._left = 0; + this._top = 0; + this._transX = 0; + this._transY = 0; + this._nextTransX = 0; + this._nextTransY = 0; // Bind animation handlers. this._setupAnimation = this._setupAnimation.bind(this); this._startAnimation = this._startAnimation.bind(this); + this._updateDimensions = this._updateDimensions.bind(this); // Bind event handlers. this._onLayoutStart = this._onLayoutStart.bind(this); this._onLayoutEnd = this._onLayoutEnd.bind(this); this._onReleaseEnd = this._onReleaseEnd.bind(this); this._onMigrate = this._onMigrate.bind(this); + this._onHide = this._onHide.bind(this); } /** @@ -3090,47 +4507,67 @@ * ************************* */ + /** + * Update placeholder's dimensions to match the item's dimensions. + * + * @private + */ + ItemDragPlaceholder.prototype._updateDimensions = function () { + if (!this.isActive()) return; + setStyles(this._element, { + width: this._item._width + 'px', + height: this._item._height + 'px', + }); + }; + /** * Move placeholder to a new position. * * @private - * @memberof ItemDragPlaceholder.prototype + * @param {Item[]} items + * @param {Boolean} isInstant */ - ItemDragPlaceholder.prototype._onLayoutStart = function() { + ItemDragPlaceholder.prototype._onLayoutStart = function (items, isInstant) { var item = this._item; - var grid = item.getGrid(); - // Find out the item's new (unapplied) left and top position. - var itemIndex = grid._items.indexOf(item); - var nextLeft = grid._layout.slots[itemIndex * 2]; - var nextTop = grid._layout.slots[itemIndex * 2 + 1]; + // If the item is not part of the layout anymore reset placeholder. + if (items.indexOf(item) === -1) { + this.reset(); + return; + } + + var nextLeft = item._left; + var nextTop = item._top; + var currentLeft = this._left; + var currentTop = this._top; - // If item's position did not change and the item did not migrate we can - // safely skip layout. - if (!this._didMigrate && item._left === nextLeft && item._top === nextTop) { + // Keep track of item layout position. + this._left = nextLeft; + this._top = nextTop; + + // If item's position did not change, and the item did not migrate and the + // layout is not instant and we can safely skip layout. + if (!isInstant && !this._didMigrate && currentLeft === nextLeft && currentTop === nextTop) { return; } // Slots data is calculated with item margins added to them so we need to add // item's left and top margin to the slot data to get the placeholder's // next position. - nextLeft += item._marginLeft; - nextTop += item._marginTop; + var nextX = nextLeft + item._marginLeft; + var nextY = nextTop + item._marginTop; // Just snap to new position without any animations if no animation is // required or if placeholder moves between grids. - var animEnabled = grid._settings.dragPlaceholder.duration > 0; + var grid = item.getGrid(); + var animEnabled = !isInstant && grid._settings.layoutDuration > 0; if (!animEnabled || this._didMigrate) { // Cancel potential (queued) layout tick. - cancelPlaceholderTick(item._id); + cancelPlaceholderLayoutTick(item._id); // Snap placeholder to correct position. - var targetStyles = { transform: getTranslateString(nextLeft, nextTop) }; - if (this._animate.isAnimating()) { - this._animate.stop(targetStyles); - } else { - setStyles(this._element, targetStyles); - } + this._element.style[transformProp] = getTranslateString(nextX, nextY); + this._animation.stop(); // Move placeholder inside correct container after migration. if (this._didMigrate) { @@ -3143,55 +4580,58 @@ // Start the placeholder's layout animation in the next tick. We do this to // avoid layout thrashing. - this._nextLeft = nextLeft; - this._nextTop = nextTop; - addPlaceholderTick(item._id, this._setupAnimation, this._startAnimation); + this._nextTransX = nextX; + this._nextTransY = nextY; + addPlaceholderLayoutTick(item._id, this._setupAnimation, this._startAnimation); }; /** * Prepare placeholder for layout animation. * * @private - * @memberof ItemDragPlaceholder.prototype */ - ItemDragPlaceholder.prototype._setupAnimation = function() { + ItemDragPlaceholder.prototype._setupAnimation = function () { if (!this.isActive()) return; var translate = getTranslate(this._element); - this._currentLeft = translate.x; - this._currentTop = translate.y; + this._transX = translate.x; + this._transY = translate.y; }; /** * Start layout animation. * * @private - * @memberof ItemDragPlaceholder.prototype */ - ItemDragPlaceholder.prototype._startAnimation = function() { + ItemDragPlaceholder.prototype._startAnimation = function () { if (!this.isActive()) return; - var animation = this._animate; - var currentLeft = this._currentLeft; - var currentTop = this._currentTop; - var nextLeft = this._nextLeft; - var nextTop = this._nextTop; - var targetStyles = { transform: getTranslateString(nextLeft, nextTop) }; + var animation = this._animation; + var currentX = this._transX; + var currentY = this._transY; + var nextX = this._nextTransX; + var nextY = this._nextTransY; // If placeholder is already in correct position let's just stop animation // and be done with it. - if (currentLeft === nextLeft && currentTop === nextTop) { - if (animation.isAnimating()) animation.stop(targetStyles); + if (currentX === nextX && currentY === nextY) { + if (animation.isAnimating()) { + this._element.style[transformProp] = getTranslateString(nextX, nextY); + animation.stop(); + } return; } // Otherwise let's start the animation. - var settings = this._item.getGrid()._settings.dragPlaceholder; - var currentStyles = { transform: getTranslateString(currentLeft, currentTop) }; + var settings = this._item.getGrid()._settings; + var currentStyles = {}; + var targetStyles = {}; + currentStyles[transformProp] = getTranslateString(currentX, currentY); + targetStyles[transformProp] = getTranslateString(nextX, nextY); animation.start(currentStyles, targetStyles, { - duration: settings.duration, - easing: settings.easing, - onFinish: this._onLayoutEnd + duration: settings.layoutDuration, + easing: settings.layoutEasing, + onFinish: this._onLayoutEnd, }); }; @@ -3199,9 +4639,8 @@ * Layout end handler. * * @private - * @memberof ItemDragPlaceholder.prototype */ - ItemDragPlaceholder.prototype._onLayoutEnd = function() { + ItemDragPlaceholder.prototype._onLayoutEnd = function () { if (this._resetAfterLayout) { this.reset(); } @@ -3212,13 +4651,12 @@ * emitted and receives the event data as it's argument. * * @private - * @memberof ItemDragPlaceholder.prototype * @param {Item} item */ - ItemDragPlaceholder.prototype._onReleaseEnd = function(item) { + ItemDragPlaceholder.prototype._onReleaseEnd = function (item) { if (item._id === this._item._id) { // If the placeholder is not animating anymore we can safely reset it. - if (!this._animate.isAnimating()) { + if (!this._animation.isAnimating()) { this.reset(); return; } @@ -3234,7 +4672,6 @@ * emitted and receives the event data as it's argument. * * @private - * @memberof ItemDragPlaceholder.prototype * @param {Object} data * @param {Item} data.item * @param {Grid} data.fromGrid @@ -3242,7 +4679,7 @@ * @param {Grid} data.toGrid * @param {Number} data.toIndex */ - ItemDragPlaceholder.prototype._onMigrate = function(data) { + ItemDragPlaceholder.prototype._onMigrate = function (data) { // Make sure we have a matching item. if (data.item !== this._item) return; @@ -3250,19 +4687,31 @@ var nextGrid = data.toGrid; // Unbind listeners from current grid. - grid.off(eventDragReleaseEnd, this._onReleaseEnd); - grid.off(eventLayoutStart, this._onLayoutStart); - grid.off(eventBeforeSend, this._onMigrate); + grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + grid.off(EVENT_LAYOUT_START, this._onLayoutStart); + grid.off(EVENT_BEFORE_SEND, this._onMigrate); + grid.off(EVENT_HIDE_START, this._onHide); // Bind listeners to the next grid. - nextGrid.on(eventDragReleaseEnd, this._onReleaseEnd); - nextGrid.on(eventLayoutStart, this._onLayoutStart); - nextGrid.on(eventBeforeSend, this._onMigrate); + nextGrid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + nextGrid.on(EVENT_LAYOUT_START, this._onLayoutStart); + nextGrid.on(EVENT_BEFORE_SEND, this._onMigrate); + nextGrid.on(EVENT_HIDE_START, this._onHide); // Mark the item as migrated. this._didMigrate = true; }; + /** + * Reset placeholder if the associated item is hidden. + * + * @private + * @param {Item[]} items + */ + ItemDragPlaceholder.prototype._onHide = function (items) { + if (items.indexOf(this._item) > -1) this.reset(); + }; + /** * Public prototype methods * ************************ @@ -3274,9 +4723,8 @@ * thrashing when it's called at the end of the drag start procedure. * * @public - * @memberof ItemDragPlaceholder.prototype */ - ItemDragPlaceholder.prototype.create = function() { + ItemDragPlaceholder.prototype.create = function () { // If we already have placeholder set up we can skip the initiation logic. if (this.isActive()) { this._resetAfterLayout = false; @@ -3286,14 +4734,18 @@ var item = this._item; var grid = item.getGrid(); var settings = grid._settings; - var animation = this._animate; + var animation = this._animation; + + // Keep track of layout position. + this._left = item._left; + this._top = item._top; // Create placeholder element. var element; if (isFunction(settings.dragPlaceholder.createElement)) { element = settings.dragPlaceholder.createElement(item); } else { - element = window.document.createElement('div'); + element = document.createElement('div'); } this._element = element; @@ -3306,23 +4758,26 @@ addClass(element, this._className); } - // Position the placeholder item correctly. - var left = item._left + item._marginLeft; - var top = item._top + item._marginTop; + // Set initial styles. setStyles(element, { - display: 'block', position: 'absolute', - left: '0', - top: '0', + left: '0px', + top: '0px', width: item._width + 'px', height: item._height + 'px', - transform: getTranslateString(left, top) }); + // Set initial position. + element.style[transformProp] = getTranslateString( + item._left + item._marginLeft, + item._top + item._marginTop + ); + // Bind event listeners. - grid.on(eventLayoutStart, this._onLayoutStart); - grid.on(eventDragReleaseEnd, this._onReleaseEnd); - grid.on(eventBeforeSend, this._onMigrate); + grid.on(EVENT_LAYOUT_START, this._onLayoutStart); + grid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + grid.on(EVENT_BEFORE_SEND, this._onMigrate); + grid.on(EVENT_HIDE_START, this._onHide); // onCreate hook. if (isFunction(settings.dragPlaceholder.onCreate)) { @@ -3337,31 +4792,32 @@ * Reset placeholder data. * * @public - * @memberof ItemDragPlaceholder.prototype */ - ItemDragPlaceholder.prototype.reset = function() { + ItemDragPlaceholder.prototype.reset = function () { if (!this.isActive()) return; var element = this._element; var item = this._item; var grid = item.getGrid(); var settings = grid._settings; - var animation = this._animate; + var animation = this._animation; // Reset flag. this._resetAfterLayout = false; // Cancel potential (queued) layout tick. - cancelPlaceholderTick(item._id); + cancelPlaceholderLayoutTick(item._id); + cancelPlaceholderResizeTick(item._id); // Reset animation instance. animation.stop(); animation._element = null; // Unbind event listeners. - grid.off(eventDragReleaseEnd, this._onReleaseEnd); - grid.off(eventLayoutStart, this._onLayoutStart); - grid.off(eventBeforeSend, this._onMigrate); + grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + grid.off(EVENT_LAYOUT_START, this._onLayoutStart); + grid.off(EVENT_BEFORE_SEND, this._onMigrate); + grid.off(EVENT_HIDE_START, this._onHide); // Remove placeholder class from the placeholder element. if (this._className) { @@ -3382,53 +4838,64 @@ }; /** - * Update placeholder's dimensions. + * Check if placeholder is currently active (visible). * * @public - * @memberof ItemDragPlaceholder.prototype - * @param {Number} width - * @param {height} height + * @returns {Boolean} */ - ItemDragPlaceholder.prototype.updateDimensions = function(width, height) { - if (this.isActive()) { - setStyles(this._element, { - width: width + 'px', - height: height + 'px' - }); - } + ItemDragPlaceholder.prototype.isActive = function () { + return !!this._element; }; /** - * Check if placeholder is currently active (visible). + * Get placeholder element. * * @public - * @memberof ItemDragPlaceholder.prototype - * @returns {Boolean} + * @returns {?HTMLElement} */ - ItemDragPlaceholder.prototype.isActive = function() { - return !!this._element; + ItemDragPlaceholder.prototype.getElement = function () { + return this._element; + }; + + /** + * Update placeholder's dimensions to match the item's dimensions. Note that + * the updating is done asynchronously in the next tick to avoid layout + * thrashing. + * + * @public + */ + ItemDragPlaceholder.prototype.updateDimensions = function () { + if (!this.isActive()) return; + addPlaceholderResizeTick(this._item._id, this._updateDimensions); }; /** * Destroy placeholder instance. * * @public - * @memberof ItemDragPlaceholder.prototype */ - ItemDragPlaceholder.prototype.destroy = function() { + ItemDragPlaceholder.prototype.destroy = function () { this.reset(); - this._animate.destroy(); - this._item = this._animate = null; + this._animation.destroy(); + this._item = this._animation = null; }; /** - * Queue constructor. + * The release process handler constructor. Although this might seem as proper + * fit for the drag process this needs to be separated into it's own logic + * because there might be a scenario where drag is disabled, but the release + * process still needs to be implemented (dragging from a grid to another). * * @class + * @param {Item} item */ - function Queue() { - this._queue = []; + function ItemDragRelease(item) { + this._item = item; + this._isActive = false; this._isDestroyed = false; + this._isPositioningStarted = false; + this._containerDiffX = 0; + this._containerDiffY = 0; } /** @@ -3437,97 +4904,181 @@ */ /** - * Add callback to the queue. + * Start the release process of an item. * * @public - * @memberof Queue.prototype - * @param {Function} callback - * @returns {Queue} */ - Queue.prototype.add = function(callback) { - if (this._isDestroyed) return this; - this._queue.push(callback); - return this; + ItemDragRelease.prototype.start = function () { + if (this._isDestroyed || this._isActive) return; + + var item = this._item; + var grid = item.getGrid(); + var settings = grid._settings; + + this._isActive = true; + addClass(item._element, settings.itemReleasingClass); + if (!settings.dragRelease.useDragContainer) { + this._placeToGrid(); + } + grid._emit(EVENT_DRAG_RELEASE_START, item); + + // Let's start layout manually _only_ if there is no unfinished layout in + // about to finish. + if (!grid._nextLayoutData) item._layout.start(false); + }; + + /** + * End the release process of an item. This method can be used to abort an + * ongoing release process (animation) or finish the release process. + * + * @public + * @param {Boolean} [abort=false] + * - Should the release be aborted? When true, the release end event won't be + * emitted. Set to true only when you need to abort the release process + * while the item is animating to it's position. + * @param {Number} [left] + * - The element's current translateX value (optional). + * @param {Number} [top] + * - The element's current translateY value (optional). + */ + ItemDragRelease.prototype.stop = function (abort, left, top) { + if (this._isDestroyed || !this._isActive) return; + + var item = this._item; + var grid = item.getGrid(); + + if (!abort && (left === undefined || top === undefined)) { + left = item._left; + top = item._top; + } + + var didReparent = this._placeToGrid(left, top); + this._reset(didReparent); + + if (!abort) grid._emit(EVENT_DRAG_RELEASE_END, item); + }; + + ItemDragRelease.prototype.isJustReleased = function () { + return this._isActive && this._isPositioningStarted === false; }; /** - * Process queue callbacks and reset the queue. + * Destroy instance. * * @public - * @memberof Queue.prototype - * @param {*} arg1 - * @param {*} arg2 - * @returns {Queue} */ - Queue.prototype.flush = function(arg1, arg2) { - if (this._isDestroyed) return this; + ItemDragRelease.prototype.destroy = function () { + if (this._isDestroyed) return; + this.stop(true); + this._item = null; + this._isDestroyed = true; + }; - var queue = this._queue; - var length = queue.length; - var i; + /** + * Private prototype methods + * ************************* + */ - // Quit early if the queue is empty. - if (!length) return this; + /** + * Move the element back to the grid container element if it does not exist + * there already. + * + * @private + * @param {Number} [left] + * - The element's current translateX value (optional). + * @param {Number} [top] + * - The element's current translateY value (optional). + * @returns {Boolean} + * - Returns `true` if the element was reparented. + */ + ItemDragRelease.prototype._placeToGrid = function (left, top) { + if (this._isDestroyed) return; - var singleCallback = length === 1; - var snapshot = singleCallback ? queue[0] : queue.slice(0); + var item = this._item; + var element = item._element; + var container = item.getGrid()._element; + var didReparent = false; - // Reset queue. - queue.length = 0; + if (element.parentNode !== container) { + if (left === undefined || top === undefined) { + var translate = getTranslate(element); + left = translate.x - this._containerDiffX; + top = translate.y - this._containerDiffY; + } - // If we only have a single callback let's just call it. - if (singleCallback) { - snapshot(arg1, arg2); - return this; + container.appendChild(element); + item._setTranslate(left, top); + didReparent = true; } - // If we have multiple callbacks, let's process them. - for (i = 0; i < length; i++) { - snapshot[i](arg1, arg2); - if (this._isDestroyed) break; - } + this._containerDiffX = 0; + this._containerDiffY = 0; - return this; + return didReparent; }; /** - * Destroy Queue instance. + * Reset data and remove releasing class. * - * @public - * @memberof Queue.prototype - * @returns {Queue} + * @private + * @param {Boolean} [needsReflow] */ - Queue.prototype.destroy = function() { - if (this._isDestroyed) return this; + ItemDragRelease.prototype._reset = function (needsReflow) { + if (this._isDestroyed) return; - this._isDestroyed = true; - this._queue.length = 0; + var item = this._item; + var releasingClass = item.getGrid()._settings.itemReleasingClass; - return this; + this._isActive = false; + this._isPositioningStarted = false; + this._containerDiffX = 0; + this._containerDiffY = 0; + + // If the element was just reparented we need to do a forced reflow to remove + // the class gracefully. + if (releasingClass) { + // eslint-disable-next-line + if (needsReflow) item._element.clientWidth; + removeClass(item._element, releasingClass); + } }; + var MIN_ANIMATION_DISTANCE = 2; + /** - * Layout manager for Item instance. + * Layout manager for Item instance, handles the positioning of an item. * * @class * @param {Item} item */ function ItemLayout(item) { + var element = item._element; + var elementStyle = element.style; + this._item = item; this._isActive = false; this._isDestroyed = false; this._isInterrupted = false; this._currentStyles = {}; this._targetStyles = {}; - this._currentLeft = 0; - this._currentTop = 0; + this._nextLeft = 0; + this._nextTop = 0; this._offsetLeft = 0; this._offsetTop = 0; this._skipNextAnimation = false; - this._animateOptions = { - onFinish: this._finish.bind(this) + this._animOptions = { + onFinish: this._finish.bind(this), + duration: 0, + easing: 0, }; - this._queue = new Queue(); + + // Set element's initial position styles. + elementStyle.left = '0px'; + elementStyle.top = '0px'; + item._setTranslate(0, 0); + + this._animation = new Animator(element); + this._queue = 'layout-' + item._id; // Bind animation handlers and finish method. this._setupAnimation = this._setupAnimation.bind(this); @@ -3543,72 +5094,68 @@ * Start item layout based on it's current data. * * @public - * @memberof ItemLayout.prototype - * @param {Boolean} [instant=false] + * @param {Boolean} instant * @param {Function} [onFinish] - * @returns {ItemLayout} */ - ItemLayout.prototype.start = function(instant, onFinish) { + ItemLayout.prototype.start = function (instant, onFinish) { if (this._isDestroyed) return; var item = this._item; - var element = item._element; - var release = item._release; + var release = item._dragRelease; var gridSettings = item.getGrid()._settings; var isPositioning = this._isActive; - var isJustReleased = release._isActive && release._isPositioningStarted === false; + var isJustReleased = release.isJustReleased(); var animDuration = isJustReleased - ? gridSettings.dragReleaseDuration + ? gridSettings.dragRelease.duration : gridSettings.layoutDuration; - var animEasing = isJustReleased ? gridSettings.dragReleaseEasing : gridSettings.layoutEasing; + var animEasing = isJustReleased ? gridSettings.dragRelease.easing : gridSettings.layoutEasing; var animEnabled = !instant && !this._skipNextAnimation && animDuration > 0; - var isAnimating; - // If the item is currently positioning process current layout callback - // queue with interrupted flag on. - if (isPositioning) this._queue.flush(true, item); + // If the item is currently positioning cancel potential queued layout tick + // and process current layout callback queue with interrupted flag on. + if (isPositioning) { + cancelLayoutTick(item._id); + item._emitter.burst(this._queue, true, item); + } // Mark release positioning as started. if (isJustReleased) release._isPositioningStarted = true; // Push the callback to the callback queue. - if (isFunction(onFinish)) this._queue.add(onFinish); + if (isFunction(onFinish)) { + item._emitter.on(this._queue, onFinish); + } + + // Reset animation skipping flag. + this._skipNextAnimation = false; // If no animations are needed, easy peasy! if (!animEnabled) { this._updateOffsets(); - this._updateTargetStyles(); - isAnimating = item._animate.isAnimating(); - this.stop(false, this._targetStyles); - !isAnimating && setStyles(element, this._targetStyles); - this._skipNextAnimation = false; - return this._finish(); + item._setTranslate(this._nextLeft, this._nextTop); + this._animation.stop(); + this._finish(); + return; } - // Set item active and store some data for the animation that is about to be - // triggered. + // Kick off animation to be started in the next tick. this._isActive = true; - this._animateOptions.easing = animEasing; - this._animateOptions.duration = animDuration; + this._animOptions.easing = animEasing; + this._animOptions.duration = animDuration; this._isInterrupted = isPositioning; - - // Start the item's layout animation in the next tick. addLayoutTick(item._id, this._setupAnimation, this._startAnimation); - - return this; }; /** * Stop item's position animation if it is currently animating. * * @public - * @memberof ItemLayout.prototype - * @param {Boolean} [processCallbackQueue=false] - * @param {Object} [targetStyles] - * @returns {ItemLayout} + * @param {Boolean} processCallbackQueue + * @param {Number} [left] + * @param {Number} [top] */ - ItemLayout.prototype.stop = function(processCallbackQueue, targetStyles) { - if (this._isDestroyed || !this._isActive) return this; + ItemLayout.prototype.stop = function (processCallbackQueue, left, top) { + if (this._isDestroyed || !this._isActive) return; var item = this._item; @@ -3616,7 +5163,15 @@ cancelLayoutTick(item._id); // Stop animation. - item._animate.stop(targetStyles); + if (this._animation.isAnimating()) { + if (left === undefined || top === undefined) { + var translate = getTranslate(item._element); + left = translate.x; + top = translate.y; + } + item._setTranslate(left, top); + this._animation.stop(); + } // Remove positioning class. removeClass(item._element, item.getGrid()._settings.itemPositioningClass); @@ -3625,25 +5180,34 @@ this._isActive = false; // Process callback queue if needed. - if (processCallbackQueue) this._queue.flush(true, item); - - return this; + if (processCallbackQueue) { + item._emitter.burst(this._queue, true, item); + } }; /** * Destroy the instance and stop current animation if it is running. * * @public - * @memberof ItemLayout.prototype - * @returns {ItemLayout} */ - ItemLayout.prototype.destroy = function() { - if (this._isDestroyed) return this; - this.stop(true, {}); - this._queue.destroy(); - this._item = this._currentStyles = this._targetStyles = this._animateOptions = null; + ItemLayout.prototype.destroy = function () { + if (this._isDestroyed) return; + + var elementStyle = this._item._element.style; + + this.stop(true, 0, 0); + this._item._emitter.clear(this._queue); + this._animation.destroy(); + + elementStyle[transformProp] = ''; + elementStyle.left = ''; + elementStyle.top = ''; + + this._item = null; + this._currentStyles = null; + this._targetStyles = null; + this._animOptions = null; this._isDestroyed = true; - return this; }; /** @@ -3655,14 +5219,13 @@ * Calculate and update item's current layout offset data. * * @private - * @memberof ItemLayout.prototype */ - ItemLayout.prototype._updateOffsets = function() { + ItemLayout.prototype._updateOffsets = function () { if (this._isDestroyed) return; var item = this._item; var migrate = item._migrate; - var release = item._release; + var release = item._dragRelease; this._offsetLeft = release._isActive ? release._containerDiffX @@ -3675,34 +5238,26 @@ : migrate._isActive ? migrate._containerDiffY : 0; - }; - /** - * Calculate and update item's layout target styles. - * - * @private - * @memberof ItemLayout.prototype - */ - ItemLayout.prototype._updateTargetStyles = function() { - if (this._isDestroyed) return; - this._targetStyles.transform = getTranslateString( - this._item._left + this._offsetLeft, - this._item._top + this._offsetTop - ); + this._nextLeft = this._item._left + this._offsetLeft; + this._nextTop = this._item._top + this._offsetTop; }; /** * Finish item layout procedure. * * @private - * @memberof ItemLayout.prototype */ - ItemLayout.prototype._finish = function() { + ItemLayout.prototype._finish = function () { if (this._isDestroyed) return; var item = this._item; var migrate = item._migrate; - var release = item._release; + var release = item._dragRelease; + + // Update internal translate values. + item._tX = this._nextLeft; + item._tY = this._nextTop; // Mark the item as inactive and remove positioning classes. if (this._isActive) { @@ -3715,42 +5270,46 @@ if (migrate._isActive) migrate.stop(); // Process the callback queue. - this._queue.flush(false, item); + item._emitter.burst(this._queue, false, item); }; /** * Prepare item for layout animation. * * @private - * @memberof ItemLayout.prototype */ - ItemLayout.prototype._setupAnimation = function() { - var translate = getTranslate(this._item._element); - this._currentLeft = translate.x; - this._currentTop = translate.y; + ItemLayout.prototype._setupAnimation = function () { + var item = this._item; + if (item._tX === undefined || item._tY === undefined) { + var translate = getTranslate(item._element); + item._tX = translate.x; + item._tY = translate.y; + } }; /** * Start layout animation. * * @private - * @memberof ItemLayout.prototype */ - ItemLayout.prototype._startAnimation = function() { + ItemLayout.prototype._startAnimation = function () { var item = this._item; var settings = item.getGrid()._settings; + var isInstant = this._animOptions.duration <= 0; // Let's update the offset data and target styles. this._updateOffsets(); - this._updateTargetStyles(); - // If the item is already in correct position let's quit early. - if ( - item._left === this._currentLeft - this._offsetLeft && - item._top === this._currentTop - this._offsetTop - ) { - if (this._isInterrupted) this.stop(false, this._targetStyles); - this._isActive = false; + var xDiff = Math.abs(item._left - (item._tX - this._offsetLeft)); + var yDiff = Math.abs(item._top - (item._tY - this._offsetTop)); + + // If there is no need for animation or if the item is already in correct + // position (or near it) let's finish the process early. + if (isInstant || (xDiff < MIN_ANIMATION_DISTANCE && yDiff < MIN_ANIMATION_DISTANCE)) { + if (xDiff || yDiff || this._isInterrupted) { + item._setTranslate(this._nextLeft, this._nextTop); + } + this._animation.stop(); this._finish(); return; } @@ -3760,14 +5319,19 @@ addClass(item._element, settings.itemPositioningClass); } - // Get current styles for animation. - this._currentStyles.transform = getTranslateString(this._currentLeft, this._currentTop); + // Get current/next styles for animation. + this._currentStyles[transformProp] = getTranslateString(item._tX, item._tY); + this._targetStyles[transformProp] = getTranslateString(this._nextLeft, this._nextTop); - // Animate. - item._animate.start(this._currentStyles, this._targetStyles, this._animateOptions); - }; + // Set internal translation values to undefined for the duration of the + // animation since they will be changing on each animation frame for the + // duration of the animation and tracking them would mean reading the DOM on + // each frame, which is pretty darn expensive. + item._tX = item._tY = undefined; - var tempStyles = {}; + // Start animation. + this._animation.start(this._currentStyles, this._targetStyles, this._animOptions); + }; /** * The migrate process handler constructor. @@ -3794,17 +5358,16 @@ * Start the migrate process of an item. * * @public - * @memberof ItemMigrate.prototype * @param {Grid} targetGrid - * @param {GridSingleItemQuery} position + * @param {(HTMLElement|Number|Item)} position * @param {HTMLElement} [container] - * @returns {ItemMigrate} */ - ItemMigrate.prototype.start = function(targetGrid, position, container) { - if (this._isDestroyed) return this; + ItemMigrate.prototype.start = function (targetGrid, position, container) { + if (this._isDestroyed) return; var item = this._item; var element = item._element; + var isActive = item.isActive(); var isVisible = item.isVisible(); var grid = item.getGrid(); var settings = grid._settings; @@ -3812,7 +5375,7 @@ var targetElement = targetGrid._element; var targetItems = targetGrid._items; var currentIndex = grid._items.indexOf(item); - var targetContainer = container || window.document.body; + var targetContainer = container || document.body; var targetIndex; var targetItem; var currentContainer; @@ -3821,14 +5384,15 @@ var translate; var translateX; var translateY; + var currentVisClass; + var nextVisClass; // Get target index. if (typeof position === 'number') { - targetIndex = normalizeArrayIndex(targetItems, position, true); + targetIndex = normalizeArrayIndex(targetItems, position, 1); } else { - targetItem = targetGrid._getItem(position); - /** @todo Consider throwing an error here instead of silently failing. */ - if (!targetItem) return this; + targetItem = targetGrid.getItem(position); + if (!targetItem) return; targetIndex = targetItems.indexOf(targetItem); } @@ -3841,62 +5405,64 @@ // Abort current positioning. if (item.isPositioning()) { - item._layout.stop(true, { transform: getTranslateString(translateX, translateY) }); + item._layout.stop(true, translateX, translateY); } // Abort current migration. if (this._isActive) { translateX -= this._containerDiffX; translateY -= this._containerDiffY; - this.stop(true, { transform: getTranslateString(translateX, translateY) }); + this.stop(true, translateX, translateY); } // Abort current release. if (item.isReleasing()) { - translateX -= item._release._containerDiffX; - translateY -= item._release._containerDiffY; - item._release.stop(true, { transform: getTranslateString(translateX, translateY) }); + translateX -= item._dragRelease._containerDiffX; + translateY -= item._dragRelease._containerDiffY; + item._dragRelease.stop(true, translateX, translateY); } - // Stop current visibility animations. - item._visibility._stopAnimation(); + // Stop current visibility animation. + item._visibility.stop(true); // Destroy current drag. if (item._drag) item._drag.destroy(); - // Process current visibility animation queue. - item._visibility._queue.flush(true, item); - // Emit beforeSend event. - if (grid._hasListeners(eventBeforeSend)) { - grid._emit(eventBeforeSend, { + if (grid._hasListeners(EVENT_BEFORE_SEND)) { + grid._emit(EVENT_BEFORE_SEND, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } // Emit beforeReceive event. - if (targetGrid._hasListeners(eventBeforeReceive)) { - targetGrid._emit(eventBeforeReceive, { + if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) { + targetGrid._emit(EVENT_BEFORE_RECEIVE, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } - // Remove current classnames. - removeClass(element, settings.itemClass); - removeClass(element, settings.itemVisibleClass); - removeClass(element, settings.itemHiddenClass); + // Update item class. + if (settings.itemClass !== targetSettings.itemClass) { + removeClass(element, settings.itemClass); + addClass(element, targetSettings.itemClass); + } - // Add new classnames. - addClass(element, targetSettings.itemClass); - addClass(element, isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass); + // Update visibility class. + currentVisClass = isVisible ? settings.itemVisibleClass : settings.itemHiddenClass; + nextVisClass = isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass; + if (currentVisClass !== nextVisClass) { + removeClass(element, currentVisClass); + addClass(element, nextVisClass); + } // Move item instance from current grid to target grid. grid._items.splice(currentIndex, 1); @@ -3905,71 +5471,80 @@ // Update item's grid id reference. item._gridId = targetGrid._id; - // Get current container. - currentContainer = element.parentNode; - - // Move the item inside the target container if it's different than the + // If item is active we need to move the item inside the target container for + // the duration of the (potential) animation if it's different than the // current container. - if (targetContainer !== currentContainer) { - targetContainer.appendChild(element); - offsetDiff = getOffsetDiff(targetContainer, currentContainer, true); - if (!translate) { - translate = getTranslate(element); - translateX = translate.x; - translateY = translate.y; - } - element.style[transformProp] = getTranslateString( - translateX + offsetDiff.left, - translateY + offsetDiff.top - ); + if (isActive) { + currentContainer = element.parentNode; + if (targetContainer !== currentContainer) { + targetContainer.appendChild(element); + offsetDiff = getOffsetDiff(targetContainer, currentContainer, true); + if (!translate) { + translate = getTranslate(element); + translateX = translate.x; + translateY = translate.y; + } + item._setTranslate(translateX + offsetDiff.left, translateY + offsetDiff.top); + } + } + // If item is not active let's just append it to the target grid's element. + else { + targetElement.appendChild(element); } // Update child element's styles to reflect the current visibility state. - item._child.removeAttribute('style'); - setStyles(item._child, isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles); - - // Update display style. - element.style.display = isVisible ? 'block' : 'hidden'; + item._visibility.setStyles( + isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles + ); - // Get offset diff for the migration data. - containerDiff = getOffsetDiff(targetContainer, targetElement, true); + // Get offset diff for the migration data, if the item is active. + if (isActive) { + containerDiff = getOffsetDiff(targetContainer, targetElement, true); + } - // Update item's cached dimensions and sort data. + // Update item's cached dimensions. item._refreshDimensions(); - item._refreshSortData(); + + // Reset item's sort data. + item._sortData = null; // Create new drag handler. item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null; // Setup migration data. - this._isActive = true; - this._container = targetContainer; - this._containerDiffX = containerDiff.left; - this._containerDiffY = containerDiff.top; + if (isActive) { + this._isActive = true; + this._container = targetContainer; + this._containerDiffX = containerDiff.left; + this._containerDiffY = containerDiff.top; + } else { + this._isActive = false; + this._container = null; + this._containerDiffX = 0; + this._containerDiffY = 0; + } // Emit send event. - if (grid._hasListeners(eventSend)) { - grid._emit(eventSend, { + if (grid._hasListeners(EVENT_SEND)) { + grid._emit(EVENT_SEND, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } // Emit receive event. - if (targetGrid._hasListeners(eventReceive)) { - targetGrid._emit(eventReceive, { + if (targetGrid._hasListeners(EVENT_RECEIVE)) { + targetGrid._emit(EVENT_RECEIVE, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } - - return this; }; /** @@ -3977,15 +5552,15 @@ * ongoing migrate process (animation) or finish the migrate process. * * @public - * @memberof ItemMigrate.prototype * @param {Boolean} [abort=false] * - Should the migration be aborted? - * @param {Object} [currentStyles] - * - Optional current translateX and translateY styles. - * @returns {ItemMigrate} + * @param {Number} [left] + * - The element's current translateX value (optional). + * @param {Number} [top] + * - The element's current translateY value (optional). */ - ItemMigrate.prototype.stop = function(abort, currentStyles) { - if (this._isDestroyed || !this._isActive) return this; + ItemMigrate.prototype.stop = function (abort, left, top) { + if (this._isDestroyed || !this._isActive) return; var item = this._item; var element = item._element; @@ -3994,202 +5569,41 @@ var translate; if (this._container !== gridElement) { - if (!currentStyles) { + if (left === undefined || top === undefined) { if (abort) { translate = getTranslate(element); - tempStyles.transform = getTranslateString( - translate.x - this._containerDiffX, - translate.y - this._containerDiffY - ); + left = translate.x - this._containerDiffX; + top = translate.y - this._containerDiffY; } else { - tempStyles.transform = getTranslateString(item._left, item._top); + left = item._left; + top = item._top; } - currentStyles = tempStyles; } + gridElement.appendChild(element); - setStyles(element, currentStyles); + item._setTranslate(left, top); } this._isActive = false; this._container = null; this._containerDiffX = 0; this._containerDiffY = 0; - - return this; - }; - - /** - * Destroy instance. - * - * @public - * @memberof ItemMigrate.prototype - * @returns {ItemMigrate} - */ - ItemMigrate.prototype.destroy = function() { - if (this._isDestroyed) return this; - this.stop(true); - this._item = null; - this._isDestroyed = true; - return this; - }; - - var tempStyles$1 = {}; - - /** - * The release process handler constructor. Although this might seem as proper - * fit for the drag process this needs to be separated into it's own logic - * because there might be a scenario where drag is disabled, but the release - * process still needs to be implemented (dragging from a grid to another). - * - * @class - * @param {Item} item - */ - function ItemRelease(item) { - this._item = item; - this._isActive = false; - this._isDestroyed = false; - this._isPositioningStarted = false; - this._containerDiffX = 0; - this._containerDiffY = 0; - } - - /** - * Public prototype methods - * ************************ - */ - - /** - * Start the release process of an item. - * - * @public - * @memberof ItemRelease.prototype - * @returns {ItemRelease} - */ - ItemRelease.prototype.start = function() { - if (this._isDestroyed || this._isActive) return this; - - var item = this._item; - var grid = item.getGrid(); - - // Flag release as active. - this._isActive = true; - - // Add release class name to the released element. - addClass(item._element, grid._settings.itemReleasingClass); - - // Emit dragReleaseStart event. - grid._emit(eventDragReleaseStart, item); - - // Position the released item. - item._layout.start(false); - - return this; - }; - - /** - * End the release process of an item. This method can be used to abort an - * ongoing release process (animation) or finish the release process. - * - * @public - * @memberof ItemRelease.prototype - * @param {Boolean} [abort=false] - * - Should the release be aborted? When true, the release end event won't be - * emitted. Set to true only when you need to abort the release process - * while the item is animating to it's position. - * @param {Object} [currentStyles] - * - Optional current translateX and translateY styles. - * @returns {ItemRelease} - */ - ItemRelease.prototype.stop = function(abort, currentStyles) { - if (this._isDestroyed || !this._isActive) return this; - - var item = this._item; - var element = item._element; - var grid = item.getGrid(); - var container = grid._element; - var translate; - - // Reset data and remove releasing class name from the element. - this._reset(); - - // If the released element is outside the grid's container element put it - // back there and adjust position accordingly. - if (element.parentNode !== container) { - if (!currentStyles) { - if (abort) { - translate = getTranslate(element); - tempStyles$1.transform = getTranslateString( - translate.x - this._containerDiffX, - translate.y - this._containerDiffY - ); - } else { - tempStyles$1.transform = getTranslateString(item._left, item._top); - } - currentStyles = tempStyles$1; - } - container.appendChild(element); - setStyles(element, currentStyles); - } - - // Emit dragReleaseEnd event. - if (!abort) grid._emit(eventDragReleaseEnd, item); - - return this; }; /** * Destroy instance. * * @public - * @memberof ItemRelease.prototype - * @returns {ItemRelease} */ - ItemRelease.prototype.destroy = function() { - if (this._isDestroyed) return this; + ItemMigrate.prototype.destroy = function () { + if (this._isDestroyed) return; this.stop(true); this._item = null; this._isDestroyed = true; - return this; - }; - - /** - * Private prototype methods - * ************************* - */ - - /** - * Reset public data and remove releasing class. - * - * @private - * @memberof ItemRelease.prototype - */ - ItemRelease.prototype._reset = function() { - if (this._isDestroyed) return; - var item = this._item; - this._isActive = false; - this._isPositioningStarted = false; - this._containerDiffX = 0; - this._containerDiffY = 0; - removeClass(item._element, item.getGrid()._settings.itemReleasingClass); }; /** - * Get current values of the provided styles definition object. - * - * @param {HTMLElement} element - * @param {Object} styles - * @return {Object} - */ - function getCurrentStyles(element, styles) { - var current = {}; - for (var prop in styles) { - current[prop] = getStyle(element, getStyleName(prop)); - } - return current; - } - - /** - * Visibility manager for Item instance. + * Visibility manager for Item instance, handles visibility of an item. * * @class * @param {Item} item @@ -4197,31 +5611,28 @@ function ItemVisibility(item) { var isActive = item._isActive; var element = item._element; + var childElement = element.children[0]; var settings = item.getGrid()._settings; + if (!childElement) { + throw new Error('No valid child element found within item element.'); + } + this._item = item; this._isDestroyed = false; - - // Set up visibility states. this._isHidden = !isActive; this._isHiding = false; this._isShowing = false; - - // Callback queue. - this._queue = new Queue(); - - // Bind show/hide finishers. + this._childElement = childElement; + this._currentStyleProps = []; + this._animation = new Animator(childElement); + this._queue = 'visibility-' + item._id; this._finishShow = this._finishShow.bind(this); this._finishHide = this._finishHide.bind(this); - // Force item to be either visible or hidden on init. - element.style.display = isActive ? 'block' : 'none'; - - // Set visible/hidden class. + element.style.display = isActive ? '' : 'none'; addClass(element, isActive ? settings.itemVisibleClass : settings.itemHiddenClass); - - // Set initial styles for the child element. - setStyles(item._child, isActive ? settings.visibleStyles : settings.hiddenStyles); + this.setStyles(isActive ? settings.visibleStyles : settings.hiddenStyles); } /** @@ -4233,17 +5644,14 @@ * Show item. * * @public - * @memberof ItemVisibility.prototype * @param {Boolean} instant * @param {Function} [onFinish] - * @returns {ItemVisibility} */ - ItemVisibility.prototype.show = function(instant, onFinish) { - if (this._isDestroyed) return this; + ItemVisibility.prototype.show = function (instant, onFinish) { + if (this._isDestroyed) return; var item = this._item; var element = item._element; - var queue = this._queue; var callback = isFunction(onFinish) ? onFinish : null; var grid = item.getGrid(); var settings = grid._settings; @@ -4251,54 +5659,49 @@ // If item is visible call the callback and be done with it. if (!this._isShowing && !this._isHidden) { callback && callback(false, item); - return this; + return; } // If item is showing and does not need to be shown instantly, let's just // push callback to the callback queue and be done with it. if (this._isShowing && !instant) { - callback && queue.add(callback); - return this; + callback && item._emitter.on(this._queue, callback); + return; } // If the item is hiding or hidden process the current visibility callback // queue with the interrupted flag active, update classes and set display // to block if necessary. if (!this._isShowing) { - queue.flush(true, item); + item._emitter.burst(this._queue, true, item); removeClass(element, settings.itemHiddenClass); addClass(element, settings.itemVisibleClass); - if (!this._isHiding) element.style.display = 'block'; + if (!this._isHiding) element.style.display = ''; } // Push callback to the callback queue. - callback && queue.add(callback); + callback && item._emitter.on(this._queue, callback); // Update visibility states. - item._isActive = this._isShowing = true; + this._isShowing = true; this._isHiding = this._isHidden = false; // Finally let's start show animation. this._startAnimation(true, instant, this._finishShow); - - return this; }; /** * Hide item. * * @public - * @memberof ItemVisibility.prototype * @param {Boolean} instant * @param {Function} [onFinish] - * @returns {ItemVisibility} */ - ItemVisibility.prototype.hide = function(instant, onFinish) { - if (this._isDestroyed) return this; + ItemVisibility.prototype.hide = function (instant, onFinish) { + if (this._isDestroyed) return; var item = this._item; var element = item._element; - var queue = this._queue; var callback = isFunction(onFinish) ? onFinish : null; var grid = item.getGrid(); var settings = grid._settings; @@ -4306,70 +5709,98 @@ // If item is already hidden call the callback and be done with it. if (!this._isHiding && this._isHidden) { callback && callback(false, item); - return this; + return; } // If item is hiding and does not need to be hidden instantly, let's just // push callback to the callback queue and be done with it. if (this._isHiding && !instant) { - callback && queue.add(callback); - return this; + callback && item._emitter.on(this._queue, callback); + return; } // If the item is showing or visible process the current visibility callback // queue with the interrupted flag active, update classes and set display // to block if necessary. if (!this._isHiding) { - queue.flush(true, item); + item._emitter.burst(this._queue, true, item); addClass(element, settings.itemHiddenClass); removeClass(element, settings.itemVisibleClass); } // Push callback to the callback queue. - callback && queue.add(callback); + callback && item._emitter.on(this._queue, callback); // Update visibility states. this._isHidden = this._isHiding = true; - item._isActive = this._isShowing = false; + this._isShowing = false; // Finally let's start hide animation. this._startAnimation(false, instant, this._finishHide); + }; + + /** + * Stop current hiding/showing process. + * + * @public + * @param {Boolean} processCallbackQueue + */ + ItemVisibility.prototype.stop = function (processCallbackQueue) { + if (this._isDestroyed) return; + if (!this._isHiding && !this._isShowing) return; + + var item = this._item; - return this; + cancelVisibilityTick(item._id); + this._animation.stop(); + if (processCallbackQueue) { + item._emitter.burst(this._queue, true, item); + } + }; + + /** + * Reset all existing visibility styles and apply new visibility styles to the + * visibility element. This method should be used to set styles when there is a + * chance that the current style properties differ from the new ones (basically + * on init and on migrations). + * + * @public + * @param {Object} styles + */ + ItemVisibility.prototype.setStyles = function (styles) { + var childElement = this._childElement; + var currentStyleProps = this._currentStyleProps; + this._removeCurrentStyles(); + for (var prop in styles) { + currentStyleProps.push(prop); + childElement.style[prop] = styles[prop]; + } }; /** * Destroy the instance and stop current animation if it is running. * * @public - * @memberof ItemVisibility.prototype - * @returns {ItemVisibility} */ - ItemVisibility.prototype.destroy = function() { - if (this._isDestroyed) return this; + ItemVisibility.prototype.destroy = function () { + if (this._isDestroyed) return; var item = this._item; var element = item._element; var grid = item.getGrid(); - var queue = this._queue; var settings = grid._settings; - // Stop visibility animation. - this._stopAnimation({}); - - // Fire all uncompleted callbacks with interrupted flag and destroy the queue. - queue.flush(true, item).destroy(); - - // Remove visible/hidden classes. + this.stop(true); + item._emitter.clear(this._queue); + this._animation.destroy(); + this._removeCurrentStyles(); removeClass(element, settings.itemVisibleClass); removeClass(element, settings.itemHiddenClass); + element.style.display = ''; // Reset state. - this._item = null; this._isHiding = this._isShowing = false; this._isDestroyed = this._isHidden = true; - - return this; }; /** @@ -4381,19 +5812,20 @@ * Start visibility animation. * * @private - * @memberof ItemVisibility.prototype * @param {Boolean} toVisible * @param {Boolean} [instant] * @param {Function} [onFinish] */ - ItemVisibility.prototype._startAnimation = function(toVisible, instant, onFinish) { + ItemVisibility.prototype._startAnimation = function (toVisible, instant, onFinish) { if (this._isDestroyed) return; var item = this._item; + var animation = this._animation; + var childElement = this._childElement; var settings = item.getGrid()._settings; var targetStyles = toVisible ? settings.visibleStyles : settings.hiddenStyles; - var duration = parseInt(toVisible ? settings.showDuration : settings.hideDuration) || 0; - var easing = (toVisible ? settings.showEasing : settings.hideEasing) || 'ease'; + var duration = toVisible ? settings.showDuration : settings.hideDuration; + var easing = toVisible ? settings.showEasing : settings.hideEasing; var isInstant = instant || duration <= 0; var currentStyles; @@ -4408,11 +5840,8 @@ // If we need to apply the styles instantly without animation. if (isInstant) { - if (item._animateChild.isAnimating()) { - item._animateChild.stop(targetStyles); - } else { - setStyles(item._child, targetStyles); - } + setStyles(childElement, targetStyles); + animation.stop(); onFinish && onFinish(); return; } @@ -4420,60 +5849,58 @@ // Start the animation in the next tick (to avoid layout thrashing). addVisibilityTick( item._id, - function() { - currentStyles = getCurrentStyles(item._child, targetStyles); + function () { + currentStyles = getCurrentStyles(childElement, targetStyles); }, - function() { - item._animateChild.start(currentStyles, targetStyles, { + function () { + animation.start(currentStyles, targetStyles, { duration: duration, easing: easing, - onFinish: onFinish + onFinish: onFinish, }); } ); }; - /** - * Stop visibility animation. - * - * @private - * @memberof ItemVisibility.prototype - * @param {Object} [targetStyles] - */ - ItemVisibility.prototype._stopAnimation = function(targetStyles) { - if (this._isDestroyed) return; - var item = this._item; - cancelVisibilityTick(item._id); - item._animateChild.stop(targetStyles); - }; - /** * Finish show procedure. * * @private - * @memberof ItemVisibility.prototype */ - ItemVisibility.prototype._finishShow = function() { + ItemVisibility.prototype._finishShow = function () { if (this._isHidden) return; this._isShowing = false; - this._queue.flush(false, this._item); + this._item._emitter.burst(this._queue, false, this._item); }; /** * Finish hide procedure. * * @private - * @memberof ItemVisibility.prototype */ - var finishStyles = {}; - ItemVisibility.prototype._finishHide = function() { + ItemVisibility.prototype._finishHide = function () { if (!this._isHidden) return; var item = this._item; this._isHiding = false; - finishStyles.transform = getTranslateString(0, 0); - item._layout.stop(true, finishStyles); + item._layout.stop(true, 0, 0); item._element.style.display = 'none'; - this._queue.flush(false, item); + item._emitter.burst(this._queue, false, item); + }; + + /** + * Remove currently applied visibility related inline style properties. + * + * @private + */ + ItemVisibility.prototype._removeCurrentStyles = function () { + var childElement = this._childElement; + var currentStyleProps = this._currentStyleProps; + + for (var i = 0; i < currentStyleProps.length; i++) { + childElement.style[currentStyleProps[i]] = ''; + } + + currentStyleProps.length = 0; }; var id = 0; @@ -4497,25 +5924,35 @@ function Item(grid, element, isActive) { var settings = grid._settings; - // Create instance id. - this._id = createUid(); + // Store item/element pair to a map (for faster item querying by element). + if (ITEM_ELEMENT_MAP) { + if (ITEM_ELEMENT_MAP.has(element)) { + throw new Error('You can only create one Muuri Item per element!'); + } else { + ITEM_ELEMENT_MAP.set(element, this); + } + } - // Reference to connected Grid instance's id. + this._id = createUid(); this._gridId = grid._id; - - // Destroyed flag. + this._element = element; this._isDestroyed = false; - - // Set up initial positions. this._left = 0; this._top = 0; - - // The elements. - this._element = element; - this._child = element.children[0]; + this._width = 0; + this._height = 0; + this._marginLeft = 0; + this._marginRight = 0; + this._marginTop = 0; + this._marginBottom = 0; + this._tX = undefined; + this._tY = undefined; + this._sortData = null; + this._emitter = new Emitter(); // If the provided item element is not a direct child of the grid container - // element, append it to the grid container. + // element, append it to the grid container. Note, we are indeed reading the + // DOM here but it's a property that does not cause reflowing. if (element.parentNode !== grid._element) { grid._element.appendChild(element); } @@ -4523,7 +5960,9 @@ // Set item class. addClass(element, settings.itemClass); - // If isActive is not defined, let's try to auto-detect it. + // If isActive is not defined, let's try to auto-detect it. Note, we are + // indeed reading the DOM here but it's a property that does not cause + // reflowing. if (typeof isActive !== 'boolean') { isActive = getStyle(element, 'display') !== 'none'; } @@ -4532,15 +5971,6 @@ // or not). this._isActive = isActive; - // Set element's initial position styles. - element.style.left = '0'; - element.style.top = '0'; - element.style[transformProp] = getTranslateString(0, 0); - - // Initiate item's animation controllers. - this._animate = new ItemAnimate(element); - this._animateChild = new ItemAnimate(this._child); - // Setup visibility handler. this._visibility = new ItemVisibility(this); @@ -4550,22 +5980,25 @@ // Set up migration handler data. this._migrate = new ItemMigrate(this); + // Set up drag handler. + this._drag = settings.dragEnabled ? new ItemDrag(this) : null; + // Set up release handler. Note that although this is fully linked to dragging // this still needs to be always instantiated to handle migration scenarios // correctly. - this._release = new ItemRelease(this); + this._dragRelease = new ItemDragRelease(this); // Set up drag placeholder handler. Note that although this is fully linked to // dragging this still needs to be always instantiated to handle migration // scenarios correctly. this._dragPlaceholder = new ItemDragPlaceholder(this); - // Set up drag handler. - this._drag = settings.dragEnabled ? new ItemDrag(this) : null; - - // Set up the initial dimensions and sort data. - this._refreshDimensions(); - this._refreshSortData(); + // Note! You must call the following methods before you start using the + // instance. They are deliberately not called in the end as it would cause + // potentially a massive amount of reflows if multiple items were instantiated + // in a loop. + // this._refreshDimensions(); + // this._refreshSortData(); } /** @@ -4577,21 +6010,19 @@ * Get the instance grid reference. * * @public - * @memberof Item.prototype * @returns {Grid} */ - Item.prototype.getGrid = function() { - return gridInstances[this._gridId]; + Item.prototype.getGrid = function () { + return GRID_INSTANCES[this._gridId]; }; /** * Get the instance element. * * @public - * @memberof Item.prototype * @returns {HTMLElement} */ - Item.prototype.getElement = function() { + Item.prototype.getElement = function () { return this._element; }; @@ -4599,10 +6030,9 @@ * Get instance element's cached width. * * @public - * @memberof Item.prototype * @returns {Number} */ - Item.prototype.getWidth = function() { + Item.prototype.getWidth = function () { return this._width; }; @@ -4610,10 +6040,9 @@ * Get instance element's cached height. * * @public - * @memberof Item.prototype * @returns {Number} */ - Item.prototype.getHeight = function() { + Item.prototype.getHeight = function () { return this._height; }; @@ -4621,17 +6050,16 @@ * Get instance element's cached margins. * * @public - * @memberof Item.prototype * @returns {Object} * - The returned object contains left, right, top and bottom properties * which indicate the item element's cached margins. */ - Item.prototype.getMargin = function() { + Item.prototype.getMargin = function () { return { left: this._marginLeft, right: this._marginRight, top: this._marginTop, - bottom: this._marginBottom + bottom: this._marginBottom, }; }; @@ -4639,15 +6067,14 @@ * Get instance element's cached position. * * @public - * @memberof Item.prototype * @returns {Object} * - The returned object contains left and top properties which indicate the * item element's cached position in the grid. */ - Item.prototype.getPosition = function() { + Item.prototype.getPosition = function () { return { left: this._left, - top: this._top + top: this._top, }; }; @@ -4655,10 +6082,9 @@ * Is the item active? * * @public - * @memberof Item.prototype * @returns {Boolean} */ - Item.prototype.isActive = function() { + Item.prototype.isActive = function () { return this._isActive; }; @@ -4666,10 +6092,9 @@ * Is the item visible? * * @public - * @memberof Item.prototype * @returns {Boolean} */ - Item.prototype.isVisible = function() { + Item.prototype.isVisible = function () { return !!this._visibility && !this._visibility._isHidden; }; @@ -4677,10 +6102,9 @@ * Is the item being animated to visible? * * @public - * @memberof Item.prototype * @returns {Boolean} */ - Item.prototype.isShowing = function() { + Item.prototype.isShowing = function () { return !!(this._visibility && this._visibility._isShowing); }; @@ -4688,10 +6112,9 @@ * Is the item being animated to hidden? * * @public - * @memberof Item.prototype * @returns {Boolean} */ - Item.prototype.isHiding = function() { + Item.prototype.isHiding = function () { return !!(this._visibility && this._visibility._isHiding); }; @@ -4699,21 +6122,19 @@ * Is the item positioning? * * @public - * @memberof Item.prototype * @returns {Boolean} */ - Item.prototype.isPositioning = function() { + Item.prototype.isPositioning = function () { return !!(this._layout && this._layout._isActive); }; /** - * Is the item being dragged? + * Is the item being dragged (or queued for dragging)? * * @public - * @memberof Item.prototype * @returns {Boolean} */ - Item.prototype.isDragging = function() { + Item.prototype.isDragging = function () { return !!(this._drag && this._drag._isActive); }; @@ -4721,21 +6142,19 @@ * Is the item being released? * * @public - * @memberof Item.prototype * @returns {Boolean} */ - Item.prototype.isReleasing = function() { - return !!(this._release && this._release._isActive); + Item.prototype.isReleasing = function () { + return !!(this._dragRelease && this._dragRelease._isActive); }; /** * Is the item destroyed? * * @public - * @memberof Item.prototype * @returns {Boolean} */ - Item.prototype.isDestroyed = function() { + Item.prototype.isDestroyed = function () { return this._isDestroyed; }; @@ -4748,10 +6167,11 @@ * Recalculate item's dimensions. * * @private - * @memberof Item.prototype + * @param {Boolean} [force=false] */ - Item.prototype._refreshDimensions = function() { - if (this._isDestroyed || this._visibility._isHidden) return; + Item.prototype._refreshDimensions = function (force) { + if (this._isDestroyed) return; + if (force !== true && this._visibility._isHidden) return; var element = this._element; var dragPlaceholder = this._dragPlaceholder; @@ -4768,18 +6188,15 @@ this._marginBottom = Math.max(0, getStyleAsFloat(element, 'margin-bottom')); // Keep drag placeholder's dimensions synced with the item's. - if (dragPlaceholder) { - dragPlaceholder.updateDimensions(this._width, this._height); - } + if (dragPlaceholder) dragPlaceholder.updateDimensions(); }; /** * Fetch and store item's sort data. * * @private - * @memberof Item.prototype */ - Item.prototype._refreshSortData = function() { + Item.prototype._refreshSortData = function () { if (this._isDestroyed) return; var data = (this._sortData = {}); @@ -4791,537 +6208,1026 @@ } }; + /** + * Add item to layout. + * + * @private + */ + Item.prototype._addToLayout = function (left, top) { + if (this._isActive === true) return; + this._isActive = true; + this._left = left || 0; + this._top = top || 0; + }; + + /** + * Remove item from layout. + * + * @private + */ + Item.prototype._removeFromLayout = function () { + if (this._isActive === false) return; + this._isActive = false; + this._left = 0; + this._top = 0; + }; + + /** + * Check if the layout procedure can be skipped for the item. + * + * @private + * @param {Number} left + * @param {Number} top + * @returns {Boolean} + */ + Item.prototype._canSkipLayout = function (left, top) { + return ( + this._left === left && + this._top === top && + !this._migrate._isActive && + !this._layout._skipNextAnimation && + !this._dragRelease.isJustReleased() + ); + }; + + /** + * Set the provided left and top arguments as the item element's translate + * values in the DOM. This method keeps track of the currently applied + * translate values and skips the update operation if the provided values are + * identical to the currently applied values. Returns `false` if there was no + * need for update and `true` if the translate value was updated. + * + * @private + * @param {Number} left + * @param {Number} top + * @returns {Boolean} + */ + Item.prototype._setTranslate = function (left, top) { + if (this._tX === left && this._tY === top) return false; + this._tX = left; + this._tY = top; + this._element.style[transformProp] = getTranslateString(left, top); + return true; + }; + /** * Destroy item instance. * * @private - * @memberof Item.prototype * @param {Boolean} [removeElement=false] */ - Item.prototype._destroy = function(removeElement) { + Item.prototype._destroy = function (removeElement) { if (this._isDestroyed) return; var element = this._element; var grid = this.getGrid(); var settings = grid._settings; - var index = grid._items.indexOf(this); // Destroy handlers. - this._release.destroy(); + this._dragPlaceholder.destroy(); + this._dragRelease.destroy(); this._migrate.destroy(); this._layout.destroy(); this._visibility.destroy(); - this._animate.destroy(); - this._animateChild.destroy(); - this._dragPlaceholder.destroy(); - this._drag && this._drag.destroy(); + if (this._drag) this._drag.destroy(); - // Remove all inline styles. - element.removeAttribute('style'); - this._child.removeAttribute('style'); + // Destroy emitter. + this._emitter.destroy(); // Remove item class. removeClass(element, settings.itemClass); - // Remove item from Grid instance if it still exists there. - index > -1 && grid._items.splice(index, 1); - // Remove element from DOM. - removeElement && element.parentNode.removeChild(element); + if (removeElement) element.parentNode.removeChild(element); + + // Remove item/element pair from map. + if (ITEM_ELEMENT_MAP) ITEM_ELEMENT_MAP.delete(element); // Reset state. this._isActive = false; this._isDestroyed = true; }; - /** - * This is the default layout algorithm for Muuri. Based on MAXRECTS approach - * as described by Jukka Jylänki in his survey: "A Thousand Ways to Pack the - * Bin - A Practical Approach to Two-Dimensional Rectangle Bin Packing.". - * - * @class - */ - function Packer() { - this._slots = []; - this._slotSizes = []; - this._freeSlots = []; - this._newSlots = []; - this._rectItem = {}; - this._rectStore = []; - this._rectId = 0; - - // The layout return data, which will be populated in getLayout. - this._layout = { - slots: null, - setWidth: false, - setHeight: false, - width: false, - height: false - }; - - // Bind sort handlers. - this._sortRectsLeftTop = this._sortRectsLeftTop.bind(this); - this._sortRectsTopLeft = this._sortRectsTopLeft.bind(this); - } + function createPackerProcessor(isWorker = false) { + var FILL_GAPS = 1; + var HORIZONTAL = 2; + var ALIGN_RIGHT = 4; + var ALIGN_BOTTOM = 8; + var ROUNDING = 16; + + var EPS = 0.001; + var MIN_SLOT_SIZE = 0.5; + + // Rounds number first to three decimal precision and then floors the result + // to two decimal precision. + // Math.floor(Math.round(number * 1000) / 10) / 100 + function roundNumber(number) { + return ((((number * 1000 + 0.5) << 0) / 10) << 0) / 100; + } + + /** + * @class + */ + function PackerProcessor() { + this.currentRects = []; + this.nextRects = []; + this.rectTarget = {}; + this.rectStore = []; + this.slotSizes = []; + this.rectId = 0; + this.slotIndex = -1; + this.slotData = { left: 0, top: 0, width: 0, height: 0 }; + this.sortRectsLeftTop = this.sortRectsLeftTop.bind(this); + this.sortRectsTopLeft = this.sortRectsTopLeft.bind(this); + } + + /** + * Takes a layout object as an argument and computes positions (slots) for the + * layout items. Also computes the final width and height of the layout. The + * provided layout object's slots array is mutated as well as the width and + * height properties. + * + * @param {Object} layout + * @param {Number} layout.width + * - The start (current) width of the layout in pixels. + * @param {Number} layout.height + * - The start (current) height of the layout in pixels. + * @param {(Item[]|Number[])} layout.items + * - List of Muuri.Item instances or a list of item dimensions + * (e.g [ item1Width, item1Height, item2Width, item2Height, ... ]). + * @param {(Array|Float32Array)} layout.slots + * - An Array/Float32Array instance which's length should equal to + * the amount of items times two. The position (width and height) of each + * item will be written into this array. + * @param {Number} settings + * - The layout's settings as bitmasks. + * @returns {Object} + */ + PackerProcessor.prototype.computeLayout = function (layout, settings) { + var items = layout.items; + var slots = layout.slots; + var fillGaps = !!(settings & FILL_GAPS); + var horizontal = !!(settings & HORIZONTAL); + var alignRight = !!(settings & ALIGN_RIGHT); + var alignBottom = !!(settings & ALIGN_BOTTOM); + var rounding = !!(settings & ROUNDING); + var isPreProcessed = typeof items[0] === 'number'; + var i, bump, item, slotWidth, slotHeight, slot; + + // No need to go further if items do not exist. + if (!items.length) return layout; + + // Compute slots for the items. + bump = isPreProcessed ? 2 : 1; + for (i = 0; i < items.length; i += bump) { + // If items are pre-processed it means that items array contains only + // the raw dimensions of the items. Otherwise we assume it is an array + // of normal Muuri items. + if (isPreProcessed) { + slotWidth = items[i]; + slotHeight = items[i + 1]; + } else { + item = items[i]; + slotWidth = item._width + item._marginLeft + item._marginRight; + slotHeight = item._height + item._marginTop + item._marginBottom; + } - /** - * @public - * @memberof Packer.prototype - * @param {Item[]} items - * @param {Number} width - * @param {Number} height - * @param {Number[]} [slots] - * @param {Object} [options] - * @param {Boolean} [options.fillGaps=false] - * @param {Boolean} [options.horizontal=false] - * @param {Boolean} [options.alignRight=false] - * @param {Boolean} [options.alignBottom=false] - * @returns {LayoutData} - */ - Packer.prototype.getLayout = function(items, width, height, slots, options) { - var layout = this._layout; - var fillGaps = !!(options && options.fillGaps); - var isHorizontal = !!(options && options.horizontal); - var alignRight = !!(options && options.alignRight); - var alignBottom = !!(options && options.alignBottom); - var rounding = !!(options && options.rounding); - var slotSizes = this._slotSizes; - var i; + // If rounding is enabled let's round the item's width and height to + // make the layout algorithm a bit more stable. This has a performance + // cost so don't use this if not necessary. + if (rounding) { + slotWidth = roundNumber(slotWidth); + slotHeight = roundNumber(slotHeight); + } - // Reset layout data. - layout.slots = slots ? slots : this._slots; - layout.width = isHorizontal ? 0 : rounding ? Math.round(width) : width; - layout.height = !isHorizontal ? 0 : rounding ? Math.round(height) : height; - layout.setWidth = isHorizontal; - layout.setHeight = !isHorizontal; + // Get slot data. + slot = this.computeNextSlot(layout, slotWidth, slotHeight, fillGaps, horizontal); - // Make sure slots and slot size arrays are reset. - layout.slots.length = 0; - slotSizes.length = 0; + // Update layout width/height. + if (horizontal) { + if (slot.left + slot.width > layout.width) { + layout.width = slot.left + slot.width; + } + } else { + if (slot.top + slot.height > layout.height) { + layout.height = slot.top + slot.height; + } + } - // No need to go further if items do not exist. - if (!items.length) return layout; + // Add item slot data to layout slots. + slots[++this.slotIndex] = slot.left; + slots[++this.slotIndex] = slot.top; - // Find slots for items. - for (i = 0; i < items.length; i++) { - this._addSlot(items[i], isHorizontal, fillGaps, rounding, alignRight || alignBottom); - } + // Store the size too (for later usage) if needed. + if (alignRight || alignBottom) { + this.slotSizes.push(slot.width, slot.height); + } + } - // If the alignment is set to right we need to adjust the results. - if (alignRight) { - for (i = 0; i < layout.slots.length; i = i + 2) { - layout.slots[i] = layout.width - (layout.slots[i] + slotSizes[i]); + // If the alignment is set to right we need to adjust the results. + if (alignRight) { + for (i = 0; i < slots.length; i += 2) { + slots[i] = layout.width - (slots[i] + this.slotSizes[i]); + } } - } - // If the alignment is set to bottom we need to adjust the results. - if (alignBottom) { - for (i = 1; i < layout.slots.length; i = i + 2) { - layout.slots[i] = layout.height - (layout.slots[i] + slotSizes[i]); + // If the alignment is set to bottom we need to adjust the results. + if (alignBottom) { + for (i = 1; i < slots.length; i += 2) { + slots[i] = layout.height - (slots[i] + this.slotSizes[i]); + } } - } - // Reset slots arrays and rect id. - slotSizes.length = 0; - this._freeSlots.length = 0; - this._newSlots.length = 0; - this._rectId = 0; + // Reset stuff. + this.slotSizes.length = 0; + this.currentRects.length = 0; + this.nextRects.length = 0; + this.rectId = 0; + this.slotIndex = -1; - return layout; - }; + return layout; + }; - /** - * Calculate position for the layout item. Returns the left and top position - * of the item in pixels. - * - * @private - * @memberof Packer.prototype - * @param {Item} item - * @param {Boolean} isHorizontal - * @param {Boolean} fillGaps - * @param {Boolean} rounding - * @returns {Array} - */ - Packer.prototype._addSlot = (function() { - var eps = 0.001; - var itemSlot = {}; - return function(item, isHorizontal, fillGaps, rounding, trackSize) { - var layout = this._layout; - var freeSlots = this._freeSlots; - var newSlots = this._newSlots; + /** + * Calculate next slot in the layout. Returns a slot object with position and + * dimensions data. The returned object is reused between calls. + * + * @param {Object} layout + * @param {Number} slotWidth + * @param {Number} slotHeight + * @param {Boolean} fillGaps + * @param {Boolean} horizontal + * @returns {Object} + */ + PackerProcessor.prototype.computeNextSlot = function ( + layout, + slotWidth, + slotHeight, + fillGaps, + horizontal + ) { + var slot = this.slotData; + var currentRects = this.currentRects; + var nextRects = this.nextRects; + var ignoreCurrentRects = false; var rect; var rectId; - var potentialSlots; - var ignoreCurrentSlots; + var shards; var i; - var ii; + var j; // Reset new slots. - newSlots.length = 0; + nextRects.length = 0; // Set item slot initial data. - itemSlot.left = null; - itemSlot.top = null; - itemSlot.width = item._width + item._marginLeft + item._marginRight; - itemSlot.height = item._height + item._marginTop + item._marginBottom; - - // Round item slot width and height if needed. - if (rounding) { - itemSlot.width = Math.round(itemSlot.width); - itemSlot.height = Math.round(itemSlot.height); - } + slot.left = null; + slot.top = null; + slot.width = slotWidth; + slot.height = slotHeight; - // Try to find a slot for the item. - for (i = 0; i < freeSlots.length; i++) { - rectId = freeSlots[i]; + // Try to find position for the slot from the existing free spaces in the + // layout. + for (i = 0; i < currentRects.length; i++) { + rectId = currentRects[i]; if (!rectId) continue; - rect = this._getRect(rectId); - if (itemSlot.width <= rect.width + eps && itemSlot.height <= rect.height + eps) { - itemSlot.left = rect.left; - itemSlot.top = rect.top; + rect = this.getRect(rectId); + if (slot.width <= rect.width + EPS && slot.height <= rect.height + EPS) { + slot.left = rect.left; + slot.top = rect.top; break; } } - // If no slot was found for the item. - if (itemSlot.left === null) { - // Position the item in to the bottom left (vertical mode) or top right - // (horizontal mode) of the grid. - itemSlot.left = !isHorizontal ? 0 : layout.width; - itemSlot.top = !isHorizontal ? layout.height : 0; + // If no position was found for the slot let's position the slot to + // the bottom left (in vertical mode) or top right (in horizontal mode) of + // the layout. + if (slot.left === null) { + if (horizontal) { + slot.left = layout.width; + slot.top = 0; + } else { + slot.left = 0; + slot.top = layout.height; + } - // If gaps don't need filling do not add any current slots to the new - // slots array. + // If gaps don't need filling let's throw away all the current free spaces + // (currentRects). if (!fillGaps) { - ignoreCurrentSlots = true; + ignoreCurrentRects = true; } } - // In vertical mode, if the item's bottom overlaps the grid's bottom. - if (!isHorizontal && itemSlot.top + itemSlot.height > layout.height) { - // If item is not aligned to the left edge, create a new slot. - if (itemSlot.left > 0) { - newSlots.push(this._addRect(0, layout.height, itemSlot.left, Infinity)); + // In vertical mode, if the slot's bottom overlaps the layout's bottom. + if (!horizontal && slot.top + slot.height > layout.height + EPS) { + // If slot is not aligned to the left edge, create a new free space to the + // left of the slot. + if (slot.left > MIN_SLOT_SIZE) { + nextRects.push(this.addRect(0, layout.height, slot.left, Infinity)); } - // If item is not aligned to the right edge, create a new slot. - if (itemSlot.left + itemSlot.width < layout.width) { - newSlots.push( - this._addRect( - itemSlot.left + itemSlot.width, + // If slot is not aligned to the right edge, create a new free space to + // the right of the slot. + if (slot.left + slot.width < layout.width - MIN_SLOT_SIZE) { + nextRects.push( + this.addRect( + slot.left + slot.width, layout.height, - layout.width - itemSlot.left - itemSlot.width, + layout.width - slot.left - slot.width, Infinity ) ); } - // Update grid height. - layout.height = itemSlot.top + itemSlot.height; + // Update layout height. + layout.height = slot.top + slot.height; } - // In horizontal mode, if the item's right overlaps the grid's right edge. - if (isHorizontal && itemSlot.left + itemSlot.width > layout.width) { - // If item is not aligned to the top, create a new slot. - if (itemSlot.top > 0) { - newSlots.push(this._addRect(layout.width, 0, Infinity, itemSlot.top)); + // In horizontal mode, if the slot's right overlaps the layout's right edge. + if (horizontal && slot.left + slot.width > layout.width + EPS) { + // If slot is not aligned to the top, create a new free space above the + // slot. + if (slot.top > MIN_SLOT_SIZE) { + nextRects.push(this.addRect(layout.width, 0, Infinity, slot.top)); } - // If item is not aligned to the bottom, create a new slot. - if (itemSlot.top + itemSlot.height < layout.height) { - newSlots.push( - this._addRect( + // If slot is not aligned to the bottom, create a new free space below + // the slot. + if (slot.top + slot.height < layout.height - MIN_SLOT_SIZE) { + nextRects.push( + this.addRect( layout.width, - itemSlot.top + itemSlot.height, + slot.top + slot.height, Infinity, - layout.height - itemSlot.top - itemSlot.height + layout.height - slot.top - slot.height ) ); } - // Update grid width. - layout.width = itemSlot.left + itemSlot.width; + // Update layout width. + layout.width = slot.left + slot.width; + } + + // Clean up the current free spaces making sure none of them overlap with + // the slot. Split all overlapping free spaces into smaller shards that do + // not overlap with the slot. + if (!ignoreCurrentRects) { + if (fillGaps) i = 0; + for (; i < currentRects.length; i++) { + rectId = currentRects[i]; + if (!rectId) continue; + rect = this.getRect(rectId); + shards = this.splitRect(rect, slot); + for (j = 0; j < shards.length; j++) { + rectId = shards[j]; + rect = this.getRect(rectId); + // Make sure that the free space is within the boundaries of the + // layout. This routine is critical to the algorithm as it makes sure + // that there are no leftover spaces with infinite height/width. + // It's also essential that we don't compare values absolutely to each + // other but leave a little headroom (EPSILON) to get rid of false + // positives. + if ( + horizontal ? rect.left + EPS < layout.width - EPS : rect.top + EPS < layout.height - EPS + ) { + nextRects.push(rectId); + } + } + } + } + + // Sanitize and sort all the new free spaces that will be used in the next + // iteration. This procedure is critical to make the bin-packing algorithm + // work. The free spaces have to be in correct order in the beginning of the + // next iteration. + if (nextRects.length > 1) { + this.purgeRects(nextRects).sort(horizontal ? this.sortRectsLeftTop : this.sortRectsTopLeft); + } + + // Finally we need to make sure that `this.currentRects` points to + // `nextRects` array as that is used in the next iteration's beginning when + // we try to find a space for the next slot. + this.currentRects = nextRects; + this.nextRects = currentRects; + + return slot; + }; + + /** + * Add a new rectangle to the rectangle store. Returns the id of the new + * rectangle. + * + * @param {Number} left + * @param {Number} top + * @param {Number} width + * @param {Number} height + * @returns {Number} + */ + PackerProcessor.prototype.addRect = function (left, top, width, height) { + var rectId = ++this.rectId; + this.rectStore[rectId] = left || 0; + this.rectStore[++this.rectId] = top || 0; + this.rectStore[++this.rectId] = width || 0; + this.rectStore[++this.rectId] = height || 0; + return rectId; + }; + + /** + * Get rectangle data from the rectangle store by id. Optionally you can + * provide a target object where the rectangle data will be written in. By + * default an internal object is reused as a target object. + * + * @param {Number} id + * @param {Object} [target] + * @returns {Object} + */ + PackerProcessor.prototype.getRect = function (id, target) { + if (!target) target = this.rectTarget; + target.left = this.rectStore[id] || 0; + target.top = this.rectStore[++id] || 0; + target.width = this.rectStore[++id] || 0; + target.height = this.rectStore[++id] || 0; + return target; + }; + + /** + * Punch a hole into a rectangle and return the shards (1-4). + * + * @param {Object} rect + * @param {Object} hole + * @returns {Number[]} + */ + PackerProcessor.prototype.splitRect = (function () { + var shards = []; + var width = 0; + var height = 0; + return function (rect, hole) { + // Reset old shards. + shards.length = 0; + + // If the slot does not overlap with the hole add slot to the return data + // as is. Note that in this case we are eager to keep the slot as is if + // possible so we use the EPSILON in favour of that logic. + if ( + rect.left + rect.width <= hole.left + EPS || + hole.left + hole.width <= rect.left + EPS || + rect.top + rect.height <= hole.top + EPS || + hole.top + hole.height <= rect.top + EPS + ) { + shards.push(this.addRect(rect.left, rect.top, rect.width, rect.height)); + return shards; + } + + // Left split. + width = hole.left - rect.left; + if (width >= MIN_SLOT_SIZE) { + shards.push(this.addRect(rect.left, rect.top, width, rect.height)); + } + + // Right split. + width = rect.left + rect.width - (hole.left + hole.width); + if (width >= MIN_SLOT_SIZE) { + shards.push(this.addRect(hole.left + hole.width, rect.top, width, rect.height)); + } + + // Top split. + height = hole.top - rect.top; + if (height >= MIN_SLOT_SIZE) { + shards.push(this.addRect(rect.left, rect.top, rect.width, height)); + } + + // Bottom split. + height = rect.top + rect.height - (hole.top + hole.height); + if (height >= MIN_SLOT_SIZE) { + shards.push(this.addRect(rect.left, hole.top + hole.height, rect.width, height)); + } + + return shards; + }; + })(); + + /** + * Check if a rectangle is fully within another rectangle. + * + * @param {Object} a + * @param {Object} b + * @returns {Boolean} + */ + PackerProcessor.prototype.isRectAWithinRectB = function (a, b) { + return ( + a.left + EPS >= b.left && + a.top + EPS >= b.top && + a.left + a.width - EPS <= b.left + b.width && + a.top + a.height - EPS <= b.top + b.height + ); + }; + + /** + * Loops through an array of rectangle ids and resets all that are fully + * within another rectangle in the array. Resetting in this case means that + * the rectangle id value is replaced with zero. + * + * @param {Number[]} rectIds + * @returns {Number[]} + */ + PackerProcessor.prototype.purgeRects = (function () { + var rectA = {}; + var rectB = {}; + return function (rectIds) { + var i = rectIds.length; + var j; + + while (i--) { + j = rectIds.length; + if (!rectIds[i]) continue; + this.getRect(rectIds[i], rectA); + while (j--) { + if (!rectIds[j] || i === j) continue; + this.getRect(rectIds[j], rectB); + if (this.isRectAWithinRectB(rectA, rectB)) { + rectIds[i] = 0; + break; + } + } + } + + return rectIds; + }; + })(); + + /** + * Sort rectangles with top-left gravity. + * + * @param {Number} aId + * @param {Number} bId + * @returns {Number} + */ + PackerProcessor.prototype.sortRectsTopLeft = (function () { + var rectA = {}; + var rectB = {}; + return function (aId, bId) { + this.getRect(aId, rectA); + this.getRect(bId, rectB); + + return rectA.top < rectB.top && rectA.top + EPS < rectB.top + ? -1 + : rectA.top > rectB.top && rectA.top - EPS > rectB.top + ? 1 + : rectA.left < rectB.left && rectA.left + EPS < rectB.left + ? -1 + : rectA.left > rectB.left && rectA.left - EPS > rectB.left + ? 1 + : 0; + }; + })(); + + /** + * Sort rectangles with left-top gravity. + * + * @param {Number} aId + * @param {Number} bId + * @returns {Number} + */ + PackerProcessor.prototype.sortRectsLeftTop = (function () { + var rectA = {}; + var rectB = {}; + return function (aId, bId) { + this.getRect(aId, rectA); + this.getRect(bId, rectB); + return rectA.left < rectB.left && rectA.left + EPS < rectB.left + ? -1 + : rectA.left > rectB.left && rectA.left - EPS < rectB.left + ? 1 + : rectA.top < rectB.top && rectA.top + EPS < rectB.top + ? -1 + : rectA.top > rectB.top && rectA.top - EPS > rectB.top + ? 1 + : 0; + }; + })(); + + if (isWorker) { + var PACKET_INDEX_WIDTH = 1; + var PACKET_INDEX_HEIGHT = 2; + var PACKET_INDEX_OPTIONS = 3; + var PACKET_HEADER_SLOTS = 4; + var processor = new PackerProcessor(); + + self.onmessage = function (msg) { + var data = new Float32Array(msg.data); + var items = data.subarray(PACKET_HEADER_SLOTS, data.length); + var slots = new Float32Array(items.length); + var settings = data[PACKET_INDEX_OPTIONS]; + var layout = { + items: items, + slots: slots, + width: data[PACKET_INDEX_WIDTH], + height: data[PACKET_INDEX_HEIGHT], + }; + + // Compute the layout (width / height / slots). + processor.computeLayout(layout, settings); + + // Copy layout data to the return data. + data[PACKET_INDEX_WIDTH] = layout.width; + data[PACKET_INDEX_HEIGHT] = layout.height; + data.set(layout.slots, PACKET_HEADER_SLOTS); + + // Send layout back to the main thread. + postMessage(data.buffer, [data.buffer]); + }; + } + + return PackerProcessor; + } + + var PackerProcessor = createPackerProcessor(); + + // + // WORKER UTILS + // + + var blobUrl = null; + var activeWorkers = []; + + function createWorkerProcessors(amount, onmessage) { + var workers = []; + + if (amount > 0) { + if (!blobUrl) { + blobUrl = URL.createObjectURL( + new Blob(['(' + createPackerProcessor.toString() + ')(true)'], { + type: 'application/javascript', + }) + ); + } + + for (var i = 0, worker; i < amount; i++) { + worker = new Worker(blobUrl); + if (onmessage) worker.onmessage = onmessage; + workers.push(worker); + activeWorkers.push(worker); + } + } + + return workers; + } + + function destroyWorkerProcessors(workers) { + var worker; + var index; + + for (var i = 0; i < workers.length; i++) { + worker = workers[i]; + worker.onmessage = null; + worker.onerror = null; + worker.onmessageerror = null; + worker.terminate(); + + index = activeWorkers.indexOf(worker); + if (index > -1) activeWorkers.splice(index, 1); + } + + if (blobUrl && !activeWorkers.length) { + URL.revokeObjectURL(blobUrl); + blobUrl = null; + } + } + + function isWorkerProcessorsSupported() { + return !!(window.Worker && window.URL && window.Blob); + } + + var FILL_GAPS = 1; + var HORIZONTAL = 2; + var ALIGN_RIGHT = 4; + var ALIGN_BOTTOM = 8; + var ROUNDING = 16; + var PACKET_INDEX_ID = 0; + var PACKET_INDEX_WIDTH = 1; + var PACKET_INDEX_HEIGHT = 2; + var PACKET_INDEX_OPTIONS = 3; + var PACKET_HEADER_SLOTS = 4; + + /** + * @class + * @param {Number} [numWorkers=0] + * @param {Object} [options] + * @param {Boolean} [options.fillGaps=false] + * @param {Boolean} [options.horizontal=false] + * @param {Boolean} [options.alignRight=false] + * @param {Boolean} [options.alignBottom=false] + * @param {Boolean} [options.rounding=false] + */ + function Packer(numWorkers, options) { + this._options = 0; + this._processor = null; + this._layoutQueue = []; + this._layouts = {}; + this._layoutCallbacks = {}; + this._layoutWorkers = {}; + this._layoutWorkerData = {}; + this._workers = []; + this._onWorkerMessage = this._onWorkerMessage.bind(this); + + // Set initial options. + this.setOptions(options); + + // Init the worker(s) or the processor if workers can't be used. + numWorkers = typeof numWorkers === 'number' ? Math.max(0, numWorkers) : 0; + if (numWorkers && isWorkerProcessorsSupported()) { + try { + this._workers = createWorkerProcessors(numWorkers, this._onWorkerMessage); + } catch (e) { + this._processor = new PackerProcessor(); } + } else { + this._processor = new PackerProcessor(); + } + } + + Packer.prototype._sendToWorker = function () { + if (!this._layoutQueue.length || !this._workers.length) return; + + var layoutId = this._layoutQueue.shift(); + var worker = this._workers.pop(); + var data = this._layoutWorkerData[layoutId]; + + delete this._layoutWorkerData[layoutId]; + this._layoutWorkers[layoutId] = worker; + worker.postMessage(data.buffer, [data.buffer]); + }; + + Packer.prototype._onWorkerMessage = function (msg) { + var data = new Float32Array(msg.data); + var layoutId = data[PACKET_INDEX_ID]; + var layout = this._layouts[layoutId]; + var callback = this._layoutCallbacks[layoutId]; + var worker = this._layoutWorkers[layoutId]; + + if (layout) delete this._layoutCallbacks[layoutId]; + if (callback) delete this._layoutCallbacks[layoutId]; + if (worker) delete this._layoutWorkers[layoutId]; + + if (layout && callback) { + layout.width = data[PACKET_INDEX_WIDTH]; + layout.height = data[PACKET_INDEX_HEIGHT]; + layout.slots = data.subarray(PACKET_HEADER_SLOTS, data.length); + this._finalizeLayout(layout); + callback(layout); + } + + if (worker) { + this._workers.push(worker); + this._sendToWorker(); + } + }; + + Packer.prototype._finalizeLayout = function (layout) { + var grid = layout._grid; + var isHorizontal = layout._settings & HORIZONTAL; + var isBorderBox = grid._boxSizing === 'border-box'; + + delete layout._grid; + delete layout._settings; + + layout.styles = {}; + + if (isHorizontal) { + layout.styles.width = + (isBorderBox ? layout.width + grid._borderLeft + grid._borderRight : layout.width) + 'px'; + } else { + layout.styles.height = + (isBorderBox ? layout.height + grid._borderTop + grid._borderBottom : layout.height) + 'px'; + } + + return layout; + }; + + /** + * @public + * @param {Object} [options] + * @param {Boolean} [options.fillGaps] + * @param {Boolean} [options.horizontal] + * @param {Boolean} [options.alignRight] + * @param {Boolean} [options.alignBottom] + * @param {Boolean} [options.rounding] + */ + Packer.prototype.setOptions = function (options) { + if (!options) return; + + var fillGaps; + if (typeof options.fillGaps === 'boolean') { + fillGaps = options.fillGaps ? FILL_GAPS : 0; + } else { + fillGaps = this._options & FILL_GAPS; + } - // Clean up the current slots making sure there are no old slots that - // overlap with the item. If an old slot overlaps with the item, split it - // into smaller slots if necessary. - for (i = fillGaps ? 0 : ignoreCurrentSlots ? freeSlots.length : i; i < freeSlots.length; i++) { - rectId = freeSlots[i]; - if (!rectId) continue; - rect = this._getRect(rectId); - potentialSlots = this._splitRect(rect, itemSlot); - for (ii = 0; ii < potentialSlots.length; ii++) { - rectId = potentialSlots[ii]; - rect = this._getRect(rectId); - // Let's make sure here that we have a big enough slot - // (width/height > 0.49px) and also let's make sure that the slot is - // within the boundaries of the grid. - if ( - rect.width > 0.49 && - rect.height > 0.49 && - ((!isHorizontal && rect.top < layout.height) || - (isHorizontal && rect.left < layout.width)) - ) { - newSlots.push(rectId); - } - } - } + var horizontal; + if (typeof options.horizontal === 'boolean') { + horizontal = options.horizontal ? HORIZONTAL : 0; + } else { + horizontal = this._options & HORIZONTAL; + } - // Sanitize new slots. - if (newSlots.length) { - this._purgeRects(newSlots).sort( - isHorizontal ? this._sortRectsLeftTop : this._sortRectsTopLeft - ); - } + var alignRight; + if (typeof options.alignRight === 'boolean') { + alignRight = options.alignRight ? ALIGN_RIGHT : 0; + } else { + alignRight = this._options & ALIGN_RIGHT; + } - // Update layout width/height. - if (isHorizontal) { - layout.width = Math.max(layout.width, itemSlot.left + itemSlot.width); - } else { - layout.height = Math.max(layout.height, itemSlot.top + itemSlot.height); - } + var alignBottom; + if (typeof options.alignBottom === 'boolean') { + alignBottom = options.alignBottom ? ALIGN_BOTTOM : 0; + } else { + alignBottom = this._options & ALIGN_BOTTOM; + } - // Add item slot data to layout slots (and store the slot size for later - // usage too if necessary). - layout.slots.push(itemSlot.left, itemSlot.top); - if (trackSize) this._slotSizes.push(itemSlot.width, itemSlot.height); + var rounding; + if (typeof options.rounding === 'boolean') { + rounding = options.rounding ? ROUNDING : 0; + } else { + rounding = this._options & ROUNDING; + } - // Free/new slots switcheroo! - this._freeSlots = newSlots; - this._newSlots = freeSlots; - }; - })(); + this._options = fillGaps | horizontal | alignRight | alignBottom | rounding; + }; /** - * Add a new rectangle to the rectangle store. Returns the id of the new - * rectangle. - * - * @private - * @memberof Packer.prototype - * @param {Number} left - * @param {Number} top + * @public + * @param {Grid} grid + * @param {Number} layoutId + * @param {Item[]} items * @param {Number} width * @param {Number} height - * @returns {RectId} + * @param {Function} callback + * @returns {?Function} */ - Packer.prototype._addRect = function(left, top, width, height) { - var rectId = ++this._rectId; - var rectStore = this._rectStore; + Packer.prototype.createLayout = function (grid, layoutId, items, width, height, callback) { + if (this._layouts[layoutId]) { + throw new Error('A layout with the provided id is currently being processed.'); + } - rectStore[rectId] = left || 0; - rectStore[++this._rectId] = top || 0; - rectStore[++this._rectId] = width || 0; - rectStore[++this._rectId] = height || 0; + var horizontal = this._options & HORIZONTAL; + var layout = { + id: layoutId, + items: items, + slots: null, + width: horizontal ? 0 : width, + height: !horizontal ? 0 : height, + // Temporary data, which will be removed before sending the layout data + // outside of Packer's context. + _grid: grid, + _settings: this._options, + }; - return rectId; - }; + // If there are no items let's call the callback immediately. + if (!items.length) { + layout.slots = []; + this._finalizeLayout(layout); + callback(layout); + return; + } - /** - * Get rectangle data from the rectangle store by id. Optionally you can - * provide a target object where the rectangle data will be written in. By - * default an internal object is reused as a target object. - * - * @private - * @memberof Packer.prototype - * @param {RectId} id - * @param {Object} [target] - * @returns {Object} - */ - Packer.prototype._getRect = function(id, target) { - var rectItem = target ? target : this._rectItem; - var rectStore = this._rectStore; + // Create layout synchronously if needed. + if (this._processor) { + layout.slots = window.Float32Array + ? new Float32Array(items.length * 2) + : new Array(items.length * 2); + this._processor.computeLayout(layout, layout._settings); + this._finalizeLayout(layout); + callback(layout); + return; + } - rectItem.left = rectStore[id] || 0; - rectItem.top = rectStore[++id] || 0; - rectItem.width = rectStore[++id] || 0; - rectItem.height = rectStore[++id] || 0; + // Worker data. + var data = new Float32Array(PACKET_HEADER_SLOTS + items.length * 2); - return rectItem; - }; + // Worker data header. + data[PACKET_INDEX_ID] = layoutId; + data[PACKET_INDEX_WIDTH] = layout.width; + data[PACKET_INDEX_HEIGHT] = layout.height; + data[PACKET_INDEX_OPTIONS] = layout._settings; - /** - * Punch a hole into a rectangle and split the remaining area into smaller - * rectangles (4 at max). - * - * @private - * @memberof Packer.prototype - * @param {Rectangle} rect - * @param {Rectangle} hole - * @returns {RectId[]} - */ - Packer.prototype._splitRect = (function() { - var results = []; - return function(rect, hole) { - // Reset old results. - results.length = 0; - - // If the rect does not overlap with the hole add rect to the return data - // as is. - if (!this._doRectsOverlap(rect, hole)) { - results.push(this._addRect(rect.left, rect.top, rect.width, rect.height)); - return results; - } - - // Left split. - if (rect.left < hole.left) { - results.push(this._addRect(rect.left, rect.top, hole.left - rect.left, rect.height)); - } - - // Right split. - if (rect.left + rect.width > hole.left + hole.width) { - results.push( - this._addRect( - hole.left + hole.width, - rect.top, - rect.left + rect.width - (hole.left + hole.width), - rect.height - ) - ); - } + // Worker data items. + var i, j, item; + for (i = 0, j = PACKET_HEADER_SLOTS - 1, item; i < items.length; i++) { + item = items[i]; + data[++j] = item._width + item._marginLeft + item._marginRight; + data[++j] = item._height + item._marginTop + item._marginBottom; + } - // Top split. - if (rect.top < hole.top) { - results.push(this._addRect(rect.left, rect.top, rect.width, hole.top - rect.top)); - } + this._layoutQueue.push(layoutId); + this._layouts[layoutId] = layout; + this._layoutCallbacks[layoutId] = callback; + this._layoutWorkerData[layoutId] = data; - // Bottom split. - if (rect.top + rect.height > hole.top + hole.height) { - results.push( - this._addRect( - rect.left, - hole.top + hole.height, - rect.width, - rect.top + rect.height - (hole.top + hole.height) - ) - ); - } + this._sendToWorker(); - return results; - }; - })(); + return this.cancelLayout.bind(this, layoutId); + }; /** - * Check if two rectangles overlap. - * - * @private - * @memberof Packer.prototype - * @param {Rectangle} a - * @param {Rectangle} b - * @returns {Boolean} + * @public + * @param {Number} layoutId */ - Packer.prototype._doRectsOverlap = function(a, b) { - return !( - a.left + a.width <= b.left || - b.left + b.width <= a.left || - a.top + a.height <= b.top || - b.top + b.height <= a.top - ); + Packer.prototype.cancelLayout = function (layoutId) { + var layout = this._layouts[layoutId]; + if (!layout) return; + + delete this._layouts[layoutId]; + delete this._layoutCallbacks[layoutId]; + + if (this._layoutWorkerData[layoutId]) { + delete this._layoutWorkerData[layoutId]; + var queueIndex = this._layoutQueue.indexOf(layoutId); + if (queueIndex > -1) this._layoutQueue.splice(queueIndex, 1); + } }; /** - * Check if a rectangle is fully within another rectangle. - * - * @private - * @memberof Packer.prototype - * @param {Rectangle} a - * @param {Rectangle} b - * @returns {Boolean} + * @public */ - Packer.prototype._isRectWithinRect = function(a, b) { - return ( - a.left >= b.left && - a.top >= b.top && - a.left + a.width <= b.left + b.width && - a.top + a.height <= b.top + b.height - ); - }; + Packer.prototype.destroy = function () { + // Move all currently used workers back in the workers array. + for (var key in this._layoutWorkers) { + this._workers.push(this._layoutWorkers[key]); + } - /** - * Loops through an array of rectangle ids and resets all that are fully - * within another rectangle in the array. Resetting in this case means that - * the rectangle id value is replaced with zero. - * - * @private - * @memberof Packer.prototype - * @param {RectId[]} rectIds - * @returns {RectId[]} - */ - Packer.prototype._purgeRects = (function() { - var rectA = {}; - var rectB = {}; - return function(rectIds) { - var i = rectIds.length; - var ii; - - while (i--) { - ii = rectIds.length; - if (!rectIds[i]) continue; - this._getRect(rectIds[i], rectA); - while (ii--) { - if (!rectIds[ii] || i === ii) continue; - if (this._isRectWithinRect(rectA, this._getRect(rectIds[ii], rectB))) { - rectIds[i] = 0; - break; - } - } - } + // Destroy all instance's workers. + destroyWorkerProcessors(this._workers); - return rectIds; - }; - })(); + // Reset data. + this._workers.length = 0; + this._layoutQueue.length = 0; + this._layouts = {}; + this._layoutCallbacks = {}; + this._layoutWorkers = {}; + this._layoutWorkerData = {}; + }; + + var debounceId = 0; /** - * Sort rectangles with top-left gravity. + * Returns a function, that, as long as it continues to be invoked, will not + * be triggered. The function will be called after it stops being called for + * N milliseconds. The returned function accepts one argument which, when + * being `true`, cancels the debounce function immediately. When the debounce + * function is canceled it cannot be invoked again. * - * @private - * @memberof Packer.prototype - * @param {RectId} aId - * @param {RectId} bId - * @returns {Number} + * @param {Function} fn + * @param {Number} durationMs + * @returns {Function} */ - Packer.prototype._sortRectsTopLeft = (function() { - var rectA = {}; - var rectB = {}; - return function(aId, bId) { - this._getRect(aId, rectA); - this._getRect(bId, rectB); - // prettier-ignore - return rectA.top < rectB.top ? -1 : - rectA.top > rectB.top ? 1 : - rectA.left < rectB.left ? -1 : - rectA.left > rectB.left ? 1 : 0; + function debounce(fn, durationMs) { + var id = ++debounceId; + var timer = 0; + var lastTime = 0; + var isCanceled = false; + var tick = function (time) { + if (isCanceled) return; + + if (lastTime) timer -= time - lastTime; + lastTime = time; + + if (timer > 0) { + addDebounceTick(id, tick); + } else { + timer = lastTime = 0; + fn(); + } }; - })(); - /** - * Sort rectangles with left-top gravity. - * - * @private - * @memberof Packer.prototype - * @param {RectId} aId - * @param {RectId} bId - * @returns {Number} - */ - Packer.prototype._sortRectsLeftTop = (function() { - var rectA = {}; - var rectB = {}; - return function(aId, bId) { - this._getRect(aId, rectA); - this._getRect(bId, rectB); - // prettier-ignore - return rectA.left < rectB.left ? -1 : - rectA.left > rectB.left ? 1 : - rectA.top < rectB.top ? -1 : - rectA.top > rectB.top ? 1 : 0; + return function (cancel) { + if (isCanceled) return; + + if (durationMs <= 0) { + if (cancel !== true) fn(); + return; + } + + if (cancel === true) { + isCanceled = true; + timer = lastTime = 0; + tick = undefined; + cancelDebounceTick(id); + return; + } + + if (timer <= 0) { + timer = durationMs; + tick(0); + } else { + timer = durationMs; + } }; - })(); + } var htmlCollectionType = '[object HTMLCollection]'; var nodeListType = '[object NodeList]'; /** - * Check if a value is a node list + * Check if a value is a node list or a html collection. * * @param {*} val * @returns {Boolean} @@ -5345,22 +7251,22 @@ return typeof val === objectType && toString.call(val) === objectToStringType; } + function noop() {} + /** * Converts a value to an array or clones an array. * - * @param {*} target + * @param {*} val * @returns {Array} */ - function toArray(target) { - return isNodeList(target) ? Array.prototype.slice.call(target) : Array.prototype.concat(target); + function toArray(val) { + return isNodeList(val) ? Array.prototype.slice.call(val) : Array.prototype.concat(val); } - var packer = new Packer(); - var noop = function() {}; - - var numberType$1 = 'number'; - var stringType = 'string'; - var instantLayout = 'instant'; + var NUMBER_TYPE = 'number'; + var STRING_TYPE = 'string'; + var INSTANT_LAYOUT = 'instant'; + var layoutId = 0; /** * Creates a new Grid instance. @@ -5368,31 +7274,31 @@ * @class * @param {(HTMLElement|String)} element * @param {Object} [options] - * @param {?(HTMLElement[]|NodeList|String)} [options.items] + * @param {(String|HTMLElement[]|NodeList|HTMLCollection)} [options.items="*"] * @param {Number} [options.showDuration=300] * @param {String} [options.showEasing="ease"] - * @param {Object} [options.visibleStyles] + * @param {Object} [options.visibleStyles={opacity: "1", transform: "scale(1)"}] * @param {Number} [options.hideDuration=300] * @param {String} [options.hideEasing="ease"] - * @param {Object} [options.hiddenStyles] + * @param {Object} [options.hiddenStyles={opacity: "0", transform: "scale(0.5)"}] * @param {(Function|Object)} [options.layout] * @param {Boolean} [options.layout.fillGaps=false] * @param {Boolean} [options.layout.horizontal=false] * @param {Boolean} [options.layout.alignRight=false] * @param {Boolean} [options.layout.alignBottom=false] - * @param {Boolean} [options.layout.rounding=true] - * @param {(Boolean|Number)} [options.layoutOnResize=100] + * @param {Boolean} [options.layout.rounding=false] + * @param {(Boolean|Number)} [options.layoutOnResize=150] * @param {Boolean} [options.layoutOnInit=true] * @param {Number} [options.layoutDuration=300] * @param {String} [options.layoutEasing="ease"] * @param {?Object} [options.sortData=null] * @param {Boolean} [options.dragEnabled=false] + * @param {?String} [options.dragHandle=null] * @param {?HtmlElement} [options.dragContainer=null] * @param {?Function} [options.dragStartPredicate] * @param {Number} [options.dragStartPredicate.distance=0] * @param {Number} [options.dragStartPredicate.delay=0] - * @param {(Boolean|String)} [options.dragStartPredicate.handle=false] - * @param {?String} [options.dragAxis] + * @param {String} [options.dragAxis="xy"] * @param {(Boolean|Function)} [options.dragSort=true] * @param {Object} [options.dragSortHeuristics] * @param {Number} [options.dragSortHeuristics.sortInterval=100] @@ -5401,16 +7307,27 @@ * @param {(Function|Object)} [options.dragSortPredicate] * @param {Number} [options.dragSortPredicate.threshold=50] * @param {String} [options.dragSortPredicate.action="move"] - * @param {Number} [options.dragReleaseDuration=300] - * @param {String} [options.dragReleaseEasing="ease"] + * @param {String} [options.dragSortPredicate.migrateAction="move"] + * @param {Object} [options.dragRelease] + * @param {Number} [options.dragRelease.duration=300] + * @param {String} [options.dragRelease.easing="ease"] + * @param {Boolean} [options.dragRelease.useDragContainer=true] * @param {Object} [options.dragCssProps] * @param {Object} [options.dragPlaceholder] * @param {Boolean} [options.dragPlaceholder.enabled=false] - * @param {Number} [options.dragPlaceholder.duration=300] - * @param {String} [options.dragPlaceholder.easing="ease"] * @param {?Function} [options.dragPlaceholder.createElement=null] * @param {?Function} [options.dragPlaceholder.onCreate=null] * @param {?Function} [options.dragPlaceholder.onRemove=null] + * @param {Object} [options.dragAutoScroll] + * @param {(Function|Array)} [options.dragAutoScroll.targets=[]] + * @param {?Function} [options.dragAutoScroll.handle=null] + * @param {Number} [options.dragAutoScroll.threshold=50] + * @param {Number} [options.dragAutoScroll.safeZone=0.2] + * @param {(Function|Number)} [options.dragAutoScroll.speed] + * @param {Boolean} [options.dragAutoScroll.sortDuringScroll=true] + * @param {Boolean} [options.dragAutoScroll.smoothStop=false] + * @param {?Function} [options.dragAutoScroll.onStart=null] + * @param {?Function} [options.dragAutoScroll.onStop=null] * @param {String} [options.containerClass="muuri"] * @param {String} [options.itemClass="muuri-item"] * @param {String} [options.itemVisibleClass="muuri-item-visible"] @@ -5420,87 +7337,56 @@ * @param {String} [options.itemReleasingClass="muuri-item-releasing"] * @param {String} [options.itemPlaceholderClass="muuri-item-placeholder"] */ - function Grid(element, options) { - var inst = this; - var settings; - var items; - var layoutOnResize; - - // Allow passing element as selector string. Store element for instance. - element = this._element = - typeof element === stringType ? window.document.querySelector(element) : element; + // Allow passing element as selector string + if (typeof element === STRING_TYPE) { + element = document.querySelector(element); + } // Throw an error if the container element is not body element or does not // exist within the body element. var isElementInDom = element.getRootNode ? element.getRootNode({ composed: true }) === document - : window.document.body.contains(element); - if (!isElementInDom || element === window.document.documentElement) { - throw new Error('Container element must be an existing DOM element'); + : document.body.contains(element); + if (!isElementInDom || element === document.documentElement) { + throw new Error('Container element must be an existing DOM element.'); } // Create instance settings by merging the options with default options. - settings = this._settings = mergeSettings(Grid.defaultOptions, options); - - // Sanitize dragSort setting. + var settings = mergeSettings(Grid.defaultOptions, options); + settings.visibleStyles = normalizeStyles(settings.visibleStyles); + settings.hiddenStyles = normalizeStyles(settings.hiddenStyles); if (!isFunction(settings.dragSort)) { settings.dragSort = !!settings.dragSort; } - // Create instance id and store it to the grid instances collection. this._id = createUid(); - gridInstances[this._id] = inst; - - // Destroyed flag. + this._element = element; + this._settings = settings; this._isDestroyed = false; - - // The layout object (mutated on every layout). + this._items = []; this._layout = { id: 0, items: [], slots: [], - setWidth: false, - setHeight: false, - width: 0, - height: 0 }; - - // Create private Emitter instance. + this._isLayoutFinished = true; + this._nextLayoutData = null; this._emitter = new Emitter(); + this._onLayoutDataReceived = this._onLayoutDataReceived.bind(this); + + // Store grid instance to the grid instances collection. + GRID_INSTANCES[this._id] = this; // Add container element's class name. addClass(element, settings.containerClass); - // Create initial items. - this._items = []; - items = settings.items; - if (typeof items === stringType) { - toArray(element.children).forEach(function(itemElement) { - if (items === '*' || elementMatches(itemElement, items)) { - inst._items.push(new Item(inst, itemElement)); - } - }); - } else if (Array.isArray(items) || isNodeList(items)) { - this._items = toArray(items).map(function(itemElement) { - return new Item(inst, itemElement); - }); - } - // If layoutOnResize option is a valid number sanitize it and bind the resize // handler. - layoutOnResize = settings.layoutOnResize; - if (typeof layoutOnResize !== numberType$1) { - layoutOnResize = layoutOnResize === true ? 0 : -1; - } - if (layoutOnResize >= 0) { - window.addEventListener( - 'resize', - (inst._resizeHandler = debounce(function() { - inst.refreshItems().layout(); - }, layoutOnResize)) - ); - } + bindLayoutOnResize(this, settings.layoutOnResize); + + // Add initial items. + this.add(getInitialGridElements(element, settings.items), { layout: false }); // Layout on init if necessary. if (settings.layoutOnInit) { @@ -5514,68 +7400,107 @@ */ /** + * @public + * @static * @see Item */ Grid.Item = Item; /** + * @public + * @static * @see ItemLayout */ Grid.ItemLayout = ItemLayout; /** + * @public + * @static * @see ItemVisibility */ Grid.ItemVisibility = ItemVisibility; /** + * @public + * @static * @see ItemMigrate */ Grid.ItemMigrate = ItemMigrate; /** - * @see ItemAnimate - */ - Grid.ItemAnimate = ItemAnimate; - - /** + * @public + * @static * @see ItemDrag */ Grid.ItemDrag = ItemDrag; /** - * @see ItemRelease + * @public + * @static + * @see ItemDragRelease */ - Grid.ItemRelease = ItemRelease; + Grid.ItemDragRelease = ItemDragRelease; /** + * @public + * @static * @see ItemDragPlaceholder */ Grid.ItemDragPlaceholder = ItemDragPlaceholder; /** + * @public + * @static * @see Emitter */ Grid.Emitter = Emitter; /** + * @public + * @static + * @see Animator + */ + Grid.Animator = Animator; + + /** + * @public + * @static * @see Dragger */ Grid.Dragger = Dragger; /** + * @public + * @static * @see Packer */ Grid.Packer = Packer; + /** + * @public + * @static + * @see AutoScroller + */ + Grid.AutoScroller = AutoScroller; + + /** + * The default Packer instance used by default for all layouts. + * + * @public + * @static + * @type {Packer} + */ + Grid.defaultPacker = new Packer(2); + /** * Default options for Grid instance. * * @public - * @memberof Grid + * @static + * @type {Object} */ Grid.defaultOptions = { - // Item elements + // Initial item elements items: '*', // Default show animation @@ -5589,11 +7514,11 @@ // Item's visible/hidden state styles visibleStyles: { opacity: '1', - transform: 'scale(1)' + transform: 'scale(1)', }, hiddenStyles: { opacity: '0', - transform: 'scale(0.5)' + transform: 'scale(0.5)', }, // Layout @@ -5602,9 +7527,9 @@ horizontal: false, alignRight: false, alignBottom: false, - rounding: true + rounding: false, }, - layoutOnResize: 100, + layoutOnResize: 150, layoutOnInit: true, layoutDuration: 300, layoutEasing: 'ease', @@ -5615,39 +7540,52 @@ // Drag & Drop dragEnabled: false, dragContainer: null, + dragHandle: null, dragStartPredicate: { distance: 0, delay: 0, - handle: false }, - dragAxis: null, + dragAxis: 'xy', dragSort: true, dragSortHeuristics: { sortInterval: 100, minDragDistance: 10, - minBounceBackAngle: 1 + minBounceBackAngle: 1, }, dragSortPredicate: { threshold: 50, - action: actionMove + action: ACTION_MOVE, + migrateAction: ACTION_MOVE, + }, + dragRelease: { + duration: 300, + easing: 'ease', + useDragContainer: true, }, - dragReleaseDuration: 300, - dragReleaseEasing: 'ease', dragCssProps: { touchAction: 'none', userSelect: 'none', userDrag: 'none', tapHighlightColor: 'rgba(0, 0, 0, 0)', touchCallout: 'none', - contentZooming: 'none' + contentZooming: 'none', }, dragPlaceholder: { enabled: false, - duration: 300, - easing: 'ease', createElement: null, onCreate: null, - onRemove: null + onRemove: null, + }, + dragAutoScroll: { + targets: [], + handle: null, + threshold: 50, + safeZone: 0.2, + speed: AutoScroller.smoothSpeed(1000, 2000, 2500), + sortDuringScroll: true, + smoothStop: false, + onStart: null, + onStop: null, }, // Classnames @@ -5658,7 +7596,7 @@ itemPositioningClass: 'muuri-item-positioning', itemDraggingClass: 'muuri-item-dragging', itemReleasingClass: 'muuri-item-releasing', - itemPlaceholderClass: 'muuri-item-placeholder' + itemPlaceholderClass: 'muuri-item-placeholder', }; /** @@ -5670,12 +7608,11 @@ * Bind an event listener. * * @public - * @memberof Grid.prototype * @param {String} event * @param {Function} listener * @returns {Grid} */ - Grid.prototype.on = function(event, listener) { + Grid.prototype.on = function (event, listener) { this._emitter.on(event, listener); return this; }; @@ -5684,12 +7621,11 @@ * Unbind an event listener. * * @public - * @memberof Grid.prototype * @param {String} event * @param {Function} listener * @returns {Grid} */ - Grid.prototype.off = function(event, listener) { + Grid.prototype.off = function (event, listener) { this._emitter.off(event, listener); return this; }; @@ -5698,82 +7634,155 @@ * Get the container element. * * @public - * @memberof Grid.prototype * @returns {HTMLElement} */ - Grid.prototype.getElement = function() { + Grid.prototype.getElement = function () { return this._element; }; /** - * Get all items. Optionally you can provide specific targets (elements and - * indices). Note that the returned array is not the same object used by the - * instance so modifying it will not affect instance's items. All items that - * are not found are omitted from the returned array. + * Get instance's item by element or by index. Target can also be an Item + * instance in which case the function returns the item if it exists within + * related Grid instance. If nothing is found with the provided target, null + * is returned. + * + * @private + * @param {(HtmlElement|Number|Item)} [target] + * @returns {?Item} + */ + Grid.prototype.getItem = function (target) { + // If no target is specified or the instance is destroyed, return null. + if (this._isDestroyed || (!target && target !== 0)) { + return null; + } + + // If target is number return the item in that index. If the number is lower + // than zero look for the item starting from the end of the items array. For + // example -1 for the last item, -2 for the second last item, etc. + if (typeof target === NUMBER_TYPE) { + return this._items[target > -1 ? target : this._items.length + target] || null; + } + + // If the target is an instance of Item return it if it is attached to this + // Grid instance, otherwise return null. + if (target instanceof Item) { + return target._gridId === this._id ? target : null; + } + + // In other cases let's assume that the target is an element, so let's try + // to find an item that matches the element and return it. If item is not + // found return null. + if (ITEM_ELEMENT_MAP) { + var item = ITEM_ELEMENT_MAP.get(target); + return item && item._gridId === this._id ? item : null; + } else { + for (var i = 0; i < this._items.length; i++) { + if (this._items[i]._element === target) { + return this._items[i]; + } + } + } + + return null; + }; + + /** + * Get all items. Optionally you can provide specific targets (elements, + * indices and item instances). All items that are not found are omitted from + * the returned array. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} [targets] + * @param {(HtmlElement|Number|Item|Array)} [targets] * @returns {Item[]} */ - Grid.prototype.getItems = function(targets) { + Grid.prototype.getItems = function (targets) { // Return all items immediately if no targets were provided or if the // instance is destroyed. - if (this._isDestroyed || (!targets && targets !== 0)) { + if (this._isDestroyed || targets === undefined) { return this._items.slice(0); } - var ret = []; - var targetItems = toArray(targets); - var item; - var i; + var items = []; + var i, item; - // If target items are defined return filtered results. - for (i = 0; i < targetItems.length; i++) { - item = this._getItem(targetItems[i]); - item && ret.push(item); + if (Array.isArray(targets) || isNodeList(targets)) { + for (i = 0; i < targets.length; i++) { + item = this.getItem(targets[i]); + if (item) items.push(item); + } + } else { + item = this.getItem(targets); + if (item) items.push(item); } - return ret; + return items; }; /** - * Update the cached dimensions of the instance's items. + * Update the cached dimensions of the instance's items. By default all the + * items are refreshed, but you can also provide an array of target items as the + * first argument if you want to refresh specific items. Note that all hidden + * items are not refreshed by default since their "display" property is "none" + * and their dimensions are therefore not readable from the DOM. However, if you + * do want to force update hidden item dimensions too you can provide `true` + * as the second argument, which makes the elements temporarily visible while + * their dimensions are being read. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} [items] + * @param {Item[]} [items] + * @param {Boolean} [force=false] * @returns {Grid} */ - Grid.prototype.refreshItems = function(items) { + Grid.prototype.refreshItems = function (items, force) { if (this._isDestroyed) return this; - var targets = this.getItems(items); - var i; + var targets = items || this._items; + var i, item, style, hiddenItemStyles; + + if (force === true) { + hiddenItemStyles = []; + for (i = 0; i < targets.length; i++) { + item = targets[i]; + if (!item.isVisible() && !item.isHiding()) { + style = item.getElement().style; + style.visibility = 'hidden'; + style.display = ''; + hiddenItemStyles.push(style); + } + } + } for (i = 0; i < targets.length; i++) { - targets[i]._refreshDimensions(); + targets[i]._refreshDimensions(force); + } + + if (force === true) { + for (i = 0; i < hiddenItemStyles.length; i++) { + style = hiddenItemStyles[i]; + style.visibility = ''; + style.display = 'none'; + } + hiddenItemStyles.length = 0; } return this; }; /** - * Update the sort data of the instance's items. + * Update the sort data of the instance's items. By default all the items are + * refreshed, but you can also provide an array of target items if you want to + * refresh specific items. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} [items] + * @param {Item[]} [items] * @returns {Grid} */ - Grid.prototype.refreshSortData = function(items) { + Grid.prototype.refreshSortData = function (items) { if (this._isDestroyed) return this; - var targetItems = this.getItems(items); - var i; - - for (i = 0; i < targetItems.length; i++) { - targetItems[i]._refreshSortData(); + var targets = items || this._items; + for (var i = 0; i < targets.length; i++) { + targets[i]._refreshSortData(); } return this; @@ -5787,33 +7796,29 @@ * left untouched. * * @public - * @memberof Grid.prototype * @returns {Grid} */ - Grid.prototype.synchronize = function() { + Grid.prototype.synchronize = function () { if (this._isDestroyed) return this; - var container = this._element; var items = this._items; + if (!items.length) return this; + var fragment; var element; - var i; - // Append all elements in order to the container element. - if (items.length) { - for (i = 0; i < items.length; i++) { - element = items[i]._element; - if (element.parentNode === container) { - fragment = fragment || window.document.createDocumentFragment(); - fragment.appendChild(element); - } + for (var i = 0; i < items.length; i++) { + element = items[i]._element; + if (element.parentNode === this._element) { + fragment = fragment || document.createDocumentFragment(); + fragment.appendChild(element); } - - if (fragment) container.appendChild(fragment); } - // Emit synchronize event. - this._emit(eventSynchronize); + if (!fragment) return this; + + this._element.appendChild(fragment); + this._emit(EVENT_SYNCHRONIZE); return this; }; @@ -5822,97 +7827,72 @@ * Calculate and apply item positions. * * @public - * @memberof Grid.prototype * @param {Boolean} [instant=false] - * @param {LayoutCallback} [onFinish] + * @param {Function} [onFinish] * @returns {Grid} - */ - Grid.prototype.layout = function(instant, onFinish) { - if (this._isDestroyed) return this; - - var inst = this; - var element = this._element; - var layout = this._updateLayout(); - var layoutId = layout.id; - var itemsLength = layout.items.length; - var counter = itemsLength; - var isBorderBox; - var item; - var i; - - // The finish function, which will be used for checking if all the items - // have laid out yet. After all items have finished their animations call - // callback and emit layoutEnd event. Only emit layoutEnd event if there - // hasn't been a new layout call during this layout. - function tryFinish() { - if (--counter > 0) return; - - var hasLayoutChanged = inst._layout.id !== layoutId; - var callback = isFunction(instant) ? instant : onFinish; - - if (isFunction(callback)) { - callback(hasLayoutChanged, layout.items.slice(0)); - } - - if (!hasLayoutChanged && inst._hasListeners(eventLayoutEnd)) { - inst._emit(eventLayoutEnd, layout.items.slice(0)); - } - } - - // If grid's width or height was modified, we need to update it's cached - // dimensions. Also keep in mind that grid's cached width/height should - // always equal to what elem.getBoundingClientRect() would return, so - // therefore we need to add the grid element's borders to the dimensions if - // it's box-sizing is border-box. Note that we support providing the - // dimensions as a string here too so that one can define the unit of the - // dimensions, in which case we don't do the border-box check. - if ( - (layout.setHeight && typeof layout.height === numberType$1) || - (layout.setWidth && typeof layout.width === numberType$1) - ) { - isBorderBox = getStyle(element, 'box-sizing') === 'border-box'; - } - if (layout.setHeight) { - if (typeof layout.height === numberType$1) { - element.style.height = - (isBorderBox ? layout.height + this._borderTop + this._borderBottom : layout.height) + 'px'; - } else { - element.style.height = layout.height; - } - } - if (layout.setWidth) { - if (typeof layout.width === numberType$1) { - element.style.width = - (isBorderBox ? layout.width + this._borderLeft + this._borderRight : layout.width) + 'px'; - } else { - element.style.width = layout.width; - } - } + */ + Grid.prototype.layout = function (instant, onFinish) { + if (this._isDestroyed) return this; - // Emit layoutStart event. Note that this is intentionally emitted after the - // container element's dimensions are set, because otherwise there would be - // no hook for reacting to container dimension changes. - if (this._hasListeners(eventLayoutStart)) { - this._emit(eventLayoutStart, layout.items.slice(0)); + // Cancel unfinished layout algorithm if possible. + var unfinishedLayout = this._nextLayoutData; + if (unfinishedLayout && isFunction(unfinishedLayout.cancel)) { + unfinishedLayout.cancel(); } - // If there are no items let's finish quickly. - if (!itemsLength) { - tryFinish(); - return this; - } + // Compute layout id (let's stay in Float32 range). + layoutId = (layoutId % MAX_SAFE_FLOAT32_INTEGER) + 1; + var nextLayoutId = layoutId; + + // Store data for next layout. + this._nextLayoutData = { + id: nextLayoutId, + instant: instant, + onFinish: onFinish, + cancel: null, + }; - // If there are items let's position them. - for (i = 0; i < itemsLength; i++) { - item = layout.items[i]; - if (!item) continue; + // Collect layout items (all active grid items). + var items = this._items; + var layoutItems = []; + for (var i = 0; i < items.length; i++) { + if (items[i]._isActive) layoutItems.push(items[i]); + } - // Update item's position. - item._left = layout.slots[i * 2]; - item._top = layout.slots[i * 2 + 1]; + // Compute new layout. + this._refreshDimensions(); + var gridWidth = this._width - this._borderLeft - this._borderRight; + var gridHeight = this._height - this._borderTop - this._borderBottom; + var layoutSettings = this._settings.layout; + var cancelLayout; + if (isFunction(layoutSettings)) { + cancelLayout = layoutSettings( + this, + nextLayoutId, + layoutItems, + gridWidth, + gridHeight, + this._onLayoutDataReceived + ); + } else { + Grid.defaultPacker.setOptions(layoutSettings); + cancelLayout = Grid.defaultPacker.createLayout( + this, + nextLayoutId, + layoutItems, + gridWidth, + gridHeight, + this._onLayoutDataReceived + ); + } - // Layout item if it is not dragged. - item.isDragging() ? tryFinish() : item._layout.start(instant === true, tryFinish); + // Store layout cancel method if available. + if ( + isFunction(cancelLayout) && + this._nextLayoutData && + this._nextLayoutData.id === nextLayoutId + ) { + this._nextLayoutData.cancel = cancelLayout; } return this; @@ -5932,31 +7912,49 @@ * are positioned without animation during their first layout. * * @public - * @memberof Grid.prototype * @param {(HTMLElement|HTMLElement[])} elements * @param {Object} [options] * @param {Number} [options.index=-1] - * @param {Boolean} [options.isActive] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {Boolean} [options.active] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Item[]} */ - Grid.prototype.add = function(elements, options) { + Grid.prototype.add = function (elements, options) { if (this._isDestroyed || !elements) return []; var newItems = toArray(elements); if (!newItems.length) return newItems; - var opts = options || 0; + var opts = options || {}; var layout = opts.layout ? opts.layout : opts.layout === undefined; var items = this._items; var needsLayout = false; + var fragment; + var element; var item; var i; + // Collect all the elements that are not child of the grid element into a + // document fragment. + for (i = 0; i < newItems.length; i++) { + element = newItems[i]; + if (element.parentNode !== this._element) { + fragment = fragment || document.createDocumentFragment(); + fragment.appendChild(element); + } + } + + // If we have a fragment, let's append it to the grid element. We could just + // not do this and the `new Item()` instantiation would handle this for us, + // but this way we can add the elements into the DOM a bit faster. + if (fragment) { + this._element.appendChild(fragment); + } + // Map provided elements into new grid items. for (i = 0; i < newItems.length; i++) { - item = new Item(this, newItems[i], opts.isActive); - newItems[i] = item; + element = newItems[i]; + item = newItems[i] = new Item(this, element, opts.active); // If the item to be added is active, we need to do a layout. Also, we // need to mark the item with the skipNextAnimation flag to make it @@ -5969,17 +7967,25 @@ } } + // Set up the items' initial dimensions and sort data. This needs to be done + // in a separate loop to avoid layout thrashing. + for (i = 0; i < newItems.length; i++) { + item = newItems[i]; + item._refreshDimensions(); + item._refreshSortData(); + } + // Add the new items to the items collection to correct index. arrayInsert(items, newItems, opts.index); // Emit add event. - if (this._hasListeners(eventAdd)) { - this._emit(eventAdd, newItems.slice(0)); + if (this._hasListeners(EVENT_ADD)) { + this._emit(EVENT_ADD, newItems.slice(0)); } // If layout is needed. if (needsLayout && layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } return newItems; @@ -5989,79 +7995,89 @@ * Remove items from the instance. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} items + * @param {Item[]} items * @param {Object} [options] * @param {Boolean} [options.removeElements=false] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Item[]} */ - Grid.prototype.remove = function(items, options) { - if (this._isDestroyed) return this; + Grid.prototype.remove = function (items, options) { + if (this._isDestroyed || !items.length) return []; - var opts = options || 0; + var opts = options || {}; var layout = opts.layout ? opts.layout : opts.layout === undefined; var needsLayout = false; var allItems = this.getItems(); - var targetItems = this.getItems(items); + var targetItems = []; var indices = []; + var index; var item; var i; // Remove the individual items. - for (i = 0; i < targetItems.length; i++) { - item = targetItems[i]; - indices.push(allItems.indexOf(item)); + for (i = 0; i < items.length; i++) { + item = items[i]; + if (item._isDestroyed) continue; + + index = this._items.indexOf(item); + if (index === -1) continue; + if (item._isActive) needsLayout = true; + + targetItems.push(item); + indices.push(allItems.indexOf(item)); item._destroy(opts.removeElements); + this._items.splice(index, 1); } // Emit remove event. - if (this._hasListeners(eventRemove)) { - this._emit(eventRemove, targetItems.slice(0), indices); + if (this._hasListeners(EVENT_REMOVE)) { + this._emit(EVENT_REMOVE, targetItems.slice(0), indices); } // If layout is needed. if (needsLayout && layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } return targetItems; }; /** - * Show instance items. + * Show specific instance items. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} items + * @param {Item[]} items * @param {Object} [options] * @param {Boolean} [options.instant=false] - * @param {ShowCallback} [options.onFinish] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {Boolean} [options.syncWithLayout=true] + * @param {Function} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ - Grid.prototype.show = function(items, options) { - if (this._isDestroyed) return this; - this._setItemsVisibility(items, true, options); + Grid.prototype.show = function (items, options) { + if (!this._isDestroyed && items.length) { + this._setItemsVisibility(items, true, options); + } return this; }; /** - * Hide instance items. + * Hide specific instance items. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} items + * @param {Item[]} items * @param {Object} [options] * @param {Boolean} [options.instant=false] - * @param {HideCallback} [options.onFinish] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {Boolean} [options.syncWithLayout=true] + * @param {Function} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ - Grid.prototype.hide = function(items, options) { - if (this._isDestroyed) return this; - this._setItemsVisibility(items, false, options); + Grid.prototype.hide = function (items, options) { + if (!this._isDestroyed && items.length) { + this._setItemsVisibility(items, false, options); + } return this; }; @@ -6076,23 +8092,24 @@ * matching items will be shown and others hidden. * * @public - * @memberof Grid.prototype * @param {(Function|String)} predicate * @param {Object} [options] * @param {Boolean} [options.instant=false] + * @param {Boolean} [options.syncWithLayout=true] * @param {FilterCallback} [options.onFinish] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ - Grid.prototype.filter = function(predicate, options) { + Grid.prototype.filter = function (predicate, options) { if (this._isDestroyed || !this._items.length) return this; var itemsToShow = []; var itemsToHide = []; - var isPredicateString = typeof predicate === stringType; + var isPredicateString = typeof predicate === STRING_TYPE; var isPredicateFn = isFunction(predicate); - var opts = options || 0; + var opts = options || {}; var isInstant = opts.instant === true; + var syncWithLayout = opts.syncWithLayout; var layout = opts.layout ? opts.layout : opts.layout === undefined; var onFinish = isFunction(opts.onFinish) ? opts.onFinish : null; var tryFinishCounter = -1; @@ -6102,7 +8119,7 @@ // If we have onFinish callback, let's create proper tryFinish callback. if (onFinish) { - tryFinish = function() { + tryFinish = function () { ++tryFinishCounter && onFinish(itemsToShow.slice(0), itemsToHide.slice(0)); }; } @@ -6123,8 +8140,9 @@ if (itemsToShow.length) { this.show(itemsToShow, { instant: isInstant, + syncWithLayout: syncWithLayout, onFinish: tryFinish, - layout: false + layout: false, }); } else { tryFinish(); @@ -6134,8 +8152,9 @@ if (itemsToHide.length) { this.hide(itemsToHide, { instant: isInstant, + syncWithLayout: syncWithLayout, onFinish: tryFinish, - layout: false + layout: false, }); } else { tryFinish(); @@ -6144,13 +8163,13 @@ // If there are any items to filter. if (itemsToShow.length || itemsToHide.length) { // Emit filter event. - if (this._hasListeners(eventFilter)) { - this._emit(eventFilter, itemsToShow.slice(0), itemsToHide.slice(0)); + if (this._hasListeners(EVENT_FILTER)) { + this._emit(EVENT_FILTER, itemsToShow.slice(0), itemsToHide.slice(0)); } // If layout is needed. if (layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } } @@ -6168,42 +8187,18 @@ * same order. * * @public - * @memberof Grid.prototype - * @param {(Function|Item[]|String|String[])} comparer + * @param {(Function|String|Item[])} comparer * @param {Object} [options] * @param {Boolean} [options.descending=false] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ - Grid.prototype.sort = (function() { + Grid.prototype.sort = (function () { var sortComparer; var isDescending; var origItems; var indexMap; - function parseCriteria(data) { - return data - .trim() - .split(' ') - .map(function(val) { - return val.split(':'); - }); - } - - function getIndexMap(items) { - var ret = {}; - for (var i = 0; i < items.length; i++) { - ret[items[i]._id] = i; - } - return ret; - } - - function compareIndices(itemA, itemB) { - var indexA = indexMap[itemA._id]; - var indexB = indexMap[itemB._id]; - return isDescending ? indexB - indexA : indexA - indexB; - } - function defaultComparer(a, b) { var result = 0; var criteriaName; @@ -6235,80 +8230,83 @@ } // If values are equal let's compare the item indices to make sure we - // have a stable sort. + // have a stable sort. Note that this is not necessary in evergreen browsers + // because Array.sort() is nowadays stable. However, in order to guarantee + // same results in older browsers we need this. if (!result) { - if (!indexMap) indexMap = getIndexMap(origItems); - result = compareIndices(a, b); + if (!indexMap) indexMap = createIndexMap(origItems); + result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b); } return result; } function customComparer(a, b) { - var result = sortComparer(a, b); - // If descending let's invert the result value. - if (isDescending && result) result = -result; - // If we have a valid result (not zero) let's return it right away. - if (result) return result; - // If result is zero let's compare the item indices to make sure we have a - // stable sort. - if (!indexMap) indexMap = getIndexMap(origItems); - return compareIndices(a, b); - } - - return function(comparer, options) { + var result = isDescending ? -sortComparer(a, b) : sortComparer(a, b); + if (!result) { + if (!indexMap) indexMap = createIndexMap(origItems); + result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b); + } + return result; + } + + return function (comparer, options) { if (this._isDestroyed || this._items.length < 2) return this; var items = this._items; - var opts = options || 0; + var opts = options || {}; var layout = opts.layout ? opts.layout : opts.layout === undefined; - var i; // Setup parent scope data. - sortComparer = comparer; isDescending = !!opts.descending; origItems = items.slice(0); indexMap = null; // If function is provided do a native array sort. - if (isFunction(sortComparer)) { + if (isFunction(comparer)) { + sortComparer = comparer; items.sort(customComparer); } // Otherwise if we got a string, let's sort by the sort data as provided in // the instance's options. - else if (typeof sortComparer === stringType) { - sortComparer = parseCriteria(comparer); + else if (typeof comparer === STRING_TYPE) { + sortComparer = comparer + .trim() + .split(' ') + .filter(function (val) { + return val; + }) + .map(function (val) { + return val.split(':'); + }); items.sort(defaultComparer); } // Otherwise if we got an array, let's assume it's a presorted array of the - // items and order the items based on it. - else if (Array.isArray(sortComparer)) { - if (sortComparer.length !== items.length) { - throw new Error('[' + namespace + '] sort reference items do not match with grid items.'); - } - for (i = 0; i < items.length; i++) { - if (sortComparer.indexOf(items[i]) < 0) { - throw new Error('[' + namespace + '] sort reference items do not match with grid items.'); - } - items[i] = sortComparer[i]; - } - if (isDescending) items.reverse(); + // items and order the items based on it. Here we blindly trust that the + // presorted array consists of the same item instances as the current + // `gird._items` array. + else if (Array.isArray(comparer)) { + items.length = 0; + items.push.apply(items, comparer); } - // Otherwise let's just skip it, nothing we can do here. + // Otherwise let's throw an error. else { - /** @todo Maybe throw an error here? */ - return this; + sortComparer = isDescending = origItems = indexMap = null; + throw new Error('Invalid comparer argument provided.'); } // Emit sort event. - if (this._hasListeners(eventSort)) { - this._emit(eventSort, items.slice(0), origItems); + if (this._hasListeners(EVENT_SORT)) { + this._emit(EVENT_SORT, items.slice(0), origItems); } // If layout is needed. if (layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } + // Reset data (to avoid mem leaks). + sortComparer = isDescending = origItems = indexMap = null; + return this; }; })(); @@ -6317,27 +8315,26 @@ * Move item to another index or in place of another item. * * @public - * @memberof Grid.prototype - * @param {GridSingleItemQuery} item - * @param {GridSingleItemQuery} position + * @param {(HtmlElement|Number|Item)} item + * @param {(HtmlElement|Number|Item)} position * @param {Object} [options] * @param {String} [options.action="move"] * - Accepts either "move" or "swap". * - "move" moves the item in place of the other item. * - "swap" swaps the position of the items. - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ - Grid.prototype.move = function(item, position, options) { + Grid.prototype.move = function (item, position, options) { if (this._isDestroyed || this._items.length < 2) return this; var items = this._items; - var opts = options || 0; + var opts = options || {}; var layout = opts.layout ? opts.layout : opts.layout === undefined; - var isSwap = opts.action === actionSwap; - var action = isSwap ? actionSwap : actionMove; - var fromItem = this._getItem(item); - var toItem = this._getItem(position); + var isSwap = opts.action === ACTION_SWAP; + var action = isSwap ? ACTION_SWAP : ACTION_MOVE; + var fromItem = this.getItem(item); + var toItem = this.getItem(position); var fromIndex; var toIndex; @@ -6355,18 +8352,18 @@ } // Emit move event. - if (this._hasListeners(eventMove)) { - this._emit(eventMove, { + if (this._hasListeners(EVENT_MOVE)) { + this._emit(EVENT_MOVE, { item: fromItem, fromIndex: fromIndex, toIndex: toIndex, - action: action + action: action, }); } // If layout is needed. if (layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } } @@ -6377,45 +8374,44 @@ * Send item to another Grid instance. * * @public - * @memberof Grid.prototype - * @param {GridSingleItemQuery} item - * @param {Grid} grid - * @param {GridSingleItemQuery} position + * @param {(HtmlElement|Number|Item)} item + * @param {Grid} targetGrid + * @param {(HtmlElement|Number|Item)} position * @param {Object} [options] * @param {HTMLElement} [options.appendTo=document.body] - * @param {(Boolean|LayoutCallback|String)} [options.layoutSender=true] - * @param {(Boolean|LayoutCallback|String)} [options.layoutReceiver=true] + * @param {(Boolean|Function|String)} [options.layoutSender=true] + * @param {(Boolean|Function|String)} [options.layoutReceiver=true] * @returns {Grid} */ - Grid.prototype.send = function(item, grid, position, options) { - if (this._isDestroyed || grid._isDestroyed || this === grid) return this; + Grid.prototype.send = function (item, targetGrid, position, options) { + if (this._isDestroyed || targetGrid._isDestroyed || this === targetGrid) return this; // Make sure we have a valid target item. - item = this._getItem(item); + item = this.getItem(item); if (!item) return this; - var opts = options || 0; - var container = opts.appendTo || window.document.body; + var opts = options || {}; + var container = opts.appendTo || document.body; var layoutSender = opts.layoutSender ? opts.layoutSender : opts.layoutSender === undefined; var layoutReceiver = opts.layoutReceiver ? opts.layoutReceiver : opts.layoutReceiver === undefined; // Start the migration process. - item._migrate.start(grid, position, container); + item._migrate.start(targetGrid, position, container); // If migration was started successfully and the item is active, let's layout // the grids. if (item._migrate._isActive && item._isActive) { if (layoutSender) { this.layout( - layoutSender === instantLayout, + layoutSender === INSTANT_LAYOUT, isFunction(layoutSender) ? layoutSender : undefined ); } if (layoutReceiver) { - grid.layout( - layoutReceiver === instantLayout, + targetGrid.layout( + layoutReceiver === INSTANT_LAYOUT, isFunction(layoutReceiver) ? layoutReceiver : undefined ); } @@ -6428,38 +8424,34 @@ * Destroy the instance. * * @public - * @memberof Grid.prototype * @param {Boolean} [removeElements=false] * @returns {Grid} */ - Grid.prototype.destroy = function(removeElements) { + Grid.prototype.destroy = function (removeElements) { if (this._isDestroyed) return this; var container = this._element; var items = this._items.slice(0); - var i; + var layoutStyles = (this._layout && this._layout.styles) || {}; + var i, prop; // Unbind window resize event listener. - if (this._resizeHandler) { - window.removeEventListener('resize', this._resizeHandler); - } + unbindLayoutOnResize(this); // Destroy items. - for (i = 0; i < items.length; i++) { - items[i]._destroy(removeElements); - } + for (i = 0; i < items.length; i++) items[i]._destroy(removeElements); + this._items.length = 0; // Restore container. removeClass(container, this._settings.containerClass); - container.style.height = ''; - container.style.width = ''; + for (prop in layoutStyles) container.style[prop] = ''; // Emit destroy event and unbind all events. - this._emit(eventDestroy); + this._emit(EVENT_DESTROY); this._emitter.destroy(); // Remove reference from the grid instances collection. - gridInstances[this._id] = undefined; + delete GRID_INSTANCES[this._id]; // Flag instance as destroyed. this._isDestroyed = true; @@ -6472,106 +8464,14 @@ * ************************* */ - /** - * Get instance's item by element or by index. Target can also be an Item - * instance in which case the function returns the item if it exists within - * related Grid instance. If nothing is found with the provided target, null - * is returned. - * - * @private - * @memberof Grid.prototype - * @param {GridSingleItemQuery} [target] - * @returns {?Item} - */ - Grid.prototype._getItem = function(target) { - // If no target is specified or the instance is destroyed, return null. - if (this._isDestroyed || (!target && target !== 0)) { - return null; - } - - // If target is number return the item in that index. If the number is lower - // than zero look for the item starting from the end of the items array. For - // example -1 for the last item, -2 for the second last item, etc. - if (typeof target === numberType$1) { - return this._items[target > -1 ? target : this._items.length + target] || null; - } - - // If the target is an instance of Item return it if it is attached to this - // Grid instance, otherwise return null. - if (target instanceof Item) { - return target._gridId === this._id ? target : null; - } - - // In other cases let's assume that the target is an element, so let's try - // to find an item that matches the element and return it. If item is not - // found return null. - /** @todo This could be made a lot faster by using Map/WeakMap of elements. */ - for (var i = 0; i < this._items.length; i++) { - if (this._items[i]._element === target) { - return this._items[i]; - } - } - - return null; - }; - - /** - * Recalculates and updates instance's layout data. - * - * @private - * @memberof Grid.prototype - * @returns {LayoutData} - */ - Grid.prototype._updateLayout = function() { - var layout = this._layout; - var settings = this._settings.layout; - var width; - var height; - var newLayout; - var i; - - // Let's increment layout id. - ++layout.id; - - // Let's update layout items - layout.items.length = 0; - for (i = 0; i < this._items.length; i++) { - if (this._items[i]._isActive) layout.items.push(this._items[i]); - } - - // Let's make sure we have the correct container dimensions. - this._refreshDimensions(); - - // Calculate container width and height (without borders). - width = this._width - this._borderLeft - this._borderRight; - height = this._height - this._borderTop - this._borderBottom; - - // Calculate new layout. - if (isFunction(settings)) { - newLayout = settings(layout.items, width, height); - } else { - newLayout = packer.getLayout(layout.items, width, height, layout.slots, settings); - } - - // Let's update the grid's layout. - layout.slots = newLayout.slots; - layout.setWidth = Boolean(newLayout.setWidth); - layout.setHeight = Boolean(newLayout.setHeight); - layout.width = newLayout.width; - layout.height = newLayout.height; - - return layout; - }; - /** * Emit a grid event. * * @private - * @memberof Grid.prototype * @param {String} event * @param {...*} [arg] */ - Grid.prototype._emit = function() { + Grid.prototype._emit = function () { if (this._isDestroyed) return; this._emitter.emit.apply(this._emitter, arguments); }; @@ -6580,41 +8480,40 @@ * Check if there are any events listeners for an event. * * @private - * @memberof Grid.prototype * @param {String} event * @returns {Boolean} */ - Grid.prototype._hasListeners = function(event) { - var listeners = this._emitter._events[event]; - return !!(listeners && listeners.length); + Grid.prototype._hasListeners = function (event) { + if (this._isDestroyed) return false; + return this._emitter.countListeners(event) > 0; }; /** * Update container's width, height and offsets. * * @private - * @memberof Grid.prototype */ - Grid.prototype._updateBoundingRect = function() { + Grid.prototype._updateBoundingRect = function () { var element = this._element; var rect = element.getBoundingClientRect(); this._width = rect.width; this._height = rect.height; this._left = rect.left; this._top = rect.top; + this._right = rect.right; + this._bottom = rect.bottom; }; /** * Update container's border sizes. * * @private - * @memberof Grid.prototype * @param {Boolean} left * @param {Boolean} right * @param {Boolean} top * @param {Boolean} bottom */ - Grid.prototype._updateBorders = function(left, right, top, bottom) { + Grid.prototype._updateBorders = function (left, right, top, bottom) { var element = this._element; if (left) this._borderLeft = getStyleAsFloat(element, 'border-left-width'); if (right) this._borderRight = getStyleAsFloat(element, 'border-right-width'); @@ -6626,35 +8525,151 @@ * Refresh all of container's internal dimensions and offsets. * * @private - * @memberof Grid.prototype */ - Grid.prototype._refreshDimensions = function() { + Grid.prototype._refreshDimensions = function () { this._updateBoundingRect(); this._updateBorders(1, 1, 1, 1); + this._boxSizing = getStyle(this._element, 'box-sizing'); }; + /** + * Calculate and apply item positions. + * + * @private + * @param {Object} layout + */ + Grid.prototype._onLayoutDataReceived = (function () { + var itemsToLayout = []; + return function (layout) { + if (this._isDestroyed || !this._nextLayoutData || this._nextLayoutData.id !== layout.id) return; + + var grid = this; + var instant = this._nextLayoutData.instant; + var onFinish = this._nextLayoutData.onFinish; + var numItems = layout.items.length; + var counter = numItems; + var item; + var left; + var top; + var i; + + // Reset next layout data. + this._nextLayoutData = null; + + if (!this._isLayoutFinished && this._hasListeners(EVENT_LAYOUT_ABORT)) { + this._emit(EVENT_LAYOUT_ABORT, this._layout.items.slice(0)); + } + + // Update the layout reference. + this._layout = layout; + + // Update the item positions and collect all items that need to be laid + // out. It is critical that we update the item position _before_ the + // layoutStart event as the new data might be needed in the callback. + itemsToLayout.length = 0; + for (i = 0; i < numItems; i++) { + item = layout.items[i]; + + // Make sure we have a matching item. + if (!item) { + --counter; + continue; + } + + // Get the item's new left and top values. + left = layout.slots[i * 2]; + top = layout.slots[i * 2 + 1]; + + // Let's skip the layout process if we can. Possibly avoids a lot of DOM + // operations which saves us some CPU cycles. + if (item._canSkipLayout(left, top)) { + --counter; + continue; + } + + // Update the item's position. + item._left = left; + item._top = top; + + // Only active non-dragged items need to be moved. + if (item.isActive() && !item.isDragging()) { + itemsToLayout.push(item); + } + } + + // Set layout styles to the grid element. + if (layout.styles) { + setStyles(this._element, layout.styles); + } + + // layoutStart event is intentionally emitted after the container element's + // dimensions are set, because otherwise there would be no hook for reacting + // to container dimension changes. + if (this._hasListeners(EVENT_LAYOUT_START)) { + this._emit(EVENT_LAYOUT_START, layout.items.slice(0), instant === true); + } + + function tryFinish() { + if (--counter > 0) return; + + var hasLayoutChanged = grid._layout.id !== layout.id; + var callback = isFunction(instant) ? instant : onFinish; + + if (!hasLayoutChanged) { + grid._isLayoutFinished = true; + } + + if (isFunction(callback)) { + callback(layout.items.slice(0), hasLayoutChanged); + } + + if (!hasLayoutChanged && grid._hasListeners(EVENT_LAYOUT_END)) { + grid._emit(EVENT_LAYOUT_END, layout.items.slice(0)); + } + } + + if (!itemsToLayout.length) { + tryFinish(); + return this; + } + + this._isLayoutFinished = false; + + for (i = 0; i < itemsToLayout.length; i++) { + if (this._layout.id !== layout.id) break; + itemsToLayout[i]._layout.start(instant === true, tryFinish); + } + + if (this._layout.id === layout.id) { + itemsToLayout.length = 0; + } + + return this; + }; + })(); + /** * Show or hide Grid instance's items. * * @private - * @memberof Grid.prototype - * @param {GridMultiItemQuery} items + * @param {Item[]} items * @param {Boolean} toVisible * @param {Object} [options] * @param {Boolean} [options.instant=false] - * @param {(ShowCallback|HideCallback)} [options.onFinish] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {Boolean} [options.syncWithLayout=true] + * @param {Function} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] */ - Grid.prototype._setItemsVisibility = function(items, toVisible, options) { + Grid.prototype._setItemsVisibility = function (items, toVisible, options) { var grid = this; - var targetItems = this.getItems(items); - var opts = options || 0; + var targetItems = items.slice(0); + var opts = options || {}; var isInstant = opts.instant === true; var callback = opts.onFinish; var layout = opts.layout ? opts.layout : opts.layout === undefined; var counter = targetItems.length; - var startEvent = toVisible ? eventShowStart : eventHideStart; - var endEvent = toVisible ? eventShowEnd : eventHideEnd; + var startEvent = toVisible ? EVENT_SHOW_START : EVENT_HIDE_START; + var endEvent = toVisible ? EVENT_SHOW_END : EVENT_HIDE_END; var method = toVisible ? 'show' : 'hide'; var needsLayout = false; var completedItems = []; @@ -6668,12 +8683,7 @@ return; } - // Emit showStart/hideStart event. - if (this._hasListeners(startEvent)) { - this._emit(startEvent, targetItems.slice(0)); - } - - // Show/hide items. + // Prepare the items. for (i = 0; i < targetItems.length; i++) { item = targetItems[i]; @@ -6685,9 +8695,7 @@ // If inactive item is shown we also need to do a little hack to make the // item not animate it's next positioning (layout). - if (toVisible && !item._isActive) { - item._layout._skipNextAnimation = true; - } + item._layout._skipNextAnimation = !!(toVisible && !item._isActive); // If a hidden item is being shown we need to refresh the item's // dimensions. @@ -6695,27 +8703,66 @@ hiddenItems.push(item); } - // Show/hide the item. - item._visibility[method](isInstant, function(interrupted, item) { - // If the current item's animation was not interrupted add it to the - // completedItems array. - if (!interrupted) completedItems.push(item); + // Add item to layout or remove it from layout. + if (toVisible) { + item._addToLayout(); + } else { + item._removeFromLayout(); + } + } + + // Force refresh the dimensions of all hidden items. + if (hiddenItems.length) { + this.refreshItems(hiddenItems, true); + hiddenItems.length = 0; + } + + // Show the items in sync with the next layout. + function triggerVisibilityChange() { + if (needsLayout && opts.syncWithLayout !== false) { + grid.off(EVENT_LAYOUT_START, triggerVisibilityChange); + } + + if (grid._hasListeners(startEvent)) { + grid._emit(startEvent, targetItems.slice(0)); + } - // If all items have finished their animations call the callback - // and emit showEnd/hideEnd event. - if (--counter < 1) { - if (isFunction(callback)) callback(completedItems.slice(0)); - if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); + for (i = 0; i < targetItems.length; i++) { + // Make sure the item is still in the original grid. There is a chance + // that the item starts migrating before tiggerVisibilityChange is called. + if (targetItems[i]._gridId !== grid._id) { + if (--counter < 1) { + if (isFunction(callback)) callback(completedItems.slice(0)); + if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); + } + continue; } - }); + + targetItems[i]._visibility[method](isInstant, function (interrupted, item) { + // If the current item's animation was not interrupted add it to the + // completedItems array. + if (!interrupted) completedItems.push(item); + + // If all items have finished their animations call the callback + // and emit showEnd/hideEnd event. + if (--counter < 1) { + if (isFunction(callback)) callback(completedItems.slice(0)); + if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); + } + }); + } } - // Refresh hidden items. - if (hiddenItems.length) this.refreshItems(hiddenItems); + // Trigger the visibility change, either async with layout or instantly. + if (needsLayout && opts.syncWithLayout !== false) { + this.on(EVENT_LAYOUT_START, triggerVisibilityChange); + } else { + triggerVisibilityChange(); + } - // Layout if needed. + // Trigger layout if needed. if (needsLayout && layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } }; @@ -6737,19 +8784,29 @@ */ function mergeSettings(defaultSettings, userSettings) { // Create a fresh copy of default settings. - var ret = mergeObjects({}, defaultSettings); + var settings = mergeObjects({}, defaultSettings); // Merge user settings to default settings. if (userSettings) { - ret = mergeObjects(ret, userSettings); + settings = mergeObjects(settings, userSettings); } // Handle visible/hidden styles manually so that the whole object is // overridden instead of the props. - ret.visibleStyles = (userSettings || 0).visibleStyles || (defaultSettings || 0).visibleStyles; - ret.hiddenStyles = (userSettings || 0).hiddenStyles || (defaultSettings || 0).hiddenStyles; - return ret; + if (userSettings && userSettings.visibleStyles) { + settings.visibleStyles = userSettings.visibleStyles; + } else if (defaultSettings && defaultSettings.visibleStyles) { + settings.visibleStyles = defaultSettings.visibleStyles; + } + + if (userSettings && userSettings.hiddenStyles) { + settings.hiddenStyles = userSettings.hiddenStyles; + } else if (defaultSettings && defaultSettings.hiddenStyles) { + settings.hiddenStyles = defaultSettings.hiddenStyles; + } + + return settings; } /** @@ -6802,6 +8859,124 @@ return target; } + /** + * Collect and return initial items for grid. + * + * @param {HTMLElement} gridElement + * @param {?(HTMLElement[]|NodeList|HtmlCollection|String)} elements + * @returns {(HTMLElement[]|NodeList|HtmlCollection)} + */ + function getInitialGridElements(gridElement, elements) { + // If we have a wildcard selector let's return all the children. + if (elements === '*') { + return gridElement.children; + } + + // If we have some more specific selector, let's filter the elements. + if (typeof elements === STRING_TYPE) { + var result = []; + var children = gridElement.children; + for (var i = 0; i < children.length; i++) { + if (elementMatches(children[i], elements)) { + result.push(children[i]); + } + } + return result; + } + + // If we have an array of elements or a node list. + if (Array.isArray(elements) || isNodeList(elements)) { + return elements; + } + + // Otherwise just return an empty array. + return []; + } + + /** + * Bind grid's resize handler to window. + * + * @param {Grid} grid + * @param {(Number|Boolean)} delay + */ + function bindLayoutOnResize(grid, delay) { + if (typeof delay !== NUMBER_TYPE) { + delay = delay === true ? 0 : -1; + } + + if (delay >= 0) { + grid._resizeHandler = debounce(function () { + grid.refreshItems().layout(); + }, delay); + + window.addEventListener('resize', grid._resizeHandler); + } + } + + /** + * Unbind grid's resize handler from window. + * + * @param {Grid} grid + */ + function unbindLayoutOnResize(grid) { + if (grid._resizeHandler) { + grid._resizeHandler(true); + window.removeEventListener('resize', grid._resizeHandler); + grid._resizeHandler = null; + } + } + + /** + * Normalize style declaration object, returns a normalized (new) styles object + * (prefixed properties and invalid properties removed). + * + * @param {Object} styles + * @returns {Object} + */ + function normalizeStyles(styles) { + var normalized = {}; + var docElemStyle = document.documentElement.style; + var prop, prefixedProp; + + // Normalize visible styles (prefix and remove invalid). + for (prop in styles) { + if (!styles[prop]) continue; + prefixedProp = getPrefixedPropName(docElemStyle, prop); + if (!prefixedProp) continue; + normalized[prefixedProp] = styles[prop]; + } + + return normalized; + } + + /** + * Create index map from items. + * + * @param {Item[]} items + * @returns {Object} + */ + function createIndexMap(items) { + var result = {}; + for (var i = 0; i < items.length; i++) { + result[items[i]._id] = i; + } + return result; + } + + /** + * Sort comparer function for items' index map. + * + * @param {Object} indexMap + * @param {Item} itemA + * @param {Item} itemB + * @returns {Number} + */ + function compareIndexMap(indexMap, itemA, itemB) { + var indexA = indexMap[itemA._id]; + var indexB = indexMap[itemB._id]; + return indexA - indexB; + } + return Grid; -})); +}))); diff --git a/dist/muuri.min.js b/dist/muuri.min.js index 4bd2fc7d..037a3990 100644 --- a/dist/muuri.min.js +++ b/dist/muuri.min.js @@ -1,6 +1,6 @@ /** -* Muuri v0.8.0 -* https://github.com/haltu/muuri +* Muuri v0.9.0 +* https://muuri.dev/ * Copyright (c) 2015-present, Haltu Oy * Released under the MIT license * https://github.com/haltu/muuri/blob/master/LICENSE.md @@ -10,8 +10,12 @@ * Copyright (c) 2016-present, Niklas Rämö * @license MIT * -* Muuri Ticker / Muuri Emitter / Muuri Queue +* Muuri Ticker / Muuri Emitter / Muuri Dragger * Copyright (c) 2018-present, Niklas Rämö * @license MIT +* +* Muuri AutoScroller +* Copyright (c) 2019-present, Niklas Rämö +* @license MIT */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Muuri=e()}(this,function(){"use strict";var t={},e="layoutEnd";function i(){this._events={},this._queue=[],this._counter=0,this._isDestroyed=!1}i.prototype.on=function(t,e){if(this._isDestroyed)return this;var i=this._events[t];return i||(i=this._events[t]=[]),i.push(e),this},i.prototype.off=function(t,e){if(this._isDestroyed)return this;var i=this._events[t];if(!i||!i.length)return this;if(!e)return i.length=0,this;for(var s=i.length;s--;)e===i[s]&&i.splice(s,1);return this},i.prototype.emit=function(t,e,i,s){if(this._isDestroyed)return this;var n=this._events[t];if(!n||!n.length)return this;var r,o=this._queue,h=o.length,a=arguments.length-1;for(r=0;r-1||(M._activeInstances.push(t),M._emitter.on(D,t._onMove),M._emitter.on(A,t._onCancel),M._emitter.on(b,t._onEnd),1===M._activeInstances.length&&M._bindListeners())},M._deactivateInstance=function(t){var e=M._activeInstances.indexOf(t);-1!==e&&(M._activeInstances.splice(e,1),M._emitter.off(D,t._onMove),M._emitter.off(A,t._onCancel),M._emitter.off(b,t._onEnd),M._activeInstances.length||M._unbindListeners())},M._bindListeners=function(){var t=M._events;window.addEventListener(t.move,M._onMove,R),window.addEventListener(t.end,M._onEnd,R),t.cancel&&window.addEventListener(t.cancel,M._onCancel,R)},M._unbindListeners=function(){var t=M._events;window.removeEventListener(t.move,M._onMove,R),window.removeEventListener(t.end,M._onEnd,R),t.cancel&&window.removeEventListener(t.cancel,M._onCancel,R)},M._getEventPointerId=function(t){return"number"==typeof t.pointerId?t.pointerId:t.changedTouches?t.changedTouches[0]?t.changedTouches[0].identifier:null:1},M._getTouchById=function(t,e){if("number"==typeof t.pointerId)return t.pointerId===e?t:null;if(t.changedTouches){for(var i=0;i-1&&(this._queue[n]=void 0),s?this._queue.unshift(t):this._queue.push(t),this._reads[t]=e,this._writes[t]=i,this._nextStep||(this._nextStep=y(this._step))},T.prototype.cancel=function(t){var e=this._queue.indexOf(t);e>-1&&(this._queue[e]=void 0,delete this._reads[t],delete this._writes[t])},T.prototype._step=function(){var t,e,i=this._queue,s=this._reads,n=this._writes,r=this._batch,o=this._batchReads,h=this._batchWrites,a=i.length;for(this._nextStep=null,e=0;en?n:e<0?Math.max(n+e+1,0):e}function K(t,e,i){if(!(t.length<2)){var s=J(t,e),n=J(t,i);s!==n&&t.splice(n,0,t.splice(s,1)[0])}}function Q(t,e,i){if(!(t.length<2)){var s,n=J(t,e),r=J(t,i);n!==r&&(s=t[n],t[n]=t[r],t[r]=s)}}var tt="cancel",et="finish",it="debounce",st=0;function nt(t,e){var i,s=++st+it;return e>0?function(n){void 0!==i&&(i=window.clearTimeout(i),k.cancel(s),n===et&&t()),n!==tt&&n!==et&&(i=window.setTimeout(function(){i=void 0,k.add(s,t,null,!0)},e))}:function(e){e!==tt&&t()}}function rt(t){var e=a(t,"transform");if(!e||"none"===e)return!1;var i=a(t,"display");return"inline"!==i&&"none"!==i}function ot(t,e){for(var i=window.document,s=(e?t:t.parentElement)||i;s&&s!==i&&"static"===a(s,"position")&&!rt(s);)s=s.parentElement||i;return s}function ht(t,e){return parseFloat(a(t,e))||0}var at={},_t={},lt={};function dt(t,e){var i,s=e||{};return s.left=0,s.top=0,t===document?s:(s.left=window.pageXOffset||0,s.top=window.pageYOffset||0,t.self===window.self?s:(i=t.getBoundingClientRect(),s.left+=i.left,s.top+=i.top,s.left+=ht(t,"border-left-width"),s.top+=ht(t,"border-top-width"),s))}function ct(t,e,i){return lt.left=0,lt.top=0,t===e?lt:i&&(t=ot(t,!0))===(e=ot(e,!0))?lt:(dt(t,at),dt(e,_t),lt.left=_t.left-at.left,lt.top=_t.top-at.top,lt)}var ut="overflow",ft="overflow-x",pt="overflow-y",mt="auto",gt="scroll";function yt(t){var e=a(t,ut);return e===mt||e===gt||((e=a(t,ft))===mt||e===gt||((e=a(t,pt))===mt||e===gt))}function vt(t,e,i){for(var s=i||[],n=e?t:t.parentNode;n&&n!==document;)n.getRootNode&&n instanceof DocumentFragment?n=n.getRootNode().host:(yt(n)&&s.push(n),n=n.parentNode);return s.push(window),s}var wt={},St="transform",Dt="none",bt=/^matrix3d/,At=/([^,]*,){4}/,Et=/([^,]*,){12}/,Ct=/[^,]*,/;function Lt(t){wt.x=0,wt.y=0;var e=a(t,St);if(!e||e===Dt)return wt;var i=bt.test(e),s=e.replace(i?Et:At,""),n=s.replace(Ct,"");return wt.x=parseFloat(s)||0,wt.y=parseFloat(n)||0,wt}function It(t,e){return"translateX("+t+"px) translateY("+e+"px)"}function Rt(t,e){t.classList?t.classList.remove(e):W(t,"."+e)&&(t.className=(" "+t.className+" ").replace(" "+e+" "," ").trim())}var xt,Pt,Mt,Tt,kt=0;function Xt(t){var e=t._element,i=t.getGrid(),s=i._settings;this._item=t,this._gridId=i._id,this._isDestroyed=!1,this._isMigrating=!1,this._startPredicate=c(s.dragStartPredicate)?s.dragStartPredicate:Xt.defaultStartPredicate,this._startPredicateState=kt,this._startPredicateResult=void 0,this._hBlockedIndex=null,this._hX1=0,this._hX2=0,this._hY1=0,this._hY2=0,this._reset(),this._preStartCheck=this._preStartCheck.bind(this),this._preEndCheck=this._preEndCheck.bind(this),this._onScroll=this._onScroll.bind(this),this._prepareMove=this._prepareMove.bind(this),this._applyMove=this._applyMove.bind(this),this._prepareScroll=this._prepareScroll.bind(this),this._applyScroll=this._applyScroll.bind(this),this._checkOverlap=this._checkOverlap.bind(this);var n=s.dragSortHeuristics.sortInterval;this._checkOverlapDebounce=nt(this._checkOverlap,n),this._dragger=new M(e,s.dragCssProps),this._dragger.on("start",this._preStartCheck),this._dragger.on("move",this._preStartCheck),this._dragger.on("cancel",this._preEndCheck),this._dragger.on("end",this._preEndCheck)}function Yt(t,e){return t.left+t.width<=e.left||e.left+e.width<=t.left||t.top+t.height<=e.top||e.top+e.height<=t.top?0:(Math.min(t.left+t.width,e.left+e.width)-Math.max(t.left,e.left))*(Math.min(t.top+t.height,e.top+e.height)-Math.max(t.top,e.top))/(Math.min(t.width,e.width)*Math.min(t.height,e.height))*100}function Gt(t){this._item=t,this._animate=new p,this._element=null,this._className="",this._didMigrate=!1,this._resetAfterLayout=!1,this._currentLeft=0,this._currentTop=0,this._nextLeft=0,this._nextTop=0,this._setupAnimation=this._setupAnimation.bind(this),this._startAnimation=this._startAnimation.bind(this),this._onLayoutStart=this._onLayoutStart.bind(this),this._onLayoutEnd=this._onLayoutEnd.bind(this),this._onReleaseEnd=this._onReleaseEnd.bind(this),this._onMigrate=this._onMigrate.bind(this)}function Ht(){this._queue=[],this._isDestroyed=!1}function Ot(t){this._item=t,this._isActive=!1,this._isDestroyed=!1,this._isInterrupted=!1,this._currentStyles={},this._targetStyles={},this._currentLeft=0,this._currentTop=0,this._offsetLeft=0,this._offsetTop=0,this._skipNextAnimation=!1,this._animateOptions={onFinish:this._finish.bind(this)},this._queue=new Ht,this._setupAnimation=this._setupAnimation.bind(this),this._startAnimation=this._startAnimation.bind(this)}Xt.defaultStartPredicate=function(t,e,i){var s=t._drag,n=s._startPredicateData||s._setupStartPredicate(i);if(!e.isFinal)return!(!n.handleElement&&(n.handleElement=s._getStartPredicateHandle(e),!n.handleElement))&&(n.delay&&(n.event=e,n.delayTimer||(n.delayTimer=window.setTimeout(function(){n.delay=0,s._resolveStartPredicate(n.event)&&(s._forceResolveStartPredicate(n.event),s._resetStartPredicate())},n.delay))),s._resolveStartPredicate(e));s._finishStartPredicate(e)},Xt.defaultSortPredicate=(xt={},Pt={},Mt={},Tt=[],function(t,e){var i=t._drag,s=i._getGrid(),n=e&&"number"==typeof e.threshold?e.threshold:50,r=e&&"swap"===e.action?"swap":"move";xt.width=t._width,xt.height=t._height,xt.left=i._elementClientX,xt.top=i._elementClientY;var o=function(t,e,i){var s,n,r,o,h=null,a=e._settings.dragSort,_=-1;if(!0===a?(Tt[0]=e,n=Tt):n=a.call(e,t),!Array.isArray(n))return h;for(o=0;oi&&s>_&&(_=s,h=r));return Tt.length=0,h}(t,s,n);if(!o)return!1;var h,a,_,l,d,c=0,u=0,f=-1;for(o===s?(xt.left=i._gridX+t._marginLeft,xt.top=i._gridY+t._marginTop):(o._updateBorders(1,0,1,0),c=o._left+o._borderLeft,u=o._top+o._borderTop),d=0;df&&(h=d,f=l));return f=n&&(Mt.grid=o,Mt.index=h,Mt.action=r,Mt)}),Xt.prototype.stop=function(){var t=this._item,e=t._element,i=this._getGrid();return this._isActive?this._isMigrating?(this._finishMigration(),this):(N(t._id),q(t._id),this._unbindScrollListeners(),this._checkOverlapDebounce("cancel"),e.parentNode!==i._element&&(i._element.appendChild(e),e.style[n]=It(this._gridX,this._gridY)),Rt(e,i._settings.itemDraggingClass),this._reset(),this):this},Xt.prototype.destroy=function(){return this._isDestroyed?this:(this.stop(),this._dragger.destroy(),this._isDestroyed=!0,this)},Xt.prototype._getGrid=function(){return t[this._gridId]||null},Xt.prototype._reset=function(){this._isActive=!1,this._container=null,this._containingBlock=null,this._dragEvent=null,this._scrollEvent=null,this._scrollers=[],this._left=0,this._top=0,this._gridX=0,this._gridY=0,this._elementClientX=0,this._elementClientY=0,this._containerDiffX=0,this._containerDiffY=0},Xt.prototype._bindScrollListeners=function(){var t,e,i=this._getGrid()._element,s=this._container,n=this._scrollers;if(n.length=0,vt(this._item._element,!1,n),s!==i)for(vt(i,!0,t=[]),e=0;e=s&&t.pageX=n&&t.pageY3&&e.minBounceBackAngle>0;if(h||(this._hBlockedIndex=null),Math.abs(r)>i||Math.abs(o)>i){if(h){var a=Math.atan2(r,o),_=Math.atan2(this._hX2-this._hX1,this._hY2-this._hY1),l=Math.atan2(Math.sin(a-_),Math.cos(a-_));Math.abs(l)>e.minBounceBackAngle&&(this._hBlockedIndex=null)}return this._hX1=this._hX2,this._hY1=this._hY2,this._hX2=s,this._hY2=n,!0}return!1},Xt.prototype._resetStartPredicate=function(){var t=this._startPredicateData;t&&(t.delayTimer&&(t.delayTimer=window.clearTimeout(t.delayTimer)),this._startPredicateData=null)},Xt.prototype._checkOverlap=function(){if(this._isActive){var t,e,i,s,n,r,o,h=this._item,a=this._getGrid()._settings;(t=c(a.dragSortPredicate)?a.dragSortPredicate(h,this._dragEvent):Xt.defaultSortPredicate(h,a.dragSortPredicate))&&"number"==typeof t.index&&(o=(e=h.getGrid())!==(s=t.grid||e),i=e._items.indexOf(h),n=J(s._items,t.index,o),r="swap"===t.action?"swap":"move",(o||n!==this._hBlockedIndex)&&(o?(this._hBlockedIndex=null,e._hasListeners("beforeSend")&&e._emit("beforeSend",{item:h,fromGrid:e,fromIndex:i,toGrid:s,toIndex:n}),s._hasListeners("beforeReceive")&&s._emit("beforeReceive",{item:h,fromGrid:e,fromIndex:i,toGrid:s,toIndex:n}),h._gridId=s._id,this._isMigrating=h._gridId!==this._gridId,e._items.splice(i,1),$(s._items,h,n),h._sortData=null,e._hasListeners("send")&&e._emit("send",{item:h,fromGrid:e,fromIndex:i,toGrid:s,toIndex:n}),s._hasListeners("receive")&&s._emit("receive",{item:h,fromGrid:e,fromIndex:i,toGrid:s,toIndex:n}),e.layout(),s.layout()):i!==n&&(this._hBlockedIndex=i,("swap"===r?Q:K)(e._items,i,n),e._hasListeners("move")&&e._emit("move",{item:h,fromIndex:i,toIndex:n,action:r}),e.layout())))}},Xt.prototype._finishMigration=function(){var t,e,i=this._item,s=i._release,r=i._element,o=i._isActive,h=i.getGrid(),a=h._element,_=h._settings,l=_.dragContainer||a,d=this._getGrid()._settings,c=r.parentNode;this._isMigrating=!1,this.destroy(),Rt(r,d.itemClass),Rt(r,d.itemVisibleClass),Rt(r,d.itemHiddenClass),j(r,_.itemClass),j(r,o?_.itemVisibleClass:_.itemHiddenClass),l!==c&&(l.appendChild(r),e=ct(c,l,!0),(t=Lt(r)).x-=e.left,t.y-=e.top),i._refreshDimensions(),i._refreshSortData(),e=ct(l,a,!0),s._containerDiffX=e.left,s._containerDiffY=e.top,i._drag=_.dragEnabled?new Xt(i):null,l!==c&&(r.style[n]=It(t.x,t.y)),i._child.removeAttribute("style"),f(i._child,o?_.visibleStyles:_.hiddenStyles),s.start()},Xt.prototype._preStartCheck=function(t){this._startPredicateState===kt&&(this._startPredicateState=1),1===this._startPredicateState?(this._startPredicateResult=this._startPredicate(this._item,t),!0===this._startPredicateResult?(this._startPredicateState=2,this._onStart(t)):!1===this._startPredicateResult&&(this._startPredicateState=3)):2===this._startPredicateState&&this._isActive&&this._onMove(t)},Xt.prototype._preEndCheck=function(t){var e=2===this._startPredicateState;this._startPredicate(this._item,t),this._startPredicateState=kt,e&&this._isActive&&this._onEnd(t)},Xt.prototype._onStart=function(t){var e=this._item;if(e._isActive){var i,s=e._element,r=this._getGrid(),o=r._settings,h=e._release,a=e._migrate,_=r._element,l=o.dragContainer||_,d=ot(l,!0),c=Lt(s),u=c.x,f=c.y,p=s.getBoundingClientRect(),m=l!==_;this._resetHeuristics(t),m&&(i=ct(d,_)),e.isPositioning()&&e._layout.stop(!0,{transform:It(u,f)}),a._isActive&&(u-=a._containerDiffX,f-=a._containerDiffY,a.stop(!0,{transform:It(u,f)})),e.isReleasing()&&h._reset(),this._isActive=!0,this._dragEvent=t,this._container=l,this._containingBlock=d,this._elementClientX=p.left,this._elementClientY=p.top,this._left=this._gridX=u,this._top=this._gridY=f,o.dragPlaceholder.enabled&&e._dragPlaceholder.create(),r._emit("dragInit",e,t),m&&(this._containerDiffX=i.left,this._containerDiffY=i.top,s.parentNode===l?(this._gridX=u-this._containerDiffX,this._gridY=f-this._containerDiffY):(this._left=u+this._containerDiffX,this._top=f+this._containerDiffY,l.appendChild(s),s.style[n]=It(this._left,this._top))),j(s,o.itemDraggingClass),this._bindScrollListeners(),r._emit("dragStart",e,t)}},Xt.prototype._onMove=function(t){var e=this._item;if(e._isActive){var i,s,n,r=this._getGrid()._settings.dragAxis;if("y"!==r){var o=t.clientX-this._dragEvent.clientX;this._left+=o,this._gridX+=o,this._elementClientX+=o}if("x"!==r){var h=t.clientY-this._dragEvent.clientY;this._top+=h,this._gridY+=h,this._elementClientY+=h}this._dragEvent=t,i=e._id,s=this._prepareMove,n=this._applyMove,k.add(i+G,s,n,!0)}else this.stop()},Xt.prototype._prepareMove=function(){this._item._isActive&&this._getGrid()._settings.dragSort&&this._checkHeuristics(this._dragEvent)&&this._checkOverlapDebounce()},Xt.prototype._applyMove=function(){var t=this._item;t._isActive&&(t._element.style[n]=It(this._left,this._top),this._getGrid()._emit("dragMove",t,this._dragEvent))},Xt.prototype._onScroll=function(t){var e,i,s,n=this._item;n._isActive?(this._scrollEvent=t,e=n._id,i=this._prepareScroll,s=this._applyScroll,k.add(e+H,i,s,!0)):this.stop()},Xt.prototype._prepareScroll=function(){var t=this._item;if(t._isActive){var e,i=t._element,s=this._getGrid(),n=s._settings,r=n.dragAxis,o=s._element,h=i.getBoundingClientRect(),a=this._elementClientX-h.left,_=this._elementClientY-h.top;this._container!==o&&(e=ct(this._containingBlock,o),this._containerDiffX=e.left,this._containerDiffY=e.top),"y"!==r&&(this._left+=a,this._gridX=this._left-this._containerDiffX),"x"!==r&&(this._top+=_,this._gridY=this._top-this._containerDiffY),n.dragSort&&this._checkOverlapDebounce()}},Xt.prototype._applyScroll=function(){var t=this._item;t._isActive&&(t._element.style[n]=It(this._left,this._top),this._getGrid()._emit("dragScroll",t,this._scrollEvent))},Xt.prototype._onEnd=function(t){var e=this._item,i=e._element,s=this._getGrid(),n=s._settings,r=e._release;e._isActive?(N(e._id),q(e._id),n.dragSort&&this._checkOverlapDebounce("finish"),this._unbindScrollListeners(),r._containerDiffX=this._containerDiffX,r._containerDiffY=this._containerDiffY,this._reset(),Rt(i,n.itemDraggingClass),s._emit("dragEnd",e,t),this._isMigrating?this._finishMigration():r.start()):this.stop()},Gt.prototype._onLayoutStart=function(){var t=this._item,e=t.getGrid(),i=e._items.indexOf(t),s=e._layout.slots[2*i],n=e._layout.slots[2*i+1];if(this._didMigrate||t._left!==s||t._top!==n){var r,o,h;if(s+=t._marginLeft,n+=t._marginTop,!(e._settings.dragPlaceholder.duration>0)||this._didMigrate){F(t._id);var a={transform:It(s,n)};return this._animate.isAnimating()?this._animate.stop(a):f(this._element,a),void(this._didMigrate&&(e.getElement().appendChild(this._element),this._didMigrate=!1))}this._nextLeft=s,this._nextTop=n,r=t._id,o=this._setupAnimation,h=this._startAnimation,k.add(r+O,o,h)}},Gt.prototype._setupAnimation=function(){if(this.isActive()){var t=Lt(this._element);this._currentLeft=t.x,this._currentTop=t.y}},Gt.prototype._startAnimation=function(){if(this.isActive()){var t=this._animate,e=this._currentLeft,i=this._currentTop,s=this._nextLeft,n=this._nextTop,r={transform:It(s,n)};if(e!==s||i!==n){var o=this._item.getGrid()._settings.dragPlaceholder,h={transform:It(e,i)};t.start(h,r,{duration:o.duration,easing:o.easing,onFinish:this._onLayoutEnd})}else t.isAnimating()&&t.stop(r)}},Gt.prototype._onLayoutEnd=function(){this._resetAfterLayout&&this.reset()},Gt.prototype._onReleaseEnd=function(t){if(t._id===this._item._id){if(!this._animate.isAnimating())return void this.reset();this._resetAfterLayout=!0}},Gt.prototype._onMigrate=function(t){if(t.item===this._item){var e=this._item.getGrid(),i=t.toGrid;e.off("dragReleaseEnd",this._onReleaseEnd),e.off("layoutStart",this._onLayoutStart),e.off("beforeSend",this._onMigrate),i.on("dragReleaseEnd",this._onReleaseEnd),i.on("layoutStart",this._onLayoutStart),i.on("beforeSend",this._onMigrate),this._didMigrate=!0}},Gt.prototype.create=function(){if(this.isActive())this._resetAfterLayout=!1;else{var t,e=this._item,i=e.getGrid(),s=i._settings,n=this._animate;t=c(s.dragPlaceholder.createElement)?s.dragPlaceholder.createElement(e):window.document.createElement("div"),this._element=t,n._element=t,this._className=s.itemPlaceholderClass||"",this._className&&j(t,this._className);var r=e._left+e._marginLeft,o=e._top+e._marginTop;f(t,{display:"block",position:"absolute",left:"0",top:"0",width:e._width+"px",height:e._height+"px",transform:It(r,o)}),i.on("layoutStart",this._onLayoutStart),i.on("dragReleaseEnd",this._onReleaseEnd),i.on("beforeSend",this._onMigrate),c(s.dragPlaceholder.onCreate)&&s.dragPlaceholder.onCreate(e,t),i.getElement().appendChild(t)}},Gt.prototype.reset=function(){if(this.isActive()){var t=this._element,e=this._item,i=e.getGrid(),s=i._settings,n=this._animate;this._resetAfterLayout=!1,F(e._id),n.stop(),n._element=null,i.off("dragReleaseEnd",this._onReleaseEnd),i.off("layoutStart",this._onLayoutStart),i.off("beforeSend",this._onMigrate),this._className&&(Rt(t,this._className),this._className=""),t.parentNode.removeChild(t),this._element=null,c(s.dragPlaceholder.onRemove)&&s.dragPlaceholder.onRemove(e,t)}},Gt.prototype.updateDimensions=function(t,e){this.isActive()&&f(this._element,{width:t+"px",height:e+"px"})},Gt.prototype.isActive=function(){return!!this._element},Gt.prototype.destroy=function(){this.reset(),this._animate.destroy(),this._item=this._animate=null},Ht.prototype.add=function(t){return this._isDestroyed?this:(this._queue.push(t),this)},Ht.prototype.flush=function(t,e){if(this._isDestroyed)return this;var i,s=this._queue,n=s.length;if(!n)return this;var r=1===n,o=r?s[0]:s.slice(0);if(s.length=0,r)return o(t,e),this;for(i=0;i0;return l&&this._queue.flush(!0,o),d&&(a._isPositioningStarted=!0),c(e)&&this._queue.add(e),m?(this._isActive=!0,this._animateOptions.easing=p,this._animateOptions.duration=u,this._isInterrupted=l,s=o._id,n=this._setupAnimation,r=this._startAnimation,k.add(s+X,n,r),this):(this._updateOffsets(),this._updateTargetStyles(),i=o._animate.isAnimating(),this.stop(!1,this._targetStyles),!i&&f(h,this._targetStyles),this._skipNextAnimation=!1,this._finish())}},Ot.prototype.stop=function(t,e){if(this._isDestroyed||!this._isActive)return this;var i,s=this._item;return i=s._id,k.cancel(i+X),s._animate.stop(e),Rt(s._element,s.getGrid()._settings.itemPositioningClass),this._isActive=!1,t&&this._queue.flush(!0,s),this},Ot.prototype.destroy=function(){return this._isDestroyed?this:(this.stop(!0,{}),this._queue.destroy(),this._item=this._currentStyles=this._targetStyles=this._animateOptions=null,this._isDestroyed=!0,this)},Ot.prototype._updateOffsets=function(){if(!this._isDestroyed){var t=this._item,e=t._migrate,i=t._release;this._offsetLeft=i._isActive?i._containerDiffX:e._isActive?e._containerDiffX:0,this._offsetTop=i._isActive?i._containerDiffY:e._isActive?e._containerDiffY:0}},Ot.prototype._updateTargetStyles=function(){this._isDestroyed||(this._targetStyles.transform=It(this._item._left+this._offsetLeft,this._item._top+this._offsetTop))},Ot.prototype._finish=function(){if(!this._isDestroyed){var t=this._item,e=t._migrate,i=t._release;this._isActive&&(this._isActive=!1,Rt(t._element,t.getGrid()._settings.itemPositioningClass)),i._isActive&&i.stop(),e._isActive&&e.stop(),this._queue.flush(!1,t)}},Ot.prototype._setupAnimation=function(){var t=Lt(this._item._element);this._currentLeft=t.x,this._currentTop=t.y},Ot.prototype._startAnimation=function(){var t=this._item,e=t.getGrid()._settings;if(this._updateOffsets(),this._updateTargetStyles(),t._left===this._currentLeft-this._offsetLeft&&t._top===this._currentTop-this._offsetTop)return this._isInterrupted&&this.stop(!1,this._targetStyles),this._isActive=!1,void this._finish();this._isInterrupted||j(t._element,e.itemPositioningClass),this._currentStyles.transform=It(this._currentLeft,this._currentTop),t._animate.start(this._currentStyles,this._targetStyles,this._animateOptions)};var Bt={};function Nt(t){this._item=t,this._isActive=!1,this._isDestroyed=!1,this._container=!1,this._containerDiffX=0,this._containerDiffY=0}Nt.prototype.start=function(t,e,i){if(this._isDestroyed)return this;var s,r,o,h,a,_,l,d,c=this._item,u=c._element,p=c.isVisible(),m=c.getGrid(),g=m._settings,y=t._settings,v=t._element,w=t._items,S=m._items.indexOf(c),D=i||window.document.body;if("number"==typeof e)s=J(w,e,!0);else{if(!(r=t._getItem(e)))return this;s=w.indexOf(r)}return(c.isPositioning()||this._isActive||c.isReleasing())&&(l=(_=Lt(u)).x,d=_.y),c.isPositioning()&&c._layout.stop(!0,{transform:It(l,d)}),this._isActive&&(l-=this._containerDiffX,d-=this._containerDiffY,this.stop(!0,{transform:It(l,d)})),c.isReleasing()&&(l-=c._release._containerDiffX,d-=c._release._containerDiffY,c._release.stop(!0,{transform:It(l,d)})),c._visibility._stopAnimation(),c._drag&&c._drag.destroy(),c._visibility._queue.flush(!0,c),m._hasListeners("beforeSend")&&m._emit("beforeSend",{item:c,fromGrid:m,fromIndex:S,toGrid:t,toIndex:s}),t._hasListeners("beforeReceive")&&t._emit("beforeReceive",{item:c,fromGrid:m,fromIndex:S,toGrid:t,toIndex:s}),Rt(u,g.itemClass),Rt(u,g.itemVisibleClass),Rt(u,g.itemHiddenClass),j(u,y.itemClass),j(u,p?y.itemVisibleClass:y.itemHiddenClass),m._items.splice(S,1),$(w,c,s),c._gridId=t._id,D!==(o=u.parentNode)&&(D.appendChild(u),h=ct(D,o,!0),_||(l=(_=Lt(u)).x,d=_.y),u.style[n]=It(l+h.left,d+h.top)),c._child.removeAttribute("style"),f(c._child,p?y.visibleStyles:y.hiddenStyles),u.style.display=p?"block":"hidden",a=ct(D,v,!0),c._refreshDimensions(),c._refreshSortData(),c._drag=y.dragEnabled?new Xt(c):null,this._isActive=!0,this._container=D,this._containerDiffX=a.left,this._containerDiffY=a.top,m._hasListeners("send")&&m._emit("send",{item:c,fromGrid:m,fromIndex:S,toGrid:t,toIndex:s}),t._hasListeners("receive")&&t._emit("receive",{item:c,fromGrid:m,fromIndex:S,toGrid:t,toIndex:s}),this},Nt.prototype.stop=function(t,e){if(this._isDestroyed||!this._isActive)return this;var i,s=this._item,n=s._element,r=s.getGrid()._element;return this._container!==r&&(e||(t?(i=Lt(n),Bt.transform=It(i.x-this._containerDiffX,i.y-this._containerDiffY)):Bt.transform=It(s._left,s._top),e=Bt),r.appendChild(n),f(n,e)),this._isActive=!1,this._container=null,this._containerDiffX=0,this._containerDiffY=0,this},Nt.prototype.destroy=function(){return this._isDestroyed?this:(this.stop(!0),this._item=null,this._isDestroyed=!0,this)};var qt={};function Ft(t){this._item=t,this._isActive=!1,this._isDestroyed=!1,this._isPositioningStarted=!1,this._containerDiffX=0,this._containerDiffY=0}function zt(t){var e=t._isActive,i=t._element,s=t.getGrid()._settings;this._item=t,this._isDestroyed=!1,this._isHidden=!e,this._isHiding=!1,this._isShowing=!1,this._queue=new Ht,this._finishShow=this._finishShow.bind(this),this._finishHide=this._finishHide.bind(this),i.style.display=e?"block":"none",j(i,e?s.itemVisibleClass:s.itemHiddenClass),f(t._child,e?s.visibleStyles:s.hiddenStyles)}Ft.prototype.start=function(){if(this._isDestroyed||this._isActive)return this;var t=this._item,e=t.getGrid();return this._isActive=!0,j(t._element,e._settings.itemReleasingClass),e._emit("dragReleaseStart",t),t._layout.start(!1),this},Ft.prototype.stop=function(t,e){if(this._isDestroyed||!this._isActive)return this;var i,s=this._item,n=s._element,r=s.getGrid(),o=r._element;return this._reset(),n.parentNode!==o&&(e||(t?(i=Lt(n),qt.transform=It(i.x-this._containerDiffX,i.y-this._containerDiffY)):qt.transform=It(s._left,s._top),e=qt),o.appendChild(n),f(n,e)),t||r._emit("dragReleaseEnd",s),this},Ft.prototype.destroy=function(){return this._isDestroyed?this:(this.stop(!0),this._item=null,this._isDestroyed=!0,this)},Ft.prototype._reset=function(){if(!this._isDestroyed){var t=this._item;this._isActive=!1,this._isPositioningStarted=!1,this._containerDiffX=0,this._containerDiffY=0,Rt(t._element,t.getGrid()._settings.itemReleasingClass)}},zt.prototype.show=function(t,e){if(this._isDestroyed)return this;var i=this._item,s=i._element,n=this._queue,r=c(e)?e:null,o=i.getGrid()._settings;return this._isShowing||this._isHidden?this._isShowing&&!t?(r&&n.add(r),this):(this._isShowing||(n.flush(!0,i),Rt(s,o.itemHiddenClass),j(s,o.itemVisibleClass),this._isHiding||(s.style.display="block")),r&&n.add(r),i._isActive=this._isShowing=!0,this._isHiding=this._isHidden=!1,this._startAnimation(!0,t,this._finishShow),this):(r&&r(!1,i),this)},zt.prototype.hide=function(t,e){if(this._isDestroyed)return this;var i=this._item,s=i._element,n=this._queue,r=c(e)?e:null,o=i.getGrid()._settings;return!this._isHiding&&this._isHidden?(r&&r(!1,i),this):this._isHiding&&!t?(r&&n.add(r),this):(this._isHiding||(n.flush(!0,i),j(s,o.itemHiddenClass),Rt(s,o.itemVisibleClass)),r&&n.add(r),this._isHidden=this._isHiding=!0,i._isActive=this._isShowing=!1,this._startAnimation(!1,t,this._finishHide),this)},zt.prototype.destroy=function(){if(this._isDestroyed)return this;var t=this._item,e=t._element,i=t.getGrid(),s=this._queue,n=i._settings;return this._stopAnimation({}),s.flush(!0,t).destroy(),Rt(e,n.itemVisibleClass),Rt(e,n.itemHiddenClass),this._item=null,this._isHiding=this._isShowing=!1,this._isDestroyed=this._isHidden=!0,this},zt.prototype._startAnimation=function(t,e,i){if(!this._isDestroyed){var s,n=this._item,r=n.getGrid()._settings,o=t?r.visibleStyles:r.hiddenStyles,h=parseInt(t?r.showDuration:r.hideDuration)||0,_=(t?r.showEasing:r.hideEasing)||"ease",d=e||h<=0;if(o){if(B(n._id),d)return n._animateChild.isAnimating()?n._animateChild.stop(o):f(n._child,o),void(i&&i());var c,u,p;c=n._id,u=function(){s=function(t,e){var i={};for(var s in e)i[s]=a(t,l(s));return i}(n._child,o)},p=function(){n._animateChild.start(s,o,{duration:h,easing:_,onFinish:i})},k.add(c+Y,u,p)}else i&&i()}},zt.prototype._stopAnimation=function(t){if(!this._isDestroyed){var e=this._item;B(e._id),e._animateChild.stop(t)}},zt.prototype._finishShow=function(){this._isHidden||(this._isShowing=!1,this._queue.flush(!1,this._item))};var Vt={};zt.prototype._finishHide=function(){if(this._isHidden){var t=this._item;this._isHiding=!1,Vt.transform=It(0,0),t._layout.stop(!0,Vt),t._element.style.display="none",this._queue.flush(!1,t)}};var Wt,jt,Ut,Zt,$t=0;function Jt(){return++$t}function Kt(t,e,i){var s=t._settings;this._id=Jt(),this._gridId=t._id,this._isDestroyed=!1,this._left=0,this._top=0,this._element=e,this._child=e.children[0],e.parentNode!==t._element&&t._element.appendChild(e),j(e,s.itemClass),"boolean"!=typeof i&&(i="none"!==a(e,"display")),this._isActive=i,e.style.left="0",e.style.top="0",e.style[n]=It(0,0),this._animate=new p(e),this._animateChild=new p(this._child),this._visibility=new zt(this),this._layout=new Ot(this),this._migrate=new Nt(this),this._release=new Ft(this),this._dragPlaceholder=new Gt(this),this._drag=s.dragEnabled?new Xt(this):null,this._refreshDimensions(),this._refreshSortData()}function Qt(){this._slots=[],this._slotSizes=[],this._freeSlots=[],this._newSlots=[],this._rectItem={},this._rectStore=[],this._rectId=0,this._layout={slots:null,setWidth:!1,setHeight:!1,width:!1,height:!1},this._sortRectsLeftTop=this._sortRectsLeftTop.bind(this),this._sortRectsTopLeft=this._sortRectsTopLeft.bind(this)}Kt.prototype.getGrid=function(){return t[this._gridId]},Kt.prototype.getElement=function(){return this._element},Kt.prototype.getWidth=function(){return this._width},Kt.prototype.getHeight=function(){return this._height},Kt.prototype.getMargin=function(){return{left:this._marginLeft,right:this._marginRight,top:this._marginTop,bottom:this._marginBottom}},Kt.prototype.getPosition=function(){return{left:this._left,top:this._top}},Kt.prototype.isActive=function(){return this._isActive},Kt.prototype.isVisible=function(){return!!this._visibility&&!this._visibility._isHidden},Kt.prototype.isShowing=function(){return!(!this._visibility||!this._visibility._isShowing)},Kt.prototype.isHiding=function(){return!(!this._visibility||!this._visibility._isHiding)},Kt.prototype.isPositioning=function(){return!(!this._layout||!this._layout._isActive)},Kt.prototype.isDragging=function(){return!(!this._drag||!this._drag._isActive)},Kt.prototype.isReleasing=function(){return!(!this._release||!this._release._isActive)},Kt.prototype.isDestroyed=function(){return this._isDestroyed},Kt.prototype._refreshDimensions=function(){if(!this._isDestroyed&&!this._visibility._isHidden){var t=this._element,e=this._dragPlaceholder,i=t.getBoundingClientRect();this._width=i.width,this._height=i.height,this._marginLeft=Math.max(0,ht(t,"margin-left")),this._marginRight=Math.max(0,ht(t,"margin-right")),this._marginTop=Math.max(0,ht(t,"margin-top")),this._marginBottom=Math.max(0,ht(t,"margin-bottom")),e&&e.updateDimensions(this._width,this._height)}},Kt.prototype._refreshSortData=function(){if(!this._isDestroyed){var t,e=this._sortData={},i=this.getGrid()._settings.sortData;for(t in i)e[t]=i[t](this,this._element)}},Kt.prototype._destroy=function(t){if(!this._isDestroyed){var e=this._element,i=this.getGrid(),s=i._settings,n=i._items.indexOf(this);this._release.destroy(),this._migrate.destroy(),this._layout.destroy(),this._visibility.destroy(),this._animate.destroy(),this._animateChild.destroy(),this._dragPlaceholder.destroy(),this._drag&&this._drag.destroy(),e.removeAttribute("style"),this._child.removeAttribute("style"),Rt(e,s.itemClass),n>-1&&i._items.splice(n,1),t&&e.parentNode.removeChild(e),this._isActive=!1,this._isDestroyed=!0}},Qt.prototype.getLayout=function(t,e,i,s,n){var r,o=this._layout,h=!(!n||!n.fillGaps),a=!(!n||!n.horizontal),_=!(!n||!n.alignRight),l=!(!n||!n.alignBottom),d=!(!n||!n.rounding),c=this._slotSizes;if(o.slots=s||this._slots,o.width=a?0:d?Math.round(e):e,o.height=a?d?Math.round(i):i:0,o.setWidth=a,o.setHeight=!a,o.slots.length=0,c.length=0,!t.length)return o;for(r=0;rd.height&&(Wt.left>0&&u.push(this._addRect(0,d.height,Wt.left,1/0)),Wt.left+Wt.widthd.width&&(Wt.top>0&&u.push(this._addRect(d.width,0,1/0,Wt.top)),Wt.top+Wt.height.49&&r.height>.49&&(!e&&r.tope.left+e.width&&jt.push(this._addRect(e.left+e.width,t.top,t.left+t.width-(e.left+e.width),t.height)),t.tope.top+e.height&&jt.push(this._addRect(t.left,e.top+e.height,t.width,t.top+t.height-(e.top+e.height))),jt):(jt.push(this._addRect(t.left,t.top,t.width,t.height)),jt)}),Qt.prototype._doRectsOverlap=function(t,e){return!(t.left+t.width<=e.left||e.left+e.width<=t.left||t.top+t.height<=e.top||e.top+e.height<=t.top)},Qt.prototype._isRectWithinRect=function(t,e){return t.left>=e.left&&t.top>=e.top&&t.left+t.width<=e.left+e.width&&t.top+t.height<=e.top+e.height},Qt.prototype._purgeRects=(Ut={},Zt={},function(t){for(var e,i=t.length;i--;)if(e=t.length,t[i])for(this._getRect(t[i],Ut);e--;)if(t[e]&&i!==e&&this._isRectWithinRect(Ut,this._getRect(t[e],Zt))){t[i]=0;break}return t}),Qt.prototype._sortRectsTopLeft=function(){var t={},e={};return function(i,s){return this._getRect(i,t),this._getRect(s,e),t.tope.top?1:t.lefte.left?1:0}}(),Qt.prototype._sortRectsLeftTop=function(){var t={},e={};return function(i,s){return this._getRect(i,t),this._getRect(s,e),t.lefte.left?1:t.tope.top?1:0}}();var te="[object HTMLCollection]",ee="[object NodeList]";function ie(t){var e=Object.prototype.toString.call(t);return e===te||e===ee}var se="object",ne="[object Object]",re=Object.prototype.toString;function oe(t){return typeof t===se&&re.call(t)===ne}function he(t){return ie(t)?Array.prototype.slice.call(t):Array.prototype.concat(t)}var ae=new Qt,_e=function(){},le="number",de="string";function ce(e,s){var n,r,o,h=this;if(!((e=this._element=typeof e===de?window.document.querySelector(e):e).getRootNode?e.getRootNode({composed:!0})===document:window.document.body.contains(e))||e===window.document.documentElement)throw new Error("Container element must be an existing DOM element");c((n=this._settings=function(t,e){var i=ue({},t);e&&(i=ue(i,e));return i.visibleStyles=(e||0).visibleStyles||(t||0).visibleStyles,i.hiddenStyles=(e||0).hiddenStyles||(t||0).hiddenStyles,i}(ce.defaultOptions,s)).dragSort)||(n.dragSort=!!n.dragSort),this._id=Jt(),t[this._id]=h,this._isDestroyed=!1,this._layout={id:0,items:[],slots:[],setWidth:!1,setHeight:!1,width:0,height:0},this._emitter=new i,j(e,n.containerClass),this._items=[],typeof(r=n.items)===de?he(e.children).forEach(function(t){("*"===r||W(t,r))&&h._items.push(new Kt(h,t))}):(Array.isArray(r)||ie(r))&&(this._items=he(r).map(function(t){return new Kt(h,t)})),typeof(o=n.layoutOnResize)!==le&&(o=!0===o?0:-1),o>=0&&window.addEventListener("resize",h._resizeHandler=nt(function(){h.refreshItems().layout()},o)),n.layoutOnInit&&this.layout(!0)}function ue(t,e){var i,s,n,r=Object.keys(e),o=r.length;for(n=0;n0)){var s=o._layout.id!==l,n=c(t)?t:i;c(n)&&n(s,_.items.slice(0)),!s&&o._hasListeners(e)&&o._emit(e,_.items.slice(0))}}if((_.setHeight&&typeof _.height===le||_.setWidth&&typeof _.width===le)&&(s="border-box"===a(h,"box-sizing")),_.setHeight&&(typeof _.height===le?h.style.height=(s?_.height+this._borderTop+this._borderBottom:_.height)+"px":h.style.height=_.height),_.setWidth&&(typeof _.width===le?h.style.width=(s?_.width+this._borderLeft+this._borderRight:_.width)+"px":h.style.width=_.width),this._hasListeners("layoutStart")&&this._emit("layoutStart",_.items.slice(0)),!d)return f(),this;for(r=0;rl?1:0:ld?1:0)return c;return c||(s||(s=n(i)),c=r(o,h)),c}function h(o,h){var a=t(o,h);return e&&a&&(a=-a),a||(s||(s=n(i)),r(o,h))}return function(n,r){if(this._isDestroyed||this._items.length<2)return this;var a,_=this._items,l=r||0,d=l.layout?l.layout:void 0===l.layout;if(t=n,e=!!l.descending,i=_.slice(0),s=null,c(t))_.sort(h);else if(typeof t===de)t=n.trim().split(" ").map(function(t){return t.split(":")}),_.sort(o);else{if(!Array.isArray(t))return this;if(t.length!==_.length)throw new Error("[Muuri] sort reference items do not match with grid items.");for(a=0;a<_.length;a++){if(t.indexOf(_[a])<0)throw new Error("[Muuri] sort reference items do not match with grid items.");_[a]=t[a]}e&&_.reverse()}return this._hasListeners("sort")&&this._emit("sort",_.slice(0),i),d&&this.layout("instant"===d,c(d)?d:void 0),this}}(),ce.prototype.move=function(t,e,i){if(this._isDestroyed||this._items.length<2)return this;var s,n,r=this._items,o=i||0,h=o.layout?o.layout:void 0===o.layout,a="swap"===o.action,_=a?"swap":"move",l=this._getItem(t),d=this._getItem(e);return l&&d&&l!==d&&(s=r.indexOf(l),n=r.indexOf(d),a?Q(r,s,n):K(r,s,n),this._hasListeners("move")&&this._emit("move",{item:l,fromIndex:s,toIndex:n,action:_}),h&&this.layout("instant"===h,c(h)?h:void 0)),this},ce.prototype.send=function(t,e,i,s){if(this._isDestroyed||e._isDestroyed||this===e)return this;if(!(t=this._getItem(t)))return this;var n=s||0,r=n.appendTo||window.document.body,o=n.layoutSender?n.layoutSender:void 0===n.layoutSender,h=n.layoutReceiver?n.layoutReceiver:void 0===n.layoutReceiver;return t._migrate.start(e,i,r),t._migrate._isActive&&t._isActive&&(o&&this.layout("instant"===o,c(o)?o:void 0),h&&e.layout("instant"===h,c(h)?h:void 0)),this},ce.prototype.destroy=function(e){if(this._isDestroyed)return this;var i,s=this._element,n=this._items.slice(0);for(this._resizeHandler&&window.removeEventListener("resize",this._resizeHandler),i=0;i-1?t:this._items.length+t]||null;if(t instanceof Kt)return t._gridId===this._id?t:null;for(var e=0;e3&&((i=[]).push.apply(i,arguments),i.shift()),s.push.apply(s,e),this._clearOnEmit&&(e.length=0,this._clearOnEmit=!1),++this._counter;for(var o=n,h=s.length;o-1,f=u.indexOf("trident")>-1,p=u.indexOf("firefox")>-1,m=u.indexOf("android")>-1,g=!!d()&&{passive:!0},v=_(document.documentElement.style,"touchAction");function y(t,e){this._element=t,this._emitter=new r,this._isDestroyed=!1,this._cssProps={},this._touchAction="",this._isActive=!1,this._pointerId=null,this._startTime=0,this._startX=0,this._startY=0,this._currentX=0,this._currentY=0,this._onStart=this._onStart.bind(this),this._onMove=this._onMove.bind(this),this._onCancel=this._onCancel.bind(this),this._onEnd=this._onEnd.bind(this),this._edgeHack=null,(c||f)&&(s||n)&&(this._edgeHack=new h(this)),this.setCssProps(e),this._touchAction||this.setTouchAction("auto"),t.addEventListener("dragstart",y._preventDefault,!1),t.addEventListener(y._inputEvents.start,this._onStart,g)}y._pointerEvents={start:"pointerdown",move:"pointermove",cancel:"pointercancel",end:"pointerup"},y._msPointerEvents={start:"MSPointerDown",move:"MSPointerMove",cancel:"MSPointerCancel",end:"MSPointerUp"},y._touchEvents={start:"touchstart",move:"touchmove",cancel:"touchcancel",end:"touchend"},y._mouseEvents={start:"mousedown",move:"mousemove",cancel:"",end:"mouseup"},y._inputEvents=i?y._touchEvents:s?y._pointerEvents:n?y._msPointerEvents:y._mouseEvents,y._emitter=new r,y._emitterEvents={start:"start",move:"move",end:"end",cancel:"cancel"},y._activeInstances=[],y._preventDefault=function(t){t.preventDefault&&!1!==t.cancelable&&t.preventDefault()},y._activateInstance=function(t){y._activeInstances.indexOf(t)>-1||(y._activeInstances.push(t),y._emitter.on(y._emitterEvents.move,t._onMove),y._emitter.on(y._emitterEvents.cancel,t._onCancel),y._emitter.on(y._emitterEvents.end,t._onEnd),1===y._activeInstances.length&&y._bindListeners())},y._deactivateInstance=function(t){var e=y._activeInstances.indexOf(t);-1!==e&&(y._activeInstances.splice(e,1),y._emitter.off(y._emitterEvents.move,t._onMove),y._emitter.off(y._emitterEvents.cancel,t._onCancel),y._emitter.off(y._emitterEvents.end,t._onEnd),y._activeInstances.length||y._unbindListeners())},y._bindListeners=function(){window.addEventListener(y._inputEvents.move,y._onMove,g),window.addEventListener(y._inputEvents.end,y._onEnd,g),y._inputEvents.cancel&&window.addEventListener(y._inputEvents.cancel,y._onCancel,g)},y._unbindListeners=function(){window.removeEventListener(y._inputEvents.move,y._onMove,g),window.removeEventListener(y._inputEvents.end,y._onEnd,g),y._inputEvents.cancel&&window.removeEventListener(y._inputEvents.cancel,y._onCancel,g)},y._getEventPointerId=function(t){return"number"==typeof t.pointerId?t.pointerId:t.changedTouches?t.changedTouches[0]?t.changedTouches[0].identifier:null:1},y._getTouchById=function(t,e){if("number"==typeof t.pointerId)return t.pointerId===e?t:null;if(t.changedTouches){for(var i=0;i=this.maxValue:this.value<=0},tt.prototype.computeCurrentScrollValue=function(){return null===this.value?1&this.direction?z(this.element):V(this.element):Math.max(0,Math.min(this.value,this.maxValue))},tt.prototype.computeNextScrollValue=function(t){var e=this.speed*(t/1e3),i=4&this.direction?this.value+e:this.value-e;return Math.max(0,Math.min(i,this.maxValue))},tt.prototype.computeSpeed=(G={direction:null,threshold:0,distance:0,value:0,maxValue:0,deltaTime:0,duration:0,isEnding:!1},function(t){var e=this.item,i=Z(e).speed;return C(i)?(G.direction=this.direction,G.threshold=this.threshold,G.distance=this.distance,G.value=this.value,G.maxValue=this.maxValue,G.duration=this.duration,G.speed=this.speed,G.deltaTime=t,G.isEnding=this.isEnding,i(e,this.element,G)):i}),tt.prototype.tick=function(t){return this.isActive||(this.isActive=!0,this.onStart()),this.value=this.computeCurrentScrollValue(),this.speed=this.computeSpeed(t),this.value=this.computeNextScrollValue(t),this.duration+=t,this.value},tt.prototype.onStart=function(){var t=this.item,e=Z(t).onStart;C(e)&&e(t,this.element,this.direction)},tt.prototype.onStop=function(){var t=this.item,e=Z(t).onStop;C(e)&&e(t,this.element,this.direction),t._drag&&t._drag.sort()},et.prototype.reset=function(){this.requestX&&(this.requestX.action=null),this.requestY&&(this.requestY.action=null),this.element=null,this.requestX=null,this.requestY=null,this.scrollLeft=0,this.scrollTop=0},et.prototype.addRequest=function(t){1&t.direction?(this.removeRequest(this.requestX),this.requestX=t):(this.removeRequest(this.requestY),this.requestY=t),t.action=this},et.prototype.removeRequest=function(t){t&&(this.requestX===t?(this.requestX=null,t.action=null):this.requestY===t&&(this.requestY=null,t.action=null))},et.prototype.computeScrollValues=function(){this.scrollLeft=this.requestX?this.requestX.value:z(this.element),this.scrollTop=this.requestY?this.requestY.value:V(this.element)},et.prototype.scroll=function(){var t=this.element;t&&(t.scrollTo?t.scrollTo(this.scrollLeft,this.scrollTop):(t.scrollLeft=this.scrollLeft,t.scrollTop=this.scrollTop))},it.prototype.pick=function(){return this.pool.pop()||this.createItem()},it.prototype.release=function(t){this.releaseItem(t),-1===this.pool.indexOf(t)&&this.pool.push(t)},it.prototype.reset=function(){this.pool.length=0};var nt={width:0,height:0,left:0,right:0,top:0,bottom:0},rt={width:0,height:0,left:0,right:0,top:0,bottom:0};function ot(){this._isDestroyed=!1,this._isTicking=!1,this._tickTime=0,this._tickDeltaTime=0,this._items=[],this._actions=[],this._requests={},this._requests[1]={},this._requests[2]={},this._requestOverlapCheck={},this._dragPositions={},this._dragDirections={},this._overlapCheckInterval=150,this._requestPool=new it((function(){return new tt}),(function(t){t.reset()})),this._actionPool=new it((function(){return new et}),(function(t){t.reset()})),this._readTick=this._readTick.bind(this),this._writeTick=this._writeTick.bind(this)}ot.AXIS_X=1,ot.AXIS_Y=2,ot.FORWARD=4,ot.BACKWARD=8,ot.LEFT=9,ot.RIGHT=5,ot.UP=10,ot.DOWN=6,ot.smoothSpeed=function(t,e,i){return function(s,n,r){var o=0;if(!r.isEnding)if(r.threshold>0){var h=r.threshold-Math.max(0,r.distance);o=t/r.threshold*h}else o=t;var a=r.speed,l=o;return a===o?l:a=A&&w>0&&(g>A||m>R)&&(y=null,v=K("number"==typeof u.threshold?u.threshold:r,o,_.width,d.width),5===a?(S=d.right+v.offset-_.right)<=v.value&&z(c)0&&(y=9),null!==y&&(b=c,A=g,E=v.value,R=m,x=y,T=S,k=w)),p&&g>=I&&D>0&&(g>I||m>X)&&(y=null,v=K("number"==typeof u.threshold?u.threshold:r,o,_.height,d.height),6===l?(S=d.bottom+v.offset-_.bottom)<=v.value&&V(c)0&&(y=10),null!==y&&(L=c,I=g,M=v.value,X=m,Y=y,P=S,q=D)))));e&&(b?this._requestItemScroll(t,1,b,x,E,T,k):this._cancelItemScroll(t,1)),i&&(L?this._requestItemScroll(t,2,L,Y,M,P,q):this._cancelItemScroll(t,2))},ot.prototype._updateScrollRequest=function(t){for(var e=t.item,i=Z(e),s=C(i.targets)?i.targets(e):i.targets,n=s&&s.length||0,r=i.threshold,o=i.safeZone,h=this._getItemHandleRect(e,i.handle,nt),a=rt,l=null,_=null,d=!1,u=null,c=null,f=null,p=null,m=null,g=0;gu.value)break;if(f=d?z(_):V(_),m=4&t.direction?f>=p:f<=0)break;return t.maxValue=p,t.threshold=u.value,t.distance=c,t.isEnding=!1,!0}return!0===i.smoothStop&&t.speed>0?(null===m&&(m=t.hasReachedEnd()),t.isEnding=!m):t.isEnding=!1,t.isEnding},ot.prototype._updateRequests=function(){for(var t,e,i,s,n,r,o,h=this._items,a=this._requests[1],l=this._requests[2],_=0;_0&&this._tickTime-s>this._overlapCheckInterval,r=!0,(e=a[t._id])&&e.isActive&&(r=!this._updateScrollRequest(e))&&(n=!0,this._cancelItemScroll(t,1)),o=!0,(i=l[t._id])&&i.isActive&&(o=!this._updateScrollRequest(i))&&(n=!0,this._cancelItemScroll(t,2)),n&&(this._requestOverlapCheck[t._id]=0,this._checkItemOverlap(t,r,o))},ot.prototype._requestAction=function(t,e){for(var i=this._actions,s=1===e,n=null,r=0;rr?5:so?6:ns?s:e<0?Math.max(s+e+1,0):e}function ft(t,e,i){if(!(t.length<2)){var s=ct(t,e),n=ct(t,i);s!==n&&t.splice(n,0,t.splice(s,1)[0])}}function pt(t,e,i){if(!(t.length<2)){var s,n=ct(t,e),r=ct(t,i);n!==r&&(s=t[n],t[n]=t[r],t[r]=s)}}var mt=_(document.documentElement.style,"transform")||"transform",gt=/([A-Z])/g,vt=/^(webkit-|moz-|ms-|o-)/,yt=/^(-m-s-)/;function St(t){var e=t.replace(gt,"-$1").toLowerCase();return e=(e=e.replace(vt,"-$1")).replace(yt,"-ms-")}var wt=St(mt);function Dt(t){var e=O(t,wt);if(!e||"none"===e)return!1;var i=O(t,"display");return"inline"!==i&&"none"!==i}function bt(t){for(var e=document,i=t||e;i&&i!==e&&"static"===O(i,"position")&&!Dt(i);)i=i.parentElement||e;return i}var At={},Et={},Rt={};function xt(t,e){var i,s=e||{};return s.left=0,s.top=0,t===document?s:(s.left=window.pageXOffset||0,s.top=window.pageYOffset||0,t.self===window.self||(i=t.getBoundingClientRect(),s.left+=i.left,s.top+=i.top,s.left+=H(t,"border-left-width"),s.top+=H(t,"border-top-width")),s)}function Tt(t,e,i){return Rt.left=0,Rt.top=0,t===e||i&&(t=bt(t))===(e=bt(e))||(xt(t,At),xt(e,Et),Rt.left=Et.left-At.left,Rt.top=Et.top-At.top),Rt}function kt(t){return"auto"===t||"scroll"===t||"overlay"===t}function Lt(t){return kt(O(t,"overflow"))||kt(O(t,"overflow-x"))||kt(O(t,"overflow-y"))}function It(t,e){for(e=e||[];t&&t!==document;)t.getRootNode&&t instanceof DocumentFragment?t=t.getRootNode().host:(Lt(t)&&e.push(t),t=t.parentNode);return e.push(window),e}var Ct={},Mt=/^matrix3d/,Xt=/([^,]*,){4}/,Yt=/([^,]*,){12}/,Pt=/[^,]*,/;function qt(t){Ct.x=0,Ct.y=0;var e=O(t,wt);if(!e||"none"===e)return Ct;var i=Mt.test(e),s=e.replace(i?Yt:Xt,""),n=s.replace(Pt,"");return Ct.x=parseFloat(s)||0,Ct.y=parseFloat(n)||0,Ct}function Ot(t,e){e&&(t.classList?t.classList.remove(e):lt(t,"."+e)&&(t.className=(" "+t.className+" ").replace(" "+e+" "," ").trim()))}var Ht,Gt,Wt,Bt,Nt=!!d()&&{passive:!0};function Ft(t){var e=t._element,i=t.getGrid(),s=i._settings;this._item=t,this._gridId=i._id,this._isDestroyed=!1,this._isMigrating=!1,this._startPredicate=C(s.dragStartPredicate)?s.dragStartPredicate:Ft.defaultStartPredicate,this._startPredicateState=0,this._startPredicateResult=void 0,this._isSortNeeded=!1,this._sortTimer=void 0,this._blockedSortIndex=null,this._sortX1=0,this._sortX2=0,this._sortY1=0,this._sortY2=0,this._reset(),this._preStartCheck=this._preStartCheck.bind(this),this._preEndCheck=this._preEndCheck.bind(this),this._onScroll=this._onScroll.bind(this),this._prepareStart=this._prepareStart.bind(this),this._applyStart=this._applyStart.bind(this),this._prepareMove=this._prepareMove.bind(this),this._applyMove=this._applyMove.bind(this),this._prepareScroll=this._prepareScroll.bind(this),this._applyScroll=this._applyScroll.bind(this),this._handleSort=this._handleSort.bind(this),this._handleSortDelayed=this._handleSortDelayed.bind(this),this._handle=s.dragHandle&&e.querySelector(s.dragHandle)||e,this._dragger=new y(this._handle,s.dragCssProps),this._dragger.on("start",this._preStartCheck),this._dragger.on("move",this._preStartCheck),this._dragger.on("cancel",this._preEndCheck),this._dragger.on("end",this._preEndCheck)}function zt(t,e){var i,s,n={};if(Array.isArray(e))for(s=0;s=_||l>=d||(Gt.left=a,Gt.top=l,Gt.width=_-a,Gt.height=d-l,(s=st(Ht,Gt))>i&&s>p&&(p=s,c=r))}return Bt.length=0,c}(t,s,n);if(!h)return null;var a,l,_,d=t.getGrid()!==h,u=0,c=0,f=0,p=-1,m=!1;for(h===s?(Ht.left=i._gridX+t._marginLeft,Ht.top=i._gridY+t._marginTop):(h._updateBorders(1,0,1,0),u=h._left+h._borderLeft,c=h._top+h._borderTop),_=0;_f&&(p=_,f=l));return d&&f=n?(Wt.grid=h,Wt.index=p,Wt.action=d?o:r,Wt):null}),Ft.prototype.stop=function(){if(this._isActive)if(this._isMigrating)this._finishMigration();else{var t=this._item._id;if(R(t),x(t),T(t),this._cancelSort(),this._isStarted){this._unbindScrollListeners();var e=item._element,i=this._getGrid(),s=i._settings.itemDraggingClass;e.parentNode!==i._element&&(i._element.appendChild(e),item._setTranslate(this._gridX,this._gridY),s&&e.clientWidth),Ot(e,s)}this._reset()}},Ft.prototype.sort=function(t){var e=this._item;e._isActive&&this._dragMoveEvent&&(!0===t?this._handleSort():k(e._id,this._handleSort))},Ft.prototype.destroy=function(){this._isDestroyed||(this.stop(),this._dragger.destroy(),Ft.autoScroller.removeItem(this._item),this._isDestroyed=!0)},Ft.prototype._getGrid=function(){return t[this._gridId]||null},Ft.prototype._reset=function(){this._isActive=!1,this._isStarted=!1,this._container=null,this._containingBlock=null,this._dragStartEvent=null,this._dragMoveEvent=null,this._dragPrevMoveEvent=null,this._scrollEvent=null,this._scrollers=[],this._left=0,this._top=0,this._gridX=0,this._gridY=0,this._clientX=0,this._clientY=0,this._scrollDiffX=0,this._scrollDiffY=0,this._moveDiffX=0,this._moveDiffY=0,this._containerDiffX=0,this._containerDiffY=0},Ft.prototype._bindScrollListeners=function(){var t,e,i=this._getGrid()._element,s=this._container,n=this._scrollers;if(n.length=0,It(this._item._element.parentNode,n),s!==i)for(It(i,t=[]),e=0;e3&&i.minBounceBackAngle>0;if(o||(this._blockedSortIndex=null),Math.abs(n)>s||Math.abs(r)>s){if(o){var h=Math.atan2(n,r),a=Math.atan2(this._sortX2-this._sortX1,this._sortY2-this._sortY1),l=Math.atan2(Math.sin(h-a),Math.cos(h-a));Math.abs(l)>i.minBounceBackAngle&&(this._blockedSortIndex=null)}return this._sortX1=this._sortX2,this._sortY1=this._sortY2,this._sortX2=t,this._sortY2=e,!0}return!1},Ft.prototype._resetStartPredicate=function(){var t=this._startPredicateData;t&&(t.delayTimer&&(t.delayTimer=window.clearTimeout(t.delayTimer)),this._startPredicateData=null)},Ft.prototype._handleSort=function(){var t=this._getGrid()._settings;if(!t.dragSort||!t.dragAutoScroll.sortDuringScroll&&Ft.autoScroller.isItemScrolling(this._item))return this._sortX1=this._sortX2=this._gridX,this._sortY1=this._sortY2=this._gridY,this._isSortNeeded=!0,void(void 0!==this._sortTimer&&(this._sortTimer=window.clearTimeout(this._sortTimer)));var e=this._checkHeuristics(this._gridX,this._gridY);if(this._isSortNeeded||e){var i=t.dragSortHeuristics.sortInterval;i<=0||this._isSortNeeded?(this._isSortNeeded=!1,void 0!==this._sortTimer&&(this._sortTimer=window.clearTimeout(this._sortTimer)),this._checkOverlap()):void 0===this._sortTimer&&(this._sortTimer=window.setTimeout(this._handleSortDelayed,i))}},Ft.prototype._handleSortDelayed=function(){this._isSortNeeded=!0,this._sortTimer=void 0,k(this._item._id,this._handleSort)},Ft.prototype._cancelSort=function(){var t;this._isSortNeeded=!1,void 0!==this._sortTimer&&(this._sortTimer=window.clearTimeout(this._sortTimer)),t=this._item._id,b.remove(1,"dragSortRead"+t)},Ft.prototype._finishSort=function(){var t=this._getGrid()._settings.dragSort&&(this._isSortNeeded||void 0!==this._sortTimer);this._cancelSort(),t&&this._checkOverlap()},Ft.prototype._checkOverlap=function(){if(this._isActive){var t,e,i,s,n,r,o,h,a=this._item,l=this._getGrid()._settings;(t=C(l.dragSortPredicate)?l.dragSortPredicate(a,this._dragMoveEvent):Ft.defaultSortPredicate(a,l.dragSortPredicate))&&"number"==typeof t.index&&(o="swap"===t.action?"swap":"move",h=(e=a.getGrid())!==(s=t.grid||e),i=e._items.indexOf(a),n=ct(s._items,t.index,h&&"move"===o?1:0),(h||n!==this._blockedSortIndex)&&(h?(this._blockedSortIndex=null,r=s._items[n],e._hasListeners("beforeSend")&&e._emit("beforeSend",{item:a,fromGrid:e,fromIndex:i,toGrid:s,toIndex:n}),s._hasListeners("beforeReceive")&&s._emit("beforeReceive",{item:a,fromGrid:e,fromIndex:i,toGrid:s,toIndex:n}),a._gridId=s._id,this._isMigrating=a._gridId!==this._gridId,e._items.splice(i,1),ut(s._items,a,n),a._sortData=null,e._hasListeners("send")&&e._emit("send",{item:a,fromGrid:e,fromIndex:i,toGrid:s,toIndex:n}),s._hasListeners("receive")&&s._emit("receive",{item:a,fromGrid:e,fromIndex:i,toGrid:s,toIndex:n}),"swap"===o&&r&&r.isActive()&&s._items.indexOf(r)>-1&&s.send(r,e,i,{appendTo:this._container||document.body,layoutSender:!1,layoutReceiver:!1}),e.layout(),s.layout()):i!==n&&(this._blockedSortIndex=i,("swap"===o?pt:ft)(e._items,i,n),e._hasListeners("move")&&e._emit("move",{item:a,fromIndex:i,toIndex:n,action:o}),e.layout())))}},Ft.prototype._finishMigration=function(){var t,e,i=this._item,s=i._dragRelease,n=i._element,r=i._isActive,o=i.getGrid(),h=o._element,a=o._settings,l=a.dragContainer||h,_=this._getGrid()._settings,d=n.parentNode,u=r?_.itemVisibleClass:_.itemHiddenClass,c=r?a.itemVisibleClass:a.itemHiddenClass;this._isMigrating=!1,this.destroy(),_.itemClass!==a.itemClass&&(Ot(n,_.itemClass),_t(n,a.itemClass)),u!==c&&(Ot(n,u),_t(n,c)),l!==d&&(l.appendChild(n),e=Tt(d,l,!0),(t=qt(n)).x-=e.left,t.y-=e.top),i._refreshDimensions(),e=Tt(l,h,!0),s._containerDiffX=e.left,s._containerDiffY=e.top,i._drag=a.dragEnabled?new Ft(i):null,l!==d&&i._setTranslate(t.x,t.y),i._visibility.setStyles(r?a.visibleStyles:a.hiddenStyles),s.start()},Ft.prototype._preStartCheck=function(t){0===this._startPredicateState&&(this._startPredicateState=1),1===this._startPredicateState?(this._startPredicateResult=this._startPredicate(this._item,t),!0===this._startPredicateResult?(this._startPredicateState=2,this._onStart(t)):!1===this._startPredicateResult&&(this._resetStartPredicate(t),this._dragger._reset(),this._startPredicateState=0)):2===this._startPredicateState&&this._isActive&&this._onMove(t)},Ft.prototype._preEndCheck=function(t){var e=2===this._startPredicateState;this._startPredicate(this._item,t),this._startPredicateState=0,e&&this._isActive&&(this._isStarted?this._onEnd(t):this.stop())},Ft.prototype._onStart=function(t){var e,i,s,n=this._item;n._isActive&&(this._isActive=!0,this._dragStartEvent=t,Ft.autoScroller.addItem(n),e=n._id,i=this._prepareStart,s=this._applyStart,b.add(0,"dragStartRead"+e,i),b.add(2,"dragStartWrite"+e,s))},Ft.prototype._prepareStart=function(){var t=this._item;if(t._isActive){var e=t._element,i=this._getGrid(),s=i._settings,n=i._element,r=s.dragContainer||n,o=bt(r),h=qt(e),a=e.getBoundingClientRect(),l=r!==n;if(this._container=r,this._containingBlock=o,this._clientX=a.left,this._clientY=a.top,this._left=this._gridX=h.x,this._top=this._gridY=h.y,this._scrollDiffX=this._scrollDiffY=0,this._moveDiffX=this._moveDiffY=0,this._resetHeuristics(this._gridX,this._gridY),l){var _=Tt(o,n);this._containerDiffX=_.left,this._containerDiffY=_.top}}},Ft.prototype._applyStart=function(){var t=this._item;if(t._isActive){var e=this._getGrid(),i=t._element,s=t._dragRelease,n=t._migrate,r=this._container!==e._element;t.isPositioning()&&t._layout.stop(!0,this._left,this._top),n._isActive&&(this._left-=n._containerDiffX,this._top-=n._containerDiffY,this._gridX-=n._containerDiffX,this._gridY-=n._containerDiffY,n.stop(!0,this._left,this._top)),t.isReleasing()&&s._reset(),e._settings.dragPlaceholder.enabled&&t._dragPlaceholder.create(),this._isStarted=!0,e._emit("dragInit",t,this._dragStartEvent),r&&(i.parentNode===this._container?(this._gridX-=this._containerDiffX,this._gridY-=this._containerDiffY):(this._left+=this._containerDiffX,this._top+=this._containerDiffY,this._container.appendChild(i),t._setTranslate(this._left,this._top))),_t(i,e._settings.itemDraggingClass),this._bindScrollListeners(),e._emit("dragStart",t,this._dragStartEvent)}},Ft.prototype._onMove=function(t){var e,i,s,n=this._item;n._isActive?(this._dragMoveEvent=t,e=n._id,i=this._prepareMove,s=this._applyMove,b.add(0,"dragMoveRead"+e,i),b.add(2,"dragMoveWrite"+e,s),k(n._id,this._handleSort)):this.stop()},Ft.prototype._prepareMove=function(){if(this._item._isActive){var t=this._getGrid()._settings.dragAxis,e=this._dragMoveEvent,i=this._dragPrevMoveEvent||this._dragStartEvent||e;if("y"!==t){var s=e.clientX-i.clientX;this._left=this._left-this._moveDiffX+s,this._gridX=this._gridX-this._moveDiffX+s,this._clientX=this._clientX-this._moveDiffX+s,this._moveDiffX=s}if("x"!==t){var n=e.clientY-i.clientY;this._top=this._top-this._moveDiffY+n,this._gridY=this._gridY-this._moveDiffY+n,this._clientY=this._clientY-this._moveDiffY+n,this._moveDiffY=n}this._dragPrevMoveEvent=e}},Ft.prototype._applyMove=function(){var t=this._item;t._isActive&&(this._moveDiffX=this._moveDiffY=0,t._setTranslate(this._left,this._top),this._getGrid()._emit("dragMove",t,this._dragMoveEvent),Ft.autoScroller.updateItem(t))},Ft.prototype._onScroll=function(t){var e,i,s,n=this._item;n._isActive?(this._scrollEvent=t,e=n._id,i=this._prepareScroll,s=this._applyScroll,b.add(0,"dragScrollRead"+e,i),b.add(2,"dragScrollWrite"+e,s),k(n._id,this._handleSort)):this.stop()},Ft.prototype._prepareScroll=function(){var t=this._item;if(t._isActive){var e=t._element,i=this._getGrid(),s=i._element,n=i._settings.dragAxis,r="y"!==n,o="x"!==n,h=e.getBoundingClientRect();if(this._container!==s){var a=Tt(this._containingBlock,s);this._containerDiffX=a.left,this._containerDiffY=a.top}if(r){var l=this._clientX-this._moveDiffX-this._scrollDiffX-h.left;this._left=this._left-this._scrollDiffX+l,this._scrollDiffX=l}if(o){var _=this._clientY-this._moveDiffY-this._scrollDiffY-h.top;this._top=this._top-this._scrollDiffY+_,this._scrollDiffY=_}this._gridX=this._left-this._containerDiffX,this._gridY=this._top-this._containerDiffY}},Ft.prototype._applyScroll=function(){var t=this._item;t._isActive&&(this._scrollDiffX=this._scrollDiffY=0,t._setTranslate(this._left,this._top),this._getGrid()._emit("dragScroll",t,this._scrollEvent))},Ft.prototype._onEnd=function(t){var e=this._item,i=e._element,s=this._getGrid(),n=s._settings,r=e._dragRelease;e._isActive?(R(e._id),x(e._id),T(e._id),this._finishSort(),this._unbindScrollListeners(),r._containerDiffX=this._containerDiffX,r._containerDiffY=this._containerDiffY,this._reset(),Ot(i,n.itemDraggingClass),Ft.autoScroller.removeItem(e),s._emit("dragEnd",e,t),this._isMigrating?this._finishMigration():r.start()):this.stop()};var Vt=/^(webkit|moz|ms|o|Webkit|Moz|MS|O)(?=[A-Z])/,jt={};function Qt(t){var e=jt[t];return e||((e=t.replace(Vt,""))!==t&&(e=e[0].toLowerCase()+e.slice(1)),jt[t]=e,e)}function Ut(t,e){for(var i in e)t.style[i]=e[i]}var Zt,Jt,$t=!(!Element||!C(Element.prototype.animate)),Kt=!!(Element&&(Zt=Element.prototype.animate,Jt=window.Symbol,Zt&&C(Jt)&&C(Jt.toString)&&Jt(Zt).toString().indexOf("[native code]")>-1));function te(t){this._element=t,this._animation=null,this._duration=0,this._easing="",this._callback=null,this._props=[],this._values=[],this._isDestroyed=!1,this._onFinish=this._onFinish.bind(this)}function ee(t,e){var i={};for(var s in t)i[e?s:Qt(s)]=t[s];return i}function ie(t,e){return"translateX("+t+"px) translateY("+e+"px)"}function se(t){this._item=t,this._animation=new te,this._element=null,this._className="",this._didMigrate=!1,this._resetAfterLayout=!1,this._left=0,this._top=0,this._transX=0,this._transY=0,this._nextTransX=0,this._nextTransY=0,this._setupAnimation=this._setupAnimation.bind(this),this._startAnimation=this._startAnimation.bind(this),this._updateDimensions=this._updateDimensions.bind(this),this._onLayoutStart=this._onLayoutStart.bind(this),this._onLayoutEnd=this._onLayoutEnd.bind(this),this._onReleaseEnd=this._onReleaseEnd.bind(this),this._onMigrate=this._onMigrate.bind(this),this._onHide=this._onHide.bind(this)}function ne(t){this._item=t,this._isActive=!1,this._isDestroyed=!1,this._isPositioningStarted=!1,this._containerDiffX=0,this._containerDiffY=0}te.prototype.start=function(t,e,i){if(!this._isDestroyed){var s=this._element,n=i||{};if(!$t)return Ut(s,e),this._callback=C(n.onFinish)?n.onFinish:null,void this._onFinish();var r,o,h,a=this._animation,l=this._props,_=this._values,d=n.duration||300,u=n.easing||"ease",c=!1;if(a&&(o=0,d===this._duration&&u===this._easing||(c=!0),!c)){for(r in e)if(++o,-1===(h=l.indexOf(r))||e[r]!==_[h]){c=!0;break}o!==l.length&&(c=!0)}if(c&&a.cancel(),this._callback=C(n.onFinish)?n.onFinish:null,!a||c){for(r in l.length=_.length=0,e)l.push(r),_.push(e[r]);this._duration=d,this._easing=u,this._animation=s.animate([ee(t,Kt),ee(e,Kt)],{duration:d,easing:u}),this._animation.onfinish=this._onFinish,Ut(s,e)}}},te.prototype.stop=function(){!this._isDestroyed&&this._animation&&(this._animation.cancel(),this._animation=this._callback=null,this._props.length=this._values.length=0)},te.prototype.getCurrentStyles=function(){return zt(element,currentProps)},te.prototype.isAnimating=function(){return!!this._animation},te.prototype.destroy=function(){this._isDestroyed||(this.stop(),this._element=null,this._isDestroyed=!0)},te.prototype._onFinish=function(){var t=this._callback;this._animation=this._callback=null,this._props.length=this._values.length=0,t&&t()},se.prototype._updateDimensions=function(){this.isActive()&&Ut(this._element,{width:this._item._width+"px",height:this._item._height+"px"})},se.prototype._onLayoutStart=function(t,e){var i=this._item;if(-1!==t.indexOf(i)){var s=i._left,n=i._top,r=this._left,o=this._top;if(this._left=s,this._top=n,e||this._didMigrate||r!==s||o!==n){var h,a,l,_=s+i._marginLeft,d=n+i._marginTop,u=i.getGrid();if(!(!e&&u._settings.layoutDuration>0)||this._didMigrate)return L(i._id),this._element.style[mt]=ie(_,d),this._animation.stop(),void(this._didMigrate&&(u.getElement().appendChild(this._element),this._didMigrate=!1));this._nextTransX=_,this._nextTransY=d,h=i._id,a=this._setupAnimation,l=this._startAnimation,b.add(0,"placeholderLayoutRead"+h,a),b.add(2,"placeholderLayoutWrite"+h,l)}}else this.reset()},se.prototype._setupAnimation=function(){if(this.isActive()){var t=qt(this._element);this._transX=t.x,this._transY=t.y}},se.prototype._startAnimation=function(){if(this.isActive()){var t=this._animation,e=this._transX,i=this._transY,s=this._nextTransX,n=this._nextTransY;if(e!==s||i!==n){var r=this._item.getGrid()._settings,o={},h={};o[mt]=ie(e,i),h[mt]=ie(s,n),t.start(o,h,{duration:r.layoutDuration,easing:r.layoutEasing,onFinish:this._onLayoutEnd})}else t.isAnimating()&&(this._element.style[mt]=ie(s,n),t.stop())}},se.prototype._onLayoutEnd=function(){this._resetAfterLayout&&this.reset()},se.prototype._onReleaseEnd=function(t){if(t._id===this._item._id){if(!this._animation.isAnimating())return void this.reset();this._resetAfterLayout=!0}},se.prototype._onMigrate=function(t){if(t.item===this._item){var e=this._item.getGrid(),i=t.toGrid;e.off("dragReleaseEnd",this._onReleaseEnd),e.off("layoutStart",this._onLayoutStart),e.off("beforeSend",this._onMigrate),e.off("hideStart",this._onHide),i.on("dragReleaseEnd",this._onReleaseEnd),i.on("layoutStart",this._onLayoutStart),i.on("beforeSend",this._onMigrate),i.on("hideStart",this._onHide),this._didMigrate=!0}},se.prototype._onHide=function(t){t.indexOf(this._item)>-1&&this.reset()},se.prototype.create=function(){if(this.isActive())this._resetAfterLayout=!1;else{var t,e=this._item,i=e.getGrid(),s=i._settings,n=this._animation;this._left=e._left,this._top=e._top,t=C(s.dragPlaceholder.createElement)?s.dragPlaceholder.createElement(e):document.createElement("div"),this._element=t,n._element=t,this._className=s.itemPlaceholderClass||"",this._className&&_t(t,this._className),Ut(t,{position:"absolute",left:"0px",top:"0px",width:e._width+"px",height:e._height+"px"}),t.style[mt]=ie(e._left+e._marginLeft,e._top+e._marginTop),i.on("layoutStart",this._onLayoutStart),i.on("dragReleaseEnd",this._onReleaseEnd),i.on("beforeSend",this._onMigrate),i.on("hideStart",this._onHide),C(s.dragPlaceholder.onCreate)&&s.dragPlaceholder.onCreate(e,t),i.getElement().appendChild(t)}},se.prototype.reset=function(){if(this.isActive()){var t,e=this._element,i=this._item,s=i.getGrid(),n=s._settings,r=this._animation;this._resetAfterLayout=!1,L(i._id),t=i._id,b.remove(2,"placeholderResizeWrite"+t),r.stop(),r._element=null,s.off("dragReleaseEnd",this._onReleaseEnd),s.off("layoutStart",this._onLayoutStart),s.off("beforeSend",this._onMigrate),s.off("hideStart",this._onHide),this._className&&(Ot(e,this._className),this._className=""),e.parentNode.removeChild(e),this._element=null,C(n.dragPlaceholder.onRemove)&&n.dragPlaceholder.onRemove(i,e)}},se.prototype.isActive=function(){return!!this._element},se.prototype.getElement=function(){return this._element},se.prototype.updateDimensions=function(){var t,e;this.isActive()&&(t=this._item._id,e=this._updateDimensions,b.add(2,"placeholderResizeWrite"+t,e))},se.prototype.destroy=function(){this.reset(),this._animation.destroy(),this._item=this._animation=null},ne.prototype.start=function(){if(!this._isDestroyed&&!this._isActive){var t=this._item,e=t.getGrid(),i=e._settings;this._isActive=!0,_t(t._element,i.itemReleasingClass),i.dragRelease.useDragContainer||this._placeToGrid(),e._emit("dragReleaseStart",t),e._nextLayoutData||t._layout.start(!1)}},ne.prototype.stop=function(t,e,i){if(!this._isDestroyed&&this._isActive){var s=this._item,n=s.getGrid();t||void 0!==e&&void 0!==i||(e=s._left,i=s._top);var r=this._placeToGrid(e,i);this._reset(r),t||n._emit("dragReleaseEnd",s)}},ne.prototype.isJustReleased=function(){return this._isActive&&!1===this._isPositioningStarted},ne.prototype.destroy=function(){this._isDestroyed||(this.stop(!0),this._item=null,this._isDestroyed=!0)},ne.prototype._placeToGrid=function(t,e){if(!this._isDestroyed){var i=this._item,s=i._element,n=i.getGrid()._element,r=!1;if(s.parentNode!==n){if(void 0===t||void 0===e){var o=qt(s);t=o.x-this._containerDiffX,e=o.y-this._containerDiffY}n.appendChild(s),i._setTranslate(t,e),r=!0}return this._containerDiffX=0,this._containerDiffY=0,r}},ne.prototype._reset=function(t){if(!this._isDestroyed){var e=this._item,i=e.getGrid()._settings.itemReleasingClass;this._isActive=!1,this._isPositioningStarted=!1,this._containerDiffX=0,this._containerDiffY=0,i&&(t&&e._element.clientWidth,Ot(e._element,i))}};function re(t){var e=t._element,i=e.style;this._item=t,this._isActive=!1,this._isDestroyed=!1,this._isInterrupted=!1,this._currentStyles={},this._targetStyles={},this._nextLeft=0,this._nextTop=0,this._offsetLeft=0,this._offsetTop=0,this._skipNextAnimation=!1,this._animOptions={onFinish:this._finish.bind(this),duration:0,easing:0},i.left="0px",i.top="0px",t._setTranslate(0,0),this._animation=new te(e),this._queue="layout-"+t._id,this._setupAnimation=this._setupAnimation.bind(this),this._startAnimation=this._startAnimation.bind(this)}function oe(t){this._item=t,this._isActive=!1,this._isDestroyed=!1,this._container=!1,this._containerDiffX=0,this._containerDiffY=0}function he(t){var e=t._isActive,i=t._element,s=i.children[0],n=t.getGrid()._settings;if(!s)throw new Error("No valid child element found within item element.");this._item=t,this._isDestroyed=!1,this._isHidden=!e,this._isHiding=!1,this._isShowing=!1,this._childElement=s,this._currentStyleProps=[],this._animation=new te(s),this._queue="visibility-"+t._id,this._finishShow=this._finishShow.bind(this),this._finishHide=this._finishHide.bind(this),i.style.display=e?"":"none",_t(i,e?n.itemVisibleClass:n.itemHiddenClass),this.setStyles(e?n.visibleStyles:n.hiddenStyles)}re.prototype.start=function(t,e){if(!this._isDestroyed){var i,s,n,r=this._item,o=r._dragRelease,h=r.getGrid()._settings,a=this._isActive,l=o.isJustReleased(),_=l?h.dragRelease.duration:h.layoutDuration,d=l?h.dragRelease.easing:h.layoutEasing,u=!t&&!this._skipNextAnimation&&_>0;if(a&&(A(r._id),r._emitter.burst(this._queue,!0,r)),l&&(o._isPositioningStarted=!0),C(e)&&r._emitter.on(this._queue,e),this._skipNextAnimation=!1,!u)return this._updateOffsets(),r._setTranslate(this._nextLeft,this._nextTop),this._animation.stop(),void this._finish();this._isActive=!0,this._animOptions.easing=d,this._animOptions.duration=_,this._isInterrupted=a,i=r._id,s=this._setupAnimation,n=this._startAnimation,b.add(0,"layoutRead"+i,s),b.add(2,"layoutWrite"+i,n)}},re.prototype.stop=function(t,e,i){if(!this._isDestroyed&&this._isActive){var s=this._item;if(A(s._id),this._animation.isAnimating()){if(void 0===e||void 0===i){var n=qt(s._element);e=n.x,i=n.y}s._setTranslate(e,i),this._animation.stop()}Ot(s._element,s.getGrid()._settings.itemPositioningClass),this._isActive=!1,t&&s._emitter.burst(this._queue,!0,s)}},re.prototype.destroy=function(){if(!this._isDestroyed){var t=this._item._element.style;this.stop(!0,0,0),this._item._emitter.clear(this._queue),this._animation.destroy(),t[mt]="",t.left="",t.top="",this._item=null,this._currentStyles=null,this._targetStyles=null,this._animOptions=null,this._isDestroyed=!0}},re.prototype._updateOffsets=function(){if(!this._isDestroyed){var t=this._item,e=t._migrate,i=t._dragRelease;this._offsetLeft=i._isActive?i._containerDiffX:e._isActive?e._containerDiffX:0,this._offsetTop=i._isActive?i._containerDiffY:e._isActive?e._containerDiffY:0,this._nextLeft=this._item._left+this._offsetLeft,this._nextTop=this._item._top+this._offsetTop}},re.prototype._finish=function(){if(!this._isDestroyed){var t=this._item,e=t._migrate,i=t._dragRelease;t._tX=this._nextLeft,t._tY=this._nextTop,this._isActive&&(this._isActive=!1,Ot(t._element,t.getGrid()._settings.itemPositioningClass)),i._isActive&&i.stop(),e._isActive&&e.stop(),t._emitter.burst(this._queue,!1,t)}},re.prototype._setupAnimation=function(){var t=this._item;if(void 0===t._tX||void 0===t._tY){var e=qt(t._element);t._tX=e.x,t._tY=e.y}},re.prototype._startAnimation=function(){var t=this._item,e=t.getGrid()._settings,i=this._animOptions.duration<=0;this._updateOffsets();var s=Math.abs(t._left-(t._tX-this._offsetLeft)),n=Math.abs(t._top-(t._tY-this._offsetTop));if(i||s<2&&n<2)return(s||n||this._isInterrupted)&&t._setTranslate(this._nextLeft,this._nextTop),this._animation.stop(),void this._finish();this._isInterrupted||_t(t._element,e.itemPositioningClass),this._currentStyles[mt]=ie(t._tX,t._tY),this._targetStyles[mt]=ie(this._nextLeft,this._nextTop),t._tX=t._tY=void 0,this._animation.start(this._currentStyles,this._targetStyles,this._animOptions)},oe.prototype.start=function(t,e,i){if(!this._isDestroyed){var s,n,r,o,h,a,l,_,d,u,c=this._item,f=c._element,p=c.isActive(),m=c.isVisible(),g=c.getGrid(),v=g._settings,y=t._settings,S=t._element,w=t._items,D=g._items.indexOf(c),b=i||document.body;if("number"==typeof e)s=ct(w,e,1);else{if(!(n=t.getItem(e)))return;s=w.indexOf(n)}(c.isPositioning()||this._isActive||c.isReleasing())&&(l=(a=qt(f)).x,_=a.y),c.isPositioning()&&c._layout.stop(!0,l,_),this._isActive&&(l-=this._containerDiffX,_-=this._containerDiffY,this.stop(!0,l,_)),c.isReleasing()&&(l-=c._dragRelease._containerDiffX,_-=c._dragRelease._containerDiffY,c._dragRelease.stop(!0,l,_)),c._visibility.stop(!0),c._drag&&c._drag.destroy(),g._hasListeners("beforeSend")&&g._emit("beforeSend",{item:c,fromGrid:g,fromIndex:D,toGrid:t,toIndex:s}),t._hasListeners("beforeReceive")&&t._emit("beforeReceive",{item:c,fromGrid:g,fromIndex:D,toGrid:t,toIndex:s}),v.itemClass!==y.itemClass&&(Ot(f,v.itemClass),_t(f,y.itemClass)),(d=m?v.itemVisibleClass:v.itemHiddenClass)!==(u=m?y.itemVisibleClass:y.itemHiddenClass)&&(Ot(f,d),_t(f,u)),g._items.splice(D,1),ut(w,c,s),c._gridId=t._id,p?b!==(r=f.parentNode)&&(b.appendChild(f),o=Tt(b,r,!0),a||(l=(a=qt(f)).x,_=a.y),c._setTranslate(l+o.left,_+o.top)):S.appendChild(f),c._visibility.setStyles(m?y.visibleStyles:y.hiddenStyles),p&&(h=Tt(b,S,!0)),c._refreshDimensions(),c._sortData=null,c._drag=y.dragEnabled?new Ft(c):null,p?(this._isActive=!0,this._container=b,this._containerDiffX=h.left,this._containerDiffY=h.top):(this._isActive=!1,this._container=null,this._containerDiffX=0,this._containerDiffY=0),g._hasListeners("send")&&g._emit("send",{item:c,fromGrid:g,fromIndex:D,toGrid:t,toIndex:s}),t._hasListeners("receive")&&t._emit("receive",{item:c,fromGrid:g,fromIndex:D,toGrid:t,toIndex:s})}},oe.prototype.stop=function(t,e,i){if(!this._isDestroyed&&this._isActive){var s,n=this._item,r=n._element,o=n.getGrid()._element;this._container!==o&&(void 0!==e&&void 0!==i||(t?(e=(s=qt(r)).x-this._containerDiffX,i=s.y-this._containerDiffY):(e=n._left,i=n._top)),o.appendChild(r),n._setTranslate(e,i)),this._isActive=!1,this._container=null,this._containerDiffX=0,this._containerDiffY=0}},oe.prototype.destroy=function(){this._isDestroyed||(this.stop(!0),this._item=null,this._isDestroyed=!0)},he.prototype.show=function(t,e){if(!this._isDestroyed){var i=this._item,s=i._element,n=C(e)?e:null,r=i.getGrid()._settings;this._isShowing||this._isHidden?!this._isShowing||t?(this._isShowing||(i._emitter.burst(this._queue,!0,i),Ot(s,r.itemHiddenClass),_t(s,r.itemVisibleClass),this._isHiding||(s.style.display="")),n&&i._emitter.on(this._queue,n),this._isShowing=!0,this._isHiding=this._isHidden=!1,this._startAnimation(!0,t,this._finishShow)):n&&i._emitter.on(this._queue,n):n&&n(!1,i)}},he.prototype.hide=function(t,e){if(!this._isDestroyed){var i=this._item,s=i._element,n=C(e)?e:null,r=i.getGrid()._settings;this._isHiding||!this._isHidden?!this._isHiding||t?(this._isHiding||(i._emitter.burst(this._queue,!0,i),_t(s,r.itemHiddenClass),Ot(s,r.itemVisibleClass)),n&&i._emitter.on(this._queue,n),this._isHidden=this._isHiding=!0,this._isShowing=!1,this._startAnimation(!1,t,this._finishHide)):n&&i._emitter.on(this._queue,n):n&&n(!1,i)}},he.prototype.stop=function(t){if(!this._isDestroyed&&(this._isHiding||this._isShowing)){var e=this._item;E(e._id),this._animation.stop(),t&&e._emitter.burst(this._queue,!0,e)}},he.prototype.setStyles=function(t){var e=this._childElement,i=this._currentStyleProps;for(var s in this._removeCurrentStyles(),t)i.push(s),e.style[s]=t[s]},he.prototype.destroy=function(){if(!this._isDestroyed){var t=this._item,e=t._element,i=t.getGrid()._settings;this.stop(!0),t._emitter.clear(this._queue),this._animation.destroy(),this._removeCurrentStyles(),Ot(e,i.itemVisibleClass),Ot(e,i.itemHiddenClass),e.style.display="",this._isHiding=this._isShowing=!1,this._isDestroyed=this._isHidden=!0}},he.prototype._startAnimation=function(t,e,i){if(!this._isDestroyed){var s,n=this._item,r=this._animation,o=this._childElement,h=n.getGrid()._settings,a=t?h.visibleStyles:h.hiddenStyles,l=t?h.showDuration:h.hideDuration,_=t?h.showEasing:h.hideEasing,d=e||l<=0;if(a){if(E(n._id),d)return Ut(o,a),r.stop(),void(i&&i());var u,c,f;u=n._id,c=function(){s=zt(o,a)},f=function(){r.start(s,a,{duration:l,easing:_,onFinish:i})},b.add(0,"visibilityRead"+u,c),b.add(2,"visibilityWrite"+u,f)}else i&&i()}},he.prototype._finishShow=function(){this._isHidden||(this._isShowing=!1,this._item._emitter.burst(this._queue,!1,this._item))},he.prototype._finishHide=function(){if(this._isHidden){var t=this._item;this._isHiding=!1,t._layout.stop(!0,0,0),t._element.style.display="none",t._emitter.burst(this._queue,!1,t)}},he.prototype._removeCurrentStyles=function(){for(var t=this._childElement,e=this._currentStyleProps,i=0;it.width&&(t.width=a.left+a.width):a.top+a.height>t.height&&(t.height=a.top+a.height),_[++this.slotIndex]=a.left,_[++this.slotIndex]=a.top,(c||f)&&this.slotSizes.push(a.width,a.height);if(c)for(i=0;i<_.length;i+=2)_[i]=t.width-(_[i]+this.slotSizes[i]);if(f)for(i=1;i<_.length;i+=2)_[i]=t.height-(_[i]+this.slotSizes[i]);return this.slotSizes.length=0,this.currentRects.length=0,this.nextRects.length=0,this.rectId=0,this.slotIndex=-1,t},a.prototype.computeNextSlot=function(t,e,i,s,n){var r,h,a,l,_,d=this.slotData,u=this.currentRects,c=this.nextRects,f=!1;for(c.length=0,d.left=null,d.top=null,d.width=e,d.height=i,l=0;lt.height+o&&(d.left>.5&&c.push(this.addRect(0,t.height,d.left,1/0)),d.left+d.widtht.width+o&&(d.top>.5&&c.push(this.addRect(t.width,0,1/0,d.top)),d.top+d.height1&&this.purgeRects(c).sort(n?this.sortRectsLeftTop:this.sortRectsTopLeft),this.currentRects=c,this.nextRects=u,d},a.prototype.addRect=function(t,e,i,s){var n=++this.rectId;return this.rectStore[n]=t||0,this.rectStore[++this.rectId]=e||0,this.rectStore[++this.rectId]=i||0,this.rectStore[++this.rectId]=s||0,n},a.prototype.getRect=function(t,e){return e||(e=this.rectTarget),e.left=this.rectStore[t]||0,e.top=this.rectStore[++t]||0,e.width=this.rectStore[++t]||0,e.height=this.rectStore[++t]||0,e},a.prototype.splitRect=(e=[],i=0,s=0,function(t,n){return e.length=0,t.left+t.width<=n.left+o||n.left+n.width<=t.left+o||t.top+t.height<=n.top+o||n.top+n.height<=t.top+o?(e.push(this.addRect(t.left,t.top,t.width,t.height)),e):((i=n.left-t.left)>=.5&&e.push(this.addRect(t.left,t.top,i,t.height)),(i=t.left+t.width-(n.left+n.width))>=.5&&e.push(this.addRect(n.left+n.width,t.top,i,t.height)),(s=n.top-t.top)>=.5&&e.push(this.addRect(t.left,t.top,t.width,s)),(s=t.top+t.height-(n.top+n.height))>=.5&&e.push(this.addRect(t.left,n.top+n.height,t.width,s)),e)}),a.prototype.isRectAWithinRectB=function(t,e){return t.left+o>=e.left&&t.top+o>=e.top&&t.left+t.width-o<=e.left+e.width&&t.top+t.height-o<=e.top+e.height},a.prototype.purgeRects=(n={},r={},function(t){for(var e,i=t.length;i--;)if(e=t.length,t[i])for(this.getRect(t[i],n);e--;)if(t[e]&&i!==e&&(this.getRect(t[e],r),this.isRectAWithinRectB(n,r))){t[i]=0;break}return t}),a.prototype.sortRectsTopLeft=function(){var t={},e={};return function(i,s){return this.getRect(i,t),this.getRect(s,e),t.tope.top&&t.top-o>e.top?1:t.lefte.left&&t.left-o>e.left?1:0}}(),a.prototype.sortRectsLeftTop=function(){var t={},e={};return function(i,s){return this.getRect(i,t),this.getRect(s,e),t.lefte.left&&t.left-oe.top&&t.top-o>e.top?1:0}}(),t){var l=new a;self.onmessage=function(t){var e=new Float32Array(t.data),i=e.subarray(4,e.length),s=new Float32Array(i.length),n=e[3],r={items:i,slots:s,width:e[1],height:e[2]};l.computeLayout(r,n),e[1]=r.width,e[2]=r.height,e.set(r.slots,4),postMessage(e.buffer,[e.buffer])}}return a}_e.prototype.getGrid=function(){return t[this._gridId]},_e.prototype.getElement=function(){return this._element},_e.prototype.getWidth=function(){return this._width},_e.prototype.getHeight=function(){return this._height},_e.prototype.getMargin=function(){return{left:this._marginLeft,right:this._marginRight,top:this._marginTop,bottom:this._marginBottom}},_e.prototype.getPosition=function(){return{left:this._left,top:this._top}},_e.prototype.isActive=function(){return this._isActive},_e.prototype.isVisible=function(){return!!this._visibility&&!this._visibility._isHidden},_e.prototype.isShowing=function(){return!(!this._visibility||!this._visibility._isShowing)},_e.prototype.isHiding=function(){return!(!this._visibility||!this._visibility._isHiding)},_e.prototype.isPositioning=function(){return!(!this._layout||!this._layout._isActive)},_e.prototype.isDragging=function(){return!(!this._drag||!this._drag._isActive)},_e.prototype.isReleasing=function(){return!(!this._dragRelease||!this._dragRelease._isActive)},_e.prototype.isDestroyed=function(){return this._isDestroyed},_e.prototype._refreshDimensions=function(t){if(!(this._isDestroyed||!0!==t&&this._visibility._isHidden)){var e=this._element,i=this._dragPlaceholder,s=e.getBoundingClientRect();this._width=s.width,this._height=s.height,this._marginLeft=Math.max(0,H(e,"margin-left")),this._marginRight=Math.max(0,H(e,"margin-right")),this._marginTop=Math.max(0,H(e,"margin-top")),this._marginBottom=Math.max(0,H(e,"margin-bottom")),i&&i.updateDimensions()}},_e.prototype._refreshSortData=function(){if(!this._isDestroyed){var t,e=this._sortData={},i=this.getGrid()._settings.sortData;for(t in i)e[t]=i[t](this,this._element)}},_e.prototype._addToLayout=function(t,e){!0!==this._isActive&&(this._isActive=!0,this._left=t||0,this._top=e||0)},_e.prototype._removeFromLayout=function(){!1!==this._isActive&&(this._isActive=!1,this._left=0,this._top=0)},_e.prototype._canSkipLayout=function(t,e){return this._left===t&&this._top===e&&!this._migrate._isActive&&!this._layout._skipNextAnimation&&!this._dragRelease.isJustReleased()},_e.prototype._setTranslate=function(t,e){return(this._tX!==t||this._tY!==e)&&(this._tX=t,this._tY=e,this._element.style[mt]=ie(t,e),!0)},_e.prototype._destroy=function(t){if(!this._isDestroyed){var i=this._element,s=this.getGrid()._settings;this._dragPlaceholder.destroy(),this._dragRelease.destroy(),this._migrate.destroy(),this._layout.destroy(),this._visibility.destroy(),this._drag&&this._drag.destroy(),this._emitter.destroy(),Ot(i,s.itemClass),t&&i.parentNode.removeChild(i),e&&e.delete(i),this._isActive=!1,this._isDestroyed=!0}};var ue=de(),ce=null,fe=[];function pe(t,e){if(this._options=0,this._processor=null,this._layoutQueue=[],this._layouts={},this._layoutCallbacks={},this._layoutWorkers={},this._layoutWorkerData={},this._workers=[],this._onWorkerMessage=this._onWorkerMessage.bind(this),this.setOptions(e),(t="number"==typeof t?Math.max(0,t):0)&&window.Worker&&window.URL&&window.Blob)try{this._workers=function(t,e){var i=[];if(t>0){ce||(ce=URL.createObjectURL(new Blob(["("+de.toString()+")(true)"],{type:"application/javascript"})));for(var s,n=0;n-1&&this._layoutQueue.splice(e,1)}},pe.prototype.destroy=function(){for(var t in this._layoutWorkers)this._workers.push(this._layoutWorkers[t]);!function(t){for(var e,i,s=0;s-1&&fe.splice(i,1);ce&&!fe.length&&(URL.revokeObjectURL(ce),ce=null)}(this._workers),this._workers.length=0,this._layoutQueue.length=0,this._layouts={},this._layoutCallbacks={},this._layoutWorkers={},this._layoutWorkerData={}};var me=0;function ge(t,e){var i=++me,s=0,n=0,r=!1,o=function(e){r||(n&&(s-=e-n),n=e,s>0?function(t,e){b.add(0,"debounceRead"+t,e)}(i,o):(s=n=0,t()))};return function(h){if(!r){if(!(e<=0))return!0===h?(r=!0,s=n=0,o=void 0,void function(t){b.remove(0,"debounceRead"+t)}(i)):void(s<=0?(s=e,o(0)):s=e);!0!==h&&t()}}}function ve(t){var e=Object.prototype.toString.call(t);return"[object HTMLCollection]"===e||"[object NodeList]"===e}var ye=Object.prototype.toString;function Se(t){return"object"==typeof t&&"[object Object]"===ye.call(t)}function we(){}var De,be=0;function Ae(e,i){if("string"==typeof e&&(e=document.querySelector(e)),!(e.getRootNode?e.getRootNode({composed:!0})===document:document.body.contains(e))||e===document.documentElement)throw new Error("Container element must be an existing DOM element.");var s=function(t,e){var i=Ee({},t);e&&(i=Ee(i,e));e&&e.visibleStyles?i.visibleStyles=e.visibleStyles:t&&t.visibleStyles&&(i.visibleStyles=t.visibleStyles);e&&e.hiddenStyles?i.hiddenStyles=e.hiddenStyles:t&&t.hiddenStyles&&(i.hiddenStyles=t.hiddenStyles);return i}(Ae.defaultOptions,i);s.visibleStyles=Re(s.visibleStyles),s.hiddenStyles=Re(s.hiddenStyles),C(s.dragSort)||(s.dragSort=!!s.dragSort),this._id=le(),this._element=e,this._settings=s,this._isDestroyed=!1,this._items=[],this._layout={id:0,items:[],slots:[]},this._isLayoutFinished=!0,this._nextLayoutData=null,this._emitter=new r,this._onLayoutDataReceived=this._onLayoutDataReceived.bind(this),t[this._id]=this,_t(e,s.containerClass),function(t,e){"number"!=typeof e&&(e=!0===e?0:-1);e>=0&&(t._resizeHandler=ge((function(){t.refreshItems().layout()}),e),window.addEventListener("resize",t._resizeHandler))}(this,s.layoutOnResize),this.add(function(t,e){if("*"===e)return t.children;if("string"==typeof e){for(var i=[],s=t.children,n=0;n-1?t:this._items.length+t]||null;if(t instanceof _e)return t._gridId===this._id?t:null;if(e){var i=e.get(t);return i&&i._gridId===this._id?i:null}for(var s=0;sa?1:0:al?1:0)return _;return _||(s||(s=xe(i)),_=e?Te(s,r,n):Te(s,n,r)),_}function r(n,r){var o=e?-t(n,r):t(n,r);return o||(s||(s=xe(i)),o=e?Te(s,r,n):Te(s,n,r)),o}return function(o,h){if(this._isDestroyed||this._items.length<2)return this;var a=this._items,l=h||{},_=l.layout?l.layout:void 0===l.layout;if(e=!!l.descending,i=a.slice(0),s=null,C(o))t=o,a.sort(r);else if("string"==typeof o)t=o.trim().split(" ").filter((function(t){return t})).map((function(t){return t.split(":")})),a.sort(n);else{if(!Array.isArray(o))throw t=e=i=s=null,new Error("Invalid comparer argument provided.");a.length=0,a.push.apply(a,o)}return this._hasListeners("sort")&&this._emit("sort",a.slice(0),i),_&&this.layout("instant"===_,C(_)?_:void 0),t=e=i=s=null,this}}(),Ae.prototype.move=function(t,e,i){if(this._isDestroyed||this._items.length<2)return this;var s,n,r=this._items,o=i||{},h=o.layout?o.layout:void 0===o.layout,a="swap"===o.action,l=a?"swap":"move",_=this.getItem(t),d=this.getItem(e);return _&&d&&_!==d&&(s=r.indexOf(_),n=r.indexOf(d),a?pt(r,s,n):ft(r,s,n),this._hasListeners("move")&&this._emit("move",{item:_,fromIndex:s,toIndex:n,action:l}),h&&this.layout("instant"===h,C(h)?h:void 0)),this},Ae.prototype.send=function(t,e,i,s){if(this._isDestroyed||e._isDestroyed||this===e)return this;if(!(t=this.getItem(t)))return this;var n=s||{},r=n.appendTo||document.body,o=n.layoutSender?n.layoutSender:void 0===n.layoutSender,h=n.layoutReceiver?n.layoutReceiver:void 0===n.layoutReceiver;return t._migrate.start(e,i,r),t._migrate._isActive&&t._isActive&&(o&&this.layout("instant"===o,C(o)?o:void 0),h&&e.layout("instant"===h,C(h)?h:void 0)),this},Ae.prototype.destroy=function(e){if(this._isDestroyed)return this;var i,s,n,r=this._element,o=this._items.slice(0),h=this._layout&&this._layout.styles||{};for((n=this)._resizeHandler&&(n._resizeHandler(!0),window.removeEventListener("resize",n._resizeHandler),n._resizeHandler=null),i=0;i0},Ae.prototype._updateBoundingRect=function(){var t=this._element.getBoundingClientRect();this._width=t.width,this._height=t.height,this._left=t.left,this._top=t.top,this._right=t.right,this._bottom=t.bottom},Ae.prototype._updateBorders=function(t,e,i,s){var n=this._element;t&&(this._borderLeft=H(n,"border-left-width")),e&&(this._borderRight=H(n,"border-right-width")),i&&(this._borderTop=H(n,"border-top-width")),s&&(this._borderBottom=H(n,"border-bottom-width"))},Ae.prototype._refreshDimensions=function(){this._updateBoundingRect(),this._updateBorders(1,1,1,1),this._boxSizing=O(this._element,"box-sizing")},Ae.prototype._onLayoutDataReceived=(De=[],function(t){if(!this._isDestroyed&&this._nextLayoutData&&this._nextLayoutData.id===t.id){var e,i,s,n,r=this,o=this._nextLayoutData.instant,h=this._nextLayoutData.onFinish,a=t.items.length,l=a;for(this._nextLayoutData=null,!this._isLayoutFinished&&this._hasListeners("layoutAbort")&&this._emit("layoutAbort",this._layout.items.slice(0)),this._layout=t,De.length=0,n=0;n0)){var e=r._layout.id!==t.id,i=C(o)?o:h;e||(r._isLayoutFinished=!0),C(i)&&i(t.items.slice(0),e),!e&&r._hasListeners("layoutEnd")&&r._emit("layoutEnd",t.items.slice(0))}}}),Ae.prototype._setItemsVisibility=function(t,e,i){var s,n,r=this,o=t.slice(0),h=i||{},a=!0===h.instant,l=h.onFinish,_=h.layout?h.layout:void 0===h.layout,d=o.length,u=e?"showStart":"hideStart",c=e?"showEnd":"hideEnd",f=e?"show":"hide",p=!1,m=[],g=[];if(d){for(n=0;n +* @license MIT +* +* Muuri Ticker / Muuri Emitter / Muuri Dragger +* Copyright (c) 2018-present, Niklas Rämö +* @license MIT +* +* Muuri AutoScroller +* Copyright (c) 2019-present, Niklas Rämö +* @license MIT +*/ + +var GRID_INSTANCES = {}; +var ITEM_ELEMENT_MAP = typeof Map === 'function' ? new Map() : null; + +var ACTION_SWAP = 'swap'; +var ACTION_MOVE = 'move'; + +var EVENT_SYNCHRONIZE = 'synchronize'; +var EVENT_LAYOUT_START = 'layoutStart'; +var EVENT_LAYOUT_END = 'layoutEnd'; +var EVENT_LAYOUT_ABORT = 'layoutAbort'; +var EVENT_ADD = 'add'; +var EVENT_REMOVE = 'remove'; +var EVENT_SHOW_START = 'showStart'; +var EVENT_SHOW_END = 'showEnd'; +var EVENT_HIDE_START = 'hideStart'; +var EVENT_HIDE_END = 'hideEnd'; +var EVENT_FILTER = 'filter'; +var EVENT_SORT = 'sort'; +var EVENT_MOVE = 'move'; +var EVENT_SEND = 'send'; +var EVENT_BEFORE_SEND = 'beforeSend'; +var EVENT_RECEIVE = 'receive'; +var EVENT_BEFORE_RECEIVE = 'beforeReceive'; +var EVENT_DRAG_INIT = 'dragInit'; +var EVENT_DRAG_START = 'dragStart'; +var EVENT_DRAG_MOVE = 'dragMove'; +var EVENT_DRAG_SCROLL = 'dragScroll'; +var EVENT_DRAG_END = 'dragEnd'; +var EVENT_DRAG_RELEASE_START = 'dragReleaseStart'; +var EVENT_DRAG_RELEASE_END = 'dragReleaseEnd'; +var EVENT_DESTROY = 'destroy'; + +var HAS_TOUCH_EVENTS = 'ontouchstart' in window; +var HAS_POINTER_EVENTS = !!window.PointerEvent; +var HAS_MS_POINTER_EVENTS = !!window.navigator.msPointerEnabled; + +var MAX_SAFE_FLOAT32_INTEGER = 16777216; + +/** + * Event emitter constructor. + * + * @class + */ +function Emitter() { + this._events = {}; + this._queue = []; + this._counter = 0; + this._clearOnEmit = false; +} + +/** + * Public prototype methods + * ************************ + */ + +/** + * Bind an event listener. + * + * @public + * @param {String} event + * @param {Function} listener + * @returns {Emitter} + */ +Emitter.prototype.on = function (event, listener) { + if (!this._events || !event || !listener) return this; + + // Get listeners queue and create it if it does not exist. + var listeners = this._events[event]; + if (!listeners) listeners = this._events[event] = []; + + // Add the listener to the queue. + listeners.push(listener); + + return this; +}; + +/** + * Unbind all event listeners that match the provided listener function. + * + * @public + * @param {String} event + * @param {Function} listener + * @returns {Emitter} + */ +Emitter.prototype.off = function (event, listener) { + if (!this._events || !event || !listener) return this; + + // Get listeners and return immediately if none is found. + var listeners = this._events[event]; + if (!listeners || !listeners.length) return this; + + // Remove all matching listeners. + var index; + while ((index = listeners.indexOf(listener)) !== -1) { + listeners.splice(index, 1); + } + + return this; +}; + +/** + * Unbind all listeners of the provided event. + * + * @public + * @param {String} event + * @returns {Emitter} + */ +Emitter.prototype.clear = function (event) { + if (!this._events || !event) return this; + + var listeners = this._events[event]; + if (listeners) { + listeners.length = 0; + delete this._events[event]; + } + + return this; +}; + +/** + * Emit all listeners in a specified event with the provided arguments. + * + * @public + * @param {String} event + * @param {...*} [args] + * @returns {Emitter} + */ +Emitter.prototype.emit = function (event) { + if (!this._events || !event) { + this._clearOnEmit = false; + return this; + } + + // Get event listeners and quit early if there's no listeners. + var listeners = this._events[event]; + if (!listeners || !listeners.length) { + this._clearOnEmit = false; + return this; + } + + var queue = this._queue; + var startIndex = queue.length; + var argsLength = arguments.length - 1; + var args; + + // If we have more than 3 arguments let's put the arguments in an array and + // apply it to the listeners. + if (argsLength > 3) { + args = []; + args.push.apply(args, arguments); + args.shift(); + } + + // Add the current listeners to the callback queue before we process them. + // This is necessary to guarantee that all of the listeners are called in + // correct order even if new event listeners are removed/added during + // processing and/or events are emitted during processing. + queue.push.apply(queue, listeners); + + // Reset the event's listeners if need be. + if (this._clearOnEmit) { + listeners.length = 0; + this._clearOnEmit = false; + } + + // Increment queue counter. This is needed for the scenarios where emit is + // triggered while the queue is already processing. We need to keep track of + // how many "queue processors" there are active so that we can safely reset + // the queue in the end when the last queue processor is finished. + ++this._counter; + + // Process the queue (the specific part of it for this emit). + var i = startIndex; + var endIndex = queue.length; + for (; i < endIndex; i++) { + // prettier-ignore + argsLength === 0 ? queue[i]() : + argsLength === 1 ? queue[i](arguments[1]) : + argsLength === 2 ? queue[i](arguments[1], arguments[2]) : + argsLength === 3 ? queue[i](arguments[1], arguments[2], arguments[3]) : + queue[i].apply(null, args); + + // Stop processing if the emitter is destroyed. + if (!this._events) return this; + } + + // Decrement queue process counter. + --this._counter; + + // Reset the queue if there are no more queue processes running. + if (!this._counter) queue.length = 0; + + return this; +}; + +/** + * Emit all listeners in a specified event with the provided arguments and + * remove the event's listeners just before calling the them. This method allows + * the emitter to serve as a queue where all listeners are called only once. + * + * @public + * @param {String} event + * @param {...*} [args] + * @returns {Emitter} + */ +Emitter.prototype.burst = function () { + if (!this._events) return this; + this._clearOnEmit = true; + this.emit.apply(this, arguments); + return this; +}; + +/** + * Check how many listeners there are for a specific event. + * + * @public + * @param {String} event + * @returns {Boolean} + */ +Emitter.prototype.countListeners = function (event) { + if (!this._events) return 0; + var listeners = this._events[event]; + return listeners ? listeners.length : 0; +}; + +/** + * Destroy emitter instance. Basically just removes all bound listeners. + * + * @public + * @returns {Emitter} + */ +Emitter.prototype.destroy = function () { + if (!this._events) return this; + this._queue.length = this._counter = 0; + this._events = null; + return this; +}; + +var pointerout = HAS_POINTER_EVENTS ? 'pointerout' : HAS_MS_POINTER_EVENTS ? 'MSPointerOut' : ''; +var waitDuration = 100; + +/** + * If you happen to use Edge or IE on a touch capable device there is a + * a specific case where pointercancel and pointerend events are never emitted, + * even though one them should always be emitted when you release your finger + * from the screen. The bug appears specifically when Muuri shifts the dragged + * element's position in the DOM after pointerdown event, IE and Edge don't like + * that behaviour and quite often forget to emit the pointerend/pointercancel + * event. But, they do emit pointerout event so we utilize that here. + * Specifically, if there has been no pointermove event within 100 milliseconds + * since the last pointerout event we force cancel the drag operation. This hack + * works surprisingly well 99% of the time. There is that 1% chance there still + * that dragged items get stuck but it is what it is. + * + * @class + * @param {Dragger} dragger + */ +function EdgeHack(dragger) { + if (!pointerout) return; + + this._dragger = dragger; + this._timeout = null; + this._outEvent = null; + this._isActive = false; + + this._addBehaviour = this._addBehaviour.bind(this); + this._removeBehaviour = this._removeBehaviour.bind(this); + this._onTimeout = this._onTimeout.bind(this); + this._resetData = this._resetData.bind(this); + this._onStart = this._onStart.bind(this); + this._onOut = this._onOut.bind(this); + + this._dragger.on('start', this._onStart); +} + +/** + * @private + */ +EdgeHack.prototype._addBehaviour = function () { + if (this._isActive) return; + this._isActive = true; + this._dragger.on('move', this._resetData); + this._dragger.on('cancel', this._removeBehaviour); + this._dragger.on('end', this._removeBehaviour); + window.addEventListener(pointerout, this._onOut); +}; + +/** + * @private + */ +EdgeHack.prototype._removeBehaviour = function () { + if (!this._isActive) return; + this._dragger.off('move', this._resetData); + this._dragger.off('cancel', this._removeBehaviour); + this._dragger.off('end', this._removeBehaviour); + window.removeEventListener(pointerout, this._onOut); + this._resetData(); + this._isActive = false; +}; + +/** + * @private + */ +EdgeHack.prototype._resetData = function () { + window.clearTimeout(this._timeout); + this._timeout = null; + this._outEvent = null; +}; + +/** + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + */ +EdgeHack.prototype._onStart = function (e) { + if (e.pointerType === 'mouse') return; + this._addBehaviour(); +}; + +/** + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + */ +EdgeHack.prototype._onOut = function (e) { + if (!this._dragger._getTrackedTouch(e)) return; + this._resetData(); + this._outEvent = e; + this._timeout = window.setTimeout(this._onTimeout, waitDuration); +}; + +/** + * @private + */ +EdgeHack.prototype._onTimeout = function () { + var e = this._outEvent; + this._resetData(); + if (this._dragger.isActive()) this._dragger._onCancel(e); +}; + +/** + * @public + */ +EdgeHack.prototype.destroy = function () { + if (!pointerout) return; + this._dragger.off('start', this._onStart); + this._removeBehaviour(); +}; + +// Playing it safe here, test all potential prefixes capitalized and lowercase. +var vendorPrefixes = ['', 'webkit', 'moz', 'ms', 'o', 'Webkit', 'Moz', 'MS', 'O']; +var cache = {}; + +/** + * Get prefixed CSS property name when given a non-prefixed CSS property name. + * Returns null if the property is not supported at all. + * + * @param {CSSStyleDeclaration} style + * @param {String} prop + * @returns {String} + */ +function getPrefixedPropName(style, prop) { + var prefixedProp = cache[prop] || ''; + if (prefixedProp) return prefixedProp; + + var camelProp = prop[0].toUpperCase() + prop.slice(1); + var i = 0; + while (i < vendorPrefixes.length) { + prefixedProp = vendorPrefixes[i] ? vendorPrefixes[i] + camelProp : prop; + if (prefixedProp in style) { + cache[prop] = prefixedProp; + return prefixedProp; + } + ++i; + } + + return ''; +} + +/** + * Check if passive events are supported. + * https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection + * + * @returns {Boolean} + */ +function hasPassiveEvents() { + var isPassiveEventsSupported = false; + + try { + var passiveOpts = Object.defineProperty({}, 'passive', { + get: function () { + isPassiveEventsSupported = true; + }, + }); + window.addEventListener('testPassive', null, passiveOpts); + window.removeEventListener('testPassive', null, passiveOpts); + } catch (e) {} + + return isPassiveEventsSupported; +} + +var ua = window.navigator.userAgent.toLowerCase(); +var isEdge = ua.indexOf('edge') > -1; +var isIE = ua.indexOf('trident') > -1; +var isFirefox = ua.indexOf('firefox') > -1; +var isAndroid = ua.indexOf('android') > -1; + +var listenerOptions = hasPassiveEvents() ? { passive: true } : false; + +var taProp = 'touchAction'; +var taPropPrefixed = getPrefixedPropName(document.documentElement.style, taProp); +var taDefaultValue = 'auto'; + +/** + * Creates a new Dragger instance for an element. + * + * @public + * @class + * @param {HTMLElement} element + * @param {Object} [cssProps] + */ +function Dragger(element, cssProps) { + this._element = element; + this._emitter = new Emitter(); + this._isDestroyed = false; + this._cssProps = {}; + this._touchAction = ''; + this._isActive = false; + + this._pointerId = null; + this._startTime = 0; + this._startX = 0; + this._startY = 0; + this._currentX = 0; + this._currentY = 0; + + this._onStart = this._onStart.bind(this); + this._onMove = this._onMove.bind(this); + this._onCancel = this._onCancel.bind(this); + this._onEnd = this._onEnd.bind(this); + + // Can't believe had to build a freaking class for a hack! + this._edgeHack = null; + if ((isEdge || isIE) && (HAS_POINTER_EVENTS || HAS_MS_POINTER_EVENTS)) { + this._edgeHack = new EdgeHack(this); + } + + // Apply initial CSS props. + this.setCssProps(cssProps); + + // If touch action was not provided with initial CSS props let's assume it's + // auto. + if (!this._touchAction) { + this.setTouchAction(taDefaultValue); + } + + // Prevent native link/image dragging for the item and it's children. + element.addEventListener('dragstart', Dragger._preventDefault, false); + + // Listen to start event. + element.addEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions); +} + +/** + * Protected properties + * ******************** + */ + +Dragger._pointerEvents = { + start: 'pointerdown', + move: 'pointermove', + cancel: 'pointercancel', + end: 'pointerup', +}; + +Dragger._msPointerEvents = { + start: 'MSPointerDown', + move: 'MSPointerMove', + cancel: 'MSPointerCancel', + end: 'MSPointerUp', +}; + +Dragger._touchEvents = { + start: 'touchstart', + move: 'touchmove', + cancel: 'touchcancel', + end: 'touchend', +}; + +Dragger._mouseEvents = { + start: 'mousedown', + move: 'mousemove', + cancel: '', + end: 'mouseup', +}; + +Dragger._inputEvents = (function () { + if (HAS_TOUCH_EVENTS) return Dragger._touchEvents; + if (HAS_POINTER_EVENTS) return Dragger._pointerEvents; + if (HAS_MS_POINTER_EVENTS) return Dragger._msPointerEvents; + return Dragger._mouseEvents; +})(); + +Dragger._emitter = new Emitter(); + +Dragger._emitterEvents = { + start: 'start', + move: 'move', + end: 'end', + cancel: 'cancel', +}; + +Dragger._activeInstances = []; + +/** + * Protected static methods + * ************************ + */ + +Dragger._preventDefault = function (e) { + if (e.preventDefault && e.cancelable !== false) e.preventDefault(); +}; + +Dragger._activateInstance = function (instance) { + var index = Dragger._activeInstances.indexOf(instance); + if (index > -1) return; + + Dragger._activeInstances.push(instance); + Dragger._emitter.on(Dragger._emitterEvents.move, instance._onMove); + Dragger._emitter.on(Dragger._emitterEvents.cancel, instance._onCancel); + Dragger._emitter.on(Dragger._emitterEvents.end, instance._onEnd); + + if (Dragger._activeInstances.length === 1) { + Dragger._bindListeners(); + } +}; + +Dragger._deactivateInstance = function (instance) { + var index = Dragger._activeInstances.indexOf(instance); + if (index === -1) return; + + Dragger._activeInstances.splice(index, 1); + Dragger._emitter.off(Dragger._emitterEvents.move, instance._onMove); + Dragger._emitter.off(Dragger._emitterEvents.cancel, instance._onCancel); + Dragger._emitter.off(Dragger._emitterEvents.end, instance._onEnd); + + if (!Dragger._activeInstances.length) { + Dragger._unbindListeners(); + } +}; + +Dragger._bindListeners = function () { + window.addEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions); + window.addEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions); + if (Dragger._inputEvents.cancel) { + window.addEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions); + } +}; + +Dragger._unbindListeners = function () { + window.removeEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions); + window.removeEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions); + if (Dragger._inputEvents.cancel) { + window.removeEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions); + } +}; + +Dragger._getEventPointerId = function (event) { + // If we have pointer id available let's use it. + if (typeof event.pointerId === 'number') { + return event.pointerId; + } + + // For touch events let's get the first changed touch's identifier. + if (event.changedTouches) { + return event.changedTouches[0] ? event.changedTouches[0].identifier : null; + } + + // For mouse/other events let's provide a static id. + return 1; +}; + +Dragger._getTouchById = function (event, id) { + // If we have a pointer event return the whole event if there's a match, and + // null otherwise. + if (typeof event.pointerId === 'number') { + return event.pointerId === id ? event : null; + } + + // For touch events let's check if there's a changed touch object that matches + // the pointerId in which case return the touch object. + if (event.changedTouches) { + for (var i = 0; i < event.changedTouches.length; i++) { + if (event.changedTouches[i].identifier === id) { + return event.changedTouches[i]; + } + } + return null; + } + + // For mouse/other events let's assume there's only one pointer and just + // return the event. + return event; +}; + +Dragger._onMove = function (e) { + Dragger._emitter.emit(Dragger._emitterEvents.move, e); +}; + +Dragger._onCancel = function (e) { + Dragger._emitter.emit(Dragger._emitterEvents.cancel, e); +}; + +Dragger._onEnd = function (e) { + Dragger._emitter.emit(Dragger._emitterEvents.end, e); +}; + +/** + * Private prototype methods + * ************************* + */ + +/** + * Reset current drag operation (if any). + * + * @private + */ +Dragger.prototype._reset = function () { + this._pointerId = null; + this._startTime = 0; + this._startX = 0; + this._startY = 0; + this._currentX = 0; + this._currentY = 0; + this._isActive = false; + Dragger._deactivateInstance(this); +}; + +/** + * Create a custom dragger event from a raw event. + * + * @private + * @param {String} type + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + * @returns {Object} + */ +Dragger.prototype._createEvent = function (type, e) { + var touch = this._getTrackedTouch(e); + return { + // Hammer.js compatibility interface. + type: type, + srcEvent: e, + distance: this.getDistance(), + deltaX: this.getDeltaX(), + deltaY: this.getDeltaY(), + deltaTime: type === Dragger._emitterEvents.start ? 0 : this.getDeltaTime(), + isFirst: type === Dragger._emitterEvents.start, + isFinal: type === Dragger._emitterEvents.end || type === Dragger._emitterEvents.cancel, + pointerType: e.pointerType || (e.touches ? 'touch' : 'mouse'), + // Partial Touch API interface. + identifier: this._pointerId, + screenX: touch.screenX, + screenY: touch.screenY, + clientX: touch.clientX, + clientY: touch.clientY, + pageX: touch.pageX, + pageY: touch.pageY, + target: touch.target, + }; +}; + +/** + * Emit a raw event as dragger event internally. + * + * @private + * @param {String} type + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + */ +Dragger.prototype._emit = function (type, e) { + this._emitter.emit(type, this._createEvent(type, e)); +}; + +/** + * If the provided event is a PointerEvent this method will return it if it has + * the same pointerId as the instance. If the provided event is a TouchEvent + * this method will try to look for a Touch instance in the changedTouches that + * has an identifier matching this instance's pointerId. If the provided event + * is a MouseEvent (or just any other event than PointerEvent or TouchEvent) + * it will be returned immediately. + * + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + * @returns {?(Touch|PointerEvent|MouseEvent)} + */ +Dragger.prototype._getTrackedTouch = function (e) { + if (this._pointerId === null) return null; + return Dragger._getTouchById(e, this._pointerId); +}; + +/** + * Handler for start event. + * + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + */ +Dragger.prototype._onStart = function (e) { + if (this._isDestroyed) return; + + // If pointer id is already assigned let's return early. + if (this._pointerId !== null) return; + + // Get (and set) pointer id. + this._pointerId = Dragger._getEventPointerId(e); + if (this._pointerId === null) return; + + // Setup initial data and emit start event. + var touch = this._getTrackedTouch(e); + this._startX = this._currentX = touch.clientX; + this._startY = this._currentY = touch.clientY; + this._startTime = Date.now(); + this._isActive = true; + this._emit(Dragger._emitterEvents.start, e); + + // If the drag procedure was not reset within the start procedure let's + // activate the instance (start listening to move/cancel/end events). + if (this._isActive) { + Dragger._activateInstance(this); + } +}; + +/** + * Handler for move event. + * + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + */ +Dragger.prototype._onMove = function (e) { + var touch = this._getTrackedTouch(e); + if (!touch) return; + this._currentX = touch.clientX; + this._currentY = touch.clientY; + this._emit(Dragger._emitterEvents.move, e); +}; + +/** + * Handler for cancel event. + * + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + */ +Dragger.prototype._onCancel = function (e) { + if (!this._getTrackedTouch(e)) return; + this._emit(Dragger._emitterEvents.cancel, e); + this._reset(); +}; + +/** + * Handler for end event. + * + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + */ +Dragger.prototype._onEnd = function (e) { + if (!this._getTrackedTouch(e)) return; + this._emit(Dragger._emitterEvents.end, e); + this._reset(); +}; + +/** + * Public prototype methods + * ************************ + */ + +/** + * Check if the element is being dragged at the moment. + * + * @public + * @returns {Boolean} + */ +Dragger.prototype.isActive = function () { + return this._isActive; +}; + +/** + * Set element's touch-action CSS property. + * + * @public + * @param {String} value + */ +Dragger.prototype.setTouchAction = function (value) { + // Store unmodified touch action value (we trust user input here). + this._touchAction = value; + + // Set touch-action style. + if (taPropPrefixed) { + this._cssProps[taPropPrefixed] = ''; + this._element.style[taPropPrefixed] = value; + } + + // If we have an unsupported touch-action value let's add a special listener + // that prevents default action on touch start event. A dirty hack, but best + // we can do for now. The other options would be to somehow polyfill the + // unsupported touch action behavior with custom heuristics which sounds like + // a can of worms. We do a special exception here for Firefox Android which's + // touch-action does not work properly if the dragged element is moved in the + // the DOM tree on touchstart. + if (HAS_TOUCH_EVENTS) { + this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); + if (this._element.style[taPropPrefixed] !== value || (isFirefox && isAndroid)) { + this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); + } + } +}; + +/** + * Update element's CSS properties. Accepts an object with camel cased style + * props with value pairs as it's first argument. + * + * @public + * @param {Object} [newProps] + */ +Dragger.prototype.setCssProps = function (newProps) { + if (!newProps) return; + + var currentProps = this._cssProps; + var element = this._element; + var prop; + var prefixedProp; + + // Reset current props. + for (prop in currentProps) { + element.style[prop] = currentProps[prop]; + delete currentProps[prop]; + } + + // Set new props. + for (prop in newProps) { + // Make sure we have a value for the prop. + if (!newProps[prop]) continue; + + // Special handling for touch-action. + if (prop === taProp) { + this.setTouchAction(newProps[prop]); + continue; + } + + // Get prefixed prop and skip if it does not exist. + prefixedProp = getPrefixedPropName(element.style, prop); + if (!prefixedProp) continue; + + // Store the prop and add the style. + currentProps[prefixedProp] = ''; + element.style[prefixedProp] = newProps[prop]; + } +}; + +/** + * How much the pointer has moved on x-axis from start position, in pixels. + * Positive value indicates movement from left to right. + * + * @public + * @returns {Number} + */ +Dragger.prototype.getDeltaX = function () { + return this._currentX - this._startX; +}; + +/** + * How much the pointer has moved on y-axis from start position, in pixels. + * Positive value indicates movement from top to bottom. + * + * @public + * @returns {Number} + */ +Dragger.prototype.getDeltaY = function () { + return this._currentY - this._startY; +}; + +/** + * How far (in pixels) has pointer moved from start position. + * + * @public + * @returns {Number} + */ +Dragger.prototype.getDistance = function () { + var x = this.getDeltaX(); + var y = this.getDeltaY(); + return Math.sqrt(x * x + y * y); +}; + +/** + * How long has pointer been dragged. + * + * @public + * @returns {Number} + */ +Dragger.prototype.getDeltaTime = function () { + return this._startTime ? Date.now() - this._startTime : 0; +}; + +/** + * Bind drag event listeners. + * + * @public + * @param {String} eventName + * - 'start', 'move', 'cancel' or 'end'. + * @param {Function} listener + */ +Dragger.prototype.on = function (eventName, listener) { + this._emitter.on(eventName, listener); +}; + +/** + * Unbind drag event listeners. + * + * @public + * @param {String} eventName + * - 'start', 'move', 'cancel' or 'end'. + * @param {Function} listener + */ +Dragger.prototype.off = function (eventName, listener) { + this._emitter.off(eventName, listener); +}; + +/** + * Destroy the instance and unbind all drag event listeners. + * + * @public + */ +Dragger.prototype.destroy = function () { + if (this._isDestroyed) return; + + var element = this._element; + + if (this._edgeHack) this._edgeHack.destroy(); + + // Reset data and deactivate the instance. + this._reset(); + + // Destroy emitter. + this._emitter.destroy(); + + // Unbind event handlers. + element.removeEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions); + element.removeEventListener('dragstart', Dragger._preventDefault, false); + element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); + + // Reset styles. + for (var prop in this._cssProps) { + element.style[prop] = this._cssProps[prop]; + delete this._cssProps[prop]; + } + + // Reset data. + this._element = null; + + // Mark as destroyed. + this._isDestroyed = true; +}; + +var dt = 1000 / 60; + +var raf = ( + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { + return this.setTimeout(function () { + callback(Date.now()); + }, dt); + } +).bind(window); + +/** + * A ticker system for handling DOM reads and writes in an efficient way. + * + * @class + */ +function Ticker(numLanes) { + this._nextStep = null; + this._lanes = []; + this._stepQueue = []; + this._stepCallbacks = {}; + this._step = this._step.bind(this); + for (var i = 0; i < numLanes; i++) { + this._lanes.push(new TickerLane()); + } +} + +Ticker.prototype._step = function (time) { + var lanes = this._lanes; + var stepQueue = this._stepQueue; + var stepCallbacks = this._stepCallbacks; + var i, j, id, laneQueue, laneCallbacks, laneIndices; + + this._nextStep = null; + + for (i = 0; i < lanes.length; i++) { + laneQueue = lanes[i].queue; + laneCallbacks = lanes[i].callbacks; + laneIndices = lanes[i].indices; + for (j = 0; j < laneQueue.length; j++) { + id = laneQueue[j]; + if (!id) continue; + stepQueue.push(id); + stepCallbacks[id] = laneCallbacks[id]; + delete laneCallbacks[id]; + delete laneIndices[id]; + } + laneQueue.length = 0; + } + + for (i = 0; i < stepQueue.length; i++) { + id = stepQueue[i]; + if (stepCallbacks[id]) stepCallbacks[id](time); + delete stepCallbacks[id]; + } + + stepQueue.length = 0; +}; + +Ticker.prototype.add = function (laneIndex, id, callback) { + this._lanes[laneIndex].add(id, callback); + if (!this._nextStep) this._nextStep = raf(this._step); +}; + +Ticker.prototype.remove = function (laneIndex, id) { + this._lanes[laneIndex].remove(id); +}; + +/** + * A lane for ticker. + * + * @class + */ +function TickerLane() { + this.queue = []; + this.indices = {}; + this.callbacks = {}; +} + +TickerLane.prototype.add = function (id, callback) { + var index = this.indices[id]; + if (index !== undefined) this.queue[index] = undefined; + this.queue.push(id); + this.callbacks[id] = callback; + this.indices[id] = this.queue.length - 1; +}; + +TickerLane.prototype.remove = function (id) { + var index = this.indices[id]; + if (index === undefined) return; + this.queue[index] = undefined; + delete this.callbacks[id]; + delete this.indices[id]; +}; + +var LAYOUT_READ = 'layoutRead'; +var LAYOUT_WRITE = 'layoutWrite'; +var VISIBILITY_READ = 'visibilityRead'; +var VISIBILITY_WRITE = 'visibilityWrite'; +var DRAG_START_READ = 'dragStartRead'; +var DRAG_START_WRITE = 'dragStartWrite'; +var DRAG_MOVE_READ = 'dragMoveRead'; +var DRAG_MOVE_WRITE = 'dragMoveWrite'; +var DRAG_SCROLL_READ = 'dragScrollRead'; +var DRAG_SCROLL_WRITE = 'dragScrollWrite'; +var DRAG_SORT_READ = 'dragSortRead'; +var PLACEHOLDER_LAYOUT_READ = 'placeholderLayoutRead'; +var PLACEHOLDER_LAYOUT_WRITE = 'placeholderLayoutWrite'; +var PLACEHOLDER_RESIZE_WRITE = 'placeholderResizeWrite'; +var AUTO_SCROLL_READ = 'autoScrollRead'; +var AUTO_SCROLL_WRITE = 'autoScrollWrite'; +var DEBOUNCE_READ = 'debounceRead'; + +var LANE_READ = 0; +var LANE_READ_TAIL = 1; +var LANE_WRITE = 2; + +var ticker = new Ticker(3); + +function addLayoutTick(itemId, read, write) { + ticker.add(LANE_READ, LAYOUT_READ + itemId, read); + ticker.add(LANE_WRITE, LAYOUT_WRITE + itemId, write); +} + +function cancelLayoutTick(itemId) { + ticker.remove(LANE_READ, LAYOUT_READ + itemId); + ticker.remove(LANE_WRITE, LAYOUT_WRITE + itemId); +} + +function addVisibilityTick(itemId, read, write) { + ticker.add(LANE_READ, VISIBILITY_READ + itemId, read); + ticker.add(LANE_WRITE, VISIBILITY_WRITE + itemId, write); +} + +function cancelVisibilityTick(itemId) { + ticker.remove(LANE_READ, VISIBILITY_READ + itemId); + ticker.remove(LANE_WRITE, VISIBILITY_WRITE + itemId); +} + +function addDragStartTick(itemId, read, write) { + ticker.add(LANE_READ, DRAG_START_READ + itemId, read); + ticker.add(LANE_WRITE, DRAG_START_WRITE + itemId, write); +} + +function cancelDragStartTick(itemId) { + ticker.remove(LANE_READ, DRAG_START_READ + itemId); + ticker.remove(LANE_WRITE, DRAG_START_WRITE + itemId); +} + +function addDragMoveTick(itemId, read, write) { + ticker.add(LANE_READ, DRAG_MOVE_READ + itemId, read); + ticker.add(LANE_WRITE, DRAG_MOVE_WRITE + itemId, write); +} + +function cancelDragMoveTick(itemId) { + ticker.remove(LANE_READ, DRAG_MOVE_READ + itemId); + ticker.remove(LANE_WRITE, DRAG_MOVE_WRITE + itemId); +} + +function addDragScrollTick(itemId, read, write) { + ticker.add(LANE_READ, DRAG_SCROLL_READ + itemId, read); + ticker.add(LANE_WRITE, DRAG_SCROLL_WRITE + itemId, write); +} + +function cancelDragScrollTick(itemId) { + ticker.remove(LANE_READ, DRAG_SCROLL_READ + itemId); + ticker.remove(LANE_WRITE, DRAG_SCROLL_WRITE + itemId); +} + +function addDragSortTick(itemId, read) { + ticker.add(LANE_READ_TAIL, DRAG_SORT_READ + itemId, read); +} + +function cancelDragSortTick(itemId) { + ticker.remove(LANE_READ_TAIL, DRAG_SORT_READ + itemId); +} + +function addPlaceholderLayoutTick(itemId, read, write) { + ticker.add(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId, read); + ticker.add(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId, write); +} + +function cancelPlaceholderLayoutTick(itemId) { + ticker.remove(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId); + ticker.remove(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId); +} + +function addPlaceholderResizeTick(itemId, write) { + ticker.add(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId, write); +} + +function cancelPlaceholderResizeTick(itemId) { + ticker.remove(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId); +} + +function addAutoScrollTick(read, write) { + ticker.add(LANE_READ, AUTO_SCROLL_READ, read); + ticker.add(LANE_WRITE, AUTO_SCROLL_WRITE, write); +} + +function cancelAutoScrollTick() { + ticker.remove(LANE_READ, AUTO_SCROLL_READ); + ticker.remove(LANE_WRITE, AUTO_SCROLL_WRITE); +} + +function addDebounceTick(debounceId, read) { + ticker.add(LANE_READ, DEBOUNCE_READ + debounceId, read); +} + +function cancelDebounceTick(debounceId) { + ticker.remove(LANE_READ, DEBOUNCE_READ + debounceId); +} + +var AXIS_X = 1; +var AXIS_Y = 2; +var FORWARD = 4; +var BACKWARD = 8; +var LEFT = AXIS_X | BACKWARD; +var RIGHT = AXIS_X | FORWARD; +var UP = AXIS_Y | BACKWARD; +var DOWN = AXIS_Y | FORWARD; + +var functionType = 'function'; + +/** + * Check if a value is a function. + * + * @param {*} val + * @returns {Boolean} + */ +function isFunction(val) { + return typeof val === functionType; +} + +var isWeakMapSupported = typeof WeakMap === 'function'; +var cache$1 = isWeakMapSupported ? new WeakMap() : null; +var cacheInterval = 3000; +var cacheTimer; +var canClearCache = true; +var clearCache = function () { + if (canClearCache) { + cacheTimer = window.clearInterval(cacheTimer); + cache$1 = isWeakMapSupported ? new WeakMap() : null; + } else { + canClearCache = true; + } +}; + +/** + * Returns the computed value of an element's style property as a string. + * + * @param {HTMLElement} element + * @param {String} style + * @returns {String} + */ +function getStyle(element, style) { + var styles = cache$1 && cache$1.get(element); + + if (!styles) { + styles = window.getComputedStyle(element, null); + if (cache$1) cache$1.set(element, styles); + } + + if (cache$1) { + if (!cacheTimer) { + cacheTimer = window.setInterval(clearCache, cacheInterval); + } else { + canClearCache = false; + } + } + + return styles.getPropertyValue(style); +} + +/** + * Returns the computed value of an element's style property transformed into + * a float value. + * + * @param {HTMLElement} el + * @param {String} style + * @returns {Number} + */ +function getStyleAsFloat(el, style) { + return parseFloat(getStyle(el, style)) || 0; +} + +var DOC_ELEM = document.documentElement; +var BODY = document.body; +var THRESHOLD_DATA = { value: 0, offset: 0 }; + +/** + * @param {HTMLElement|Window} element + * @returns {HTMLElement|Window} + */ +function getScrollElement(element) { + if (element === window || element === DOC_ELEM || element === BODY) { + return window; + } else { + return element; + } +} + +/** + * @param {HTMLElement|Window} element + * @returns {Number} + */ +function getScrollLeft(element) { + return element === window ? element.pageXOffset : element.scrollLeft; +} + +/** + * @param {HTMLElement|Window} element + * @returns {Number} + */ +function getScrollTop(element) { + return element === window ? element.pageYOffset : element.scrollTop; +} + +/** + * @param {HTMLElement|Window} element + * @returns {Number} + */ +function getScrollLeftMax(element) { + if (element === window) { + return DOC_ELEM.scrollWidth - DOC_ELEM.clientWidth; + } else { + return element.scrollWidth - element.clientWidth; + } +} + +/** + * @param {HTMLElement|Window} element + * @returns {Number} + */ +function getScrollTopMax(element) { + if (element === window) { + return DOC_ELEM.scrollHeight - DOC_ELEM.clientHeight; + } else { + return element.scrollHeight - element.clientHeight; + } +} + +/** + * Get window's or element's client rectangle data relative to the element's + * content dimensions (includes inner size + padding, excludes scrollbars, + * borders and margins). + * + * @param {HTMLElement|Window} element + * @returns {Rectangle} + */ +function getContentRect(element, result) { + result = result || {}; + + if (element === window) { + result.width = DOC_ELEM.clientWidth; + result.height = DOC_ELEM.clientHeight; + result.left = 0; + result.right = result.width; + result.top = 0; + result.bottom = result.height; + } else { + var bcr = element.getBoundingClientRect(); + var borderLeft = element.clientLeft || getStyleAsFloat(element, 'border-left-width'); + var borderTop = element.clientTop || getStyleAsFloat(element, 'border-top-width'); + result.width = element.clientWidth; + result.height = element.clientHeight; + result.left = bcr.left + borderLeft; + result.right = result.left + result.width; + result.top = bcr.top + borderTop; + result.bottom = result.top + result.height; + } + + return result; +} + +/** + * @param {Item} item + * @returns {Object} + */ +function getItemAutoScrollSettings(item) { + return item._drag._getGrid()._settings.dragAutoScroll; +} + +/** + * @param {Item} item + */ +function prepareItemScrollSync(item) { + if (!item._drag) return; + item._drag._prepareScroll(); +} + +/** + * @param {Item} item + */ +function applyItemScrollSync(item) { + if (!item._drag || !item._isActive) return; + var drag = item._drag; + drag._scrollDiffX = drag._scrollDiffY = 0; + item._setTranslate(drag._left, drag._top); +} + +/** + * Compute threshold value and edge offset. + * + * @param {Number} threshold + * @param {Number} safeZone + * @param {Number} itemSize + * @param {Number} targetSize + * @returns {Object} + */ +function computeThreshold(threshold, safeZone, itemSize, targetSize) { + THRESHOLD_DATA.value = Math.min(targetSize / 2, threshold); + THRESHOLD_DATA.offset = + Math.max(0, itemSize + THRESHOLD_DATA.value * 2 + targetSize * safeZone - targetSize) / 2; + return THRESHOLD_DATA; +} + +function ScrollRequest() { + this.reset(); +} + +ScrollRequest.prototype.reset = function () { + if (this.isActive) this.onStop(); + this.item = null; + this.element = null; + this.isActive = false; + this.isEnding = false; + this.direction = null; + this.value = null; + this.maxValue = 0; + this.threshold = 0; + this.distance = 0; + this.speed = 0; + this.duration = 0; + this.action = null; +}; + +ScrollRequest.prototype.hasReachedEnd = function () { + return FORWARD & this.direction ? this.value >= this.maxValue : this.value <= 0; +}; + +ScrollRequest.prototype.computeCurrentScrollValue = function () { + if (this.value === null) { + return AXIS_X & this.direction ? getScrollLeft(this.element) : getScrollTop(this.element); + } + return Math.max(0, Math.min(this.value, this.maxValue)); +}; + +ScrollRequest.prototype.computeNextScrollValue = function (deltaTime) { + var delta = this.speed * (deltaTime / 1000); + var nextValue = FORWARD & this.direction ? this.value + delta : this.value - delta; + return Math.max(0, Math.min(nextValue, this.maxValue)); +}; + +ScrollRequest.prototype.computeSpeed = (function () { + var data = { + direction: null, + threshold: 0, + distance: 0, + value: 0, + maxValue: 0, + deltaTime: 0, + duration: 0, + isEnding: false, + }; + + return function (deltaTime) { + var item = this.item; + var speed = getItemAutoScrollSettings(item).speed; + + if (isFunction(speed)) { + data.direction = this.direction; + data.threshold = this.threshold; + data.distance = this.distance; + data.value = this.value; + data.maxValue = this.maxValue; + data.duration = this.duration; + data.speed = this.speed; + data.deltaTime = deltaTime; + data.isEnding = this.isEnding; + return speed(item, this.element, data); + } else { + return speed; + } + }; +})(); + +ScrollRequest.prototype.tick = function (deltaTime) { + if (!this.isActive) { + this.isActive = true; + this.onStart(); + } + this.value = this.computeCurrentScrollValue(); + this.speed = this.computeSpeed(deltaTime); + this.value = this.computeNextScrollValue(deltaTime); + this.duration += deltaTime; + return this.value; +}; + +ScrollRequest.prototype.onStart = function () { + var item = this.item; + var onStart = getItemAutoScrollSettings(item).onStart; + if (isFunction(onStart)) onStart(item, this.element, this.direction); +}; + +ScrollRequest.prototype.onStop = function () { + var item = this.item; + var onStop = getItemAutoScrollSettings(item).onStop; + if (isFunction(onStop)) onStop(item, this.element, this.direction); + // Manually nudge sort to happen. There's a good chance that the item is still + // after the scroll stops which means that the next sort will be triggered + // only after the item is moved or it's parent scrolled. + if (item._drag) item._drag.sort(); +}; + +function ScrollAction() { + this.element = null; + this.requestX = null; + this.requestY = null; + this.scrollLeft = 0; + this.scrollTop = 0; +} + +ScrollAction.prototype.reset = function () { + if (this.requestX) this.requestX.action = null; + if (this.requestY) this.requestY.action = null; + this.element = null; + this.requestX = null; + this.requestY = null; + this.scrollLeft = 0; + this.scrollTop = 0; +}; + +ScrollAction.prototype.addRequest = function (request) { + if (AXIS_X & request.direction) { + this.removeRequest(this.requestX); + this.requestX = request; + } else { + this.removeRequest(this.requestY); + this.requestY = request; + } + request.action = this; +}; + +ScrollAction.prototype.removeRequest = function (request) { + if (!request) return; + if (this.requestX === request) { + this.requestX = null; + request.action = null; + } else if (this.requestY === request) { + this.requestY = null; + request.action = null; + } +}; + +ScrollAction.prototype.computeScrollValues = function () { + this.scrollLeft = this.requestX ? this.requestX.value : getScrollLeft(this.element); + this.scrollTop = this.requestY ? this.requestY.value : getScrollTop(this.element); +}; + +ScrollAction.prototype.scroll = function () { + var element = this.element; + if (!element) return; + + if (element.scrollTo) { + element.scrollTo(this.scrollLeft, this.scrollTop); + } else { + element.scrollLeft = this.scrollLeft; + element.scrollTop = this.scrollTop; + } +}; + +function Pool(createItem, releaseItem) { + this.pool = []; + this.createItem = createItem; + this.releaseItem = releaseItem; +} + +Pool.prototype.pick = function () { + return this.pool.pop() || this.createItem(); +}; + +Pool.prototype.release = function (item) { + this.releaseItem(item); + if (this.pool.indexOf(item) !== -1) return; + this.pool.push(item); +}; + +Pool.prototype.reset = function () { + this.pool.length = 0; +}; + +/** + * Check if two rectangles are overlapping. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ +function isOverlapping(a, b) { + return !( + a.left + a.width <= b.left || + b.left + b.width <= a.left || + a.top + a.height <= b.top || + b.top + b.height <= a.top + ); +} + +/** + * Calculate intersection area between two rectangle. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ +function getIntersectionArea(a, b) { + if (!isOverlapping(a, b)) return 0; + var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left); + var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top); + return width * height; +} + +/** + * Calculate how many percent the intersection area of two rectangles is from + * the maximum potential intersection area between the rectangles. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ +function getIntersectionScore(a, b) { + var area = getIntersectionArea(a, b); + if (!area) return 0; + var maxArea = Math.min(a.width, b.width) * Math.min(a.height, b.height); + return (area / maxArea) * 100; +} + +var RECT_1 = { + width: 0, + height: 0, + left: 0, + right: 0, + top: 0, + bottom: 0, +}; + +var RECT_2 = { + width: 0, + height: 0, + left: 0, + right: 0, + top: 0, + bottom: 0, +}; + +function AutoScroller() { + this._isDestroyed = false; + this._isTicking = false; + this._tickTime = 0; + this._tickDeltaTime = 0; + this._items = []; + this._actions = []; + this._requests = {}; + this._requests[AXIS_X] = {}; + this._requests[AXIS_Y] = {}; + this._requestOverlapCheck = {}; + this._dragPositions = {}; + this._dragDirections = {}; + this._overlapCheckInterval = 150; + + this._requestPool = new Pool( + function () { + return new ScrollRequest(); + }, + function (request) { + request.reset(); + } + ); + + this._actionPool = new Pool( + function () { + return new ScrollAction(); + }, + function (action) { + action.reset(); + } + ); + + this._readTick = this._readTick.bind(this); + this._writeTick = this._writeTick.bind(this); +} + +AutoScroller.AXIS_X = AXIS_X; +AutoScroller.AXIS_Y = AXIS_Y; +AutoScroller.FORWARD = FORWARD; +AutoScroller.BACKWARD = BACKWARD; +AutoScroller.LEFT = LEFT; +AutoScroller.RIGHT = RIGHT; +AutoScroller.UP = UP; +AutoScroller.DOWN = DOWN; + +AutoScroller.smoothSpeed = function (maxSpeed, acceleration, deceleration) { + return function (item, element, data) { + var targetSpeed = 0; + if (!data.isEnding) { + if (data.threshold > 0) { + var factor = data.threshold - Math.max(0, data.distance); + targetSpeed = (maxSpeed / data.threshold) * factor; + } else { + targetSpeed = maxSpeed; + } + } + + var currentSpeed = data.speed; + var nextSpeed = targetSpeed; + + if (currentSpeed === targetSpeed) { + return nextSpeed; + } + + if (currentSpeed < targetSpeed) { + nextSpeed = currentSpeed + acceleration * (data.deltaTime / 1000); + return Math.min(targetSpeed, nextSpeed); + } else { + nextSpeed = currentSpeed - deceleration * (data.deltaTime / 1000); + return Math.max(targetSpeed, nextSpeed); + } + }; +}; + +AutoScroller.pointerHandle = function (pointerSize) { + var rect = { left: 0, top: 0, width: 0, height: 0 }; + var size = pointerSize || 1; + return function (item, x, y, w, h, pX, pY) { + rect.left = pX - size * 0.5; + rect.top = pY - size * 0.5; + rect.width = size; + rect.height = size; + return rect; + }; +}; + +AutoScroller.prototype._readTick = function (time) { + if (this._isDestroyed) return; + if (time && this._tickTime) { + this._tickDeltaTime = time - this._tickTime; + this._tickTime = time; + this._updateRequests(); + this._updateActions(); + } else { + this._tickTime = time; + this._tickDeltaTime = 0; + } +}; + +AutoScroller.prototype._writeTick = function () { + if (this._isDestroyed) return; + this._applyActions(); + addAutoScrollTick(this._readTick, this._writeTick); +}; + +AutoScroller.prototype._startTicking = function () { + this._isTicking = true; + addAutoScrollTick(this._readTick, this._writeTick); +}; + +AutoScroller.prototype._stopTicking = function () { + this._isTicking = false; + this._tickTime = 0; + this._tickDeltaTime = 0; + cancelAutoScrollTick(); +}; + +AutoScroller.prototype._getItemHandleRect = function (item, handle, rect) { + var itemDrag = item._drag; + + if (handle) { + var ev = itemDrag._dragMoveEvent || itemDrag._dragStartEvent; + var data = handle( + item, + itemDrag._clientX, + itemDrag._clientY, + item._width, + item._height, + ev.clientX, + ev.clientY + ); + rect.left = data.left; + rect.top = data.top; + rect.width = data.width; + rect.height = data.height; + } else { + rect.left = itemDrag._clientX; + rect.top = itemDrag._clientY; + rect.width = item._width; + rect.height = item._height; + } + + rect.right = rect.left + rect.width; + rect.bottom = rect.top + rect.height; + + return rect; +}; + +AutoScroller.prototype._requestItemScroll = function ( + item, + axis, + element, + direction, + threshold, + distance, + maxValue +) { + var reqMap = this._requests[axis]; + var request = reqMap[item._id]; + + if (request) { + if (request.element !== element || request.direction !== direction) { + request.reset(); + } + } else { + request = this._requestPool.pick(); + } + + request.item = item; + request.element = element; + request.direction = direction; + request.threshold = threshold; + request.distance = distance; + request.maxValue = maxValue; + reqMap[item._id] = request; +}; + +AutoScroller.prototype._cancelItemScroll = function (item, axis) { + var reqMap = this._requests[axis]; + var request = reqMap[item._id]; + if (!request) return; + if (request.action) request.action.removeRequest(request); + this._requestPool.release(request); + delete reqMap[item._id]; +}; + +AutoScroller.prototype._checkItemOverlap = function (item, checkX, checkY) { + var settings = getItemAutoScrollSettings(item); + var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets; + var threshold = settings.threshold; + var safeZone = settings.safeZone; + + if (!targets || !targets.length) { + checkX && this._cancelItemScroll(item, AXIS_X); + checkY && this._cancelItemScroll(item, AXIS_Y); + return; + } + + var dragDirections = this._dragDirections[item._id]; + var dragDirectionX = dragDirections[0]; + var dragDirectionY = dragDirections[1]; + + if (!dragDirectionX && !dragDirectionY) { + checkX && this._cancelItemScroll(item, AXIS_X); + checkY && this._cancelItemScroll(item, AXIS_Y); + return; + } + + var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1); + var testRect = RECT_2; + + var target = null; + var testElement = null; + var testAxisX = true; + var testAxisY = true; + var testScore = 0; + var testPriority = 0; + var testThreshold = null; + var testDirection = null; + var testDistance = 0; + var testMaxScrollX = 0; + var testMaxScrollY = 0; + + var xElement = null; + var xPriority = -Infinity; + var xThreshold = 0; + var xScore = 0; + var xDirection = null; + var xDistance = 0; + var xMaxScroll = 0; + + var yElement = null; + var yPriority = -Infinity; + var yThreshold = 0; + var yScore = 0; + var yDirection = null; + var yDistance = 0; + var yMaxScroll = 0; + + for (var i = 0; i < targets.length; i++) { + target = targets[i]; + testAxisX = checkX && dragDirectionX && target.axis !== AXIS_Y; + testAxisY = checkY && dragDirectionY && target.axis !== AXIS_X; + testPriority = target.priority || 0; + + // Ignore this item if it's x-axis and y-axis priority is lower than + // the currently matching item's. + if ((!testAxisX || testPriority < xPriority) && (!testAxisY || testPriority < yPriority)) { + continue; + } + + testElement = getScrollElement(target.element || target); + testMaxScrollX = testAxisX ? getScrollLeftMax(testElement) : -1; + testMaxScrollY = testAxisY ? getScrollTopMax(testElement) : -1; + + // Ignore this item if there is no possibility to scroll. + if (!testMaxScrollX && !testMaxScrollY) continue; + + testRect = getContentRect(testElement, testRect); + testScore = getIntersectionScore(itemRect, testRect); + + // Ignore this item if it's not overlapping at all with the dragged item. + if (testScore <= 0) continue; + + // Test x-axis. + if ( + testAxisX && + testPriority >= xPriority && + testMaxScrollX > 0 && + (testPriority > xPriority || testScore > xScore) + ) { + testDirection = null; + testThreshold = computeThreshold( + typeof target.threshold === 'number' ? target.threshold : threshold, + safeZone, + itemRect.width, + testRect.width + ); + if (dragDirectionX === RIGHT) { + testDistance = testRect.right + testThreshold.offset - itemRect.right; + if (testDistance <= testThreshold.value && getScrollLeft(testElement) < testMaxScrollX) { + testDirection = RIGHT; + } + } else if (dragDirectionX === LEFT) { + testDistance = itemRect.left - (testRect.left - testThreshold.offset); + if (testDistance <= testThreshold.value && getScrollLeft(testElement) > 0) { + testDirection = LEFT; + } + } + + if (testDirection !== null) { + xElement = testElement; + xPriority = testPriority; + xThreshold = testThreshold.value; + xScore = testScore; + xDirection = testDirection; + xDistance = testDistance; + xMaxScroll = testMaxScrollX; + } + } + + // Test y-axis. + if ( + testAxisY && + testPriority >= yPriority && + testMaxScrollY > 0 && + (testPriority > yPriority || testScore > yScore) + ) { + testDirection = null; + testThreshold = computeThreshold( + typeof target.threshold === 'number' ? target.threshold : threshold, + safeZone, + itemRect.height, + testRect.height + ); + if (dragDirectionY === DOWN) { + testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom; + if (testDistance <= testThreshold.value && getScrollTop(testElement) < testMaxScrollY) { + testDirection = DOWN; + } + } else if (dragDirectionY === UP) { + testDistance = itemRect.top - (testRect.top - testThreshold.offset); + if (testDistance <= testThreshold.value && getScrollTop(testElement) > 0) { + testDirection = UP; + } + } + + if (testDirection !== null) { + yElement = testElement; + yPriority = testPriority; + yThreshold = testThreshold.value; + yScore = testScore; + yDirection = testDirection; + yDistance = testDistance; + yMaxScroll = testMaxScrollY; + } + } + } + + // Request or cancel x-axis scroll. + if (checkX) { + if (xElement) { + this._requestItemScroll( + item, + AXIS_X, + xElement, + xDirection, + xThreshold, + xDistance, + xMaxScroll + ); + } else { + this._cancelItemScroll(item, AXIS_X); + } + } + + // Request or cancel y-axis scroll. + if (checkY) { + if (yElement) { + this._requestItemScroll( + item, + AXIS_Y, + yElement, + yDirection, + yThreshold, + yDistance, + yMaxScroll + ); + } else { + this._cancelItemScroll(item, AXIS_Y); + } + } +}; + +AutoScroller.prototype._updateScrollRequest = function (scrollRequest) { + var item = scrollRequest.item; + var settings = getItemAutoScrollSettings(item); + var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets; + var targetCount = (targets && targets.length) || 0; + var threshold = settings.threshold; + var safeZone = settings.safeZone; + var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1); + var testRect = RECT_2; + var target = null; + var testElement = null; + var testIsAxisX = false; + var testScore = null; + var testThreshold = null; + var testDistance = null; + var testScroll = null; + var testMaxScroll = null; + var hasReachedEnd = null; + + for (var i = 0; i < targetCount; i++) { + target = targets[i]; + + // Make sure we have a matching element. + testElement = getScrollElement(target.element || target); + if (testElement !== scrollRequest.element) continue; + + // Make sure we have a matching axis. + testIsAxisX = !!(AXIS_X & scrollRequest.direction); + if (testIsAxisX) { + if (target.axis === AXIS_Y) continue; + } else { + if (target.axis === AXIS_X) continue; + } + + // Stop scrolling if there is no room to scroll anymore. + testMaxScroll = testIsAxisX ? getScrollLeftMax(testElement) : getScrollTopMax(testElement); + if (testMaxScroll <= 0) { + break; + } + + testRect = getContentRect(testElement, testRect); + testScore = getIntersectionScore(itemRect, testRect); + + // Stop scrolling if dragged item is not overlapping with the scroll + // element anymore. + if (testScore <= 0) { + break; + } + + // Compute threshold and edge offset. + testThreshold = computeThreshold( + typeof target.threshold === 'number' ? target.threshold : threshold, + safeZone, + testIsAxisX ? itemRect.width : itemRect.height, + testIsAxisX ? testRect.width : testRect.height + ); + + // Compute distance (based on current direction). + if (scrollRequest.direction === LEFT) { + testDistance = itemRect.left - (testRect.left - testThreshold.offset); + } else if (scrollRequest.direction === RIGHT) { + testDistance = testRect.right + testThreshold.offset - itemRect.right; + } else if (scrollRequest.direction === UP) { + testDistance = itemRect.top - (testRect.top - testThreshold.offset); + } else { + testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom; + } + + // Stop scrolling if threshold is not exceeded. + if (testDistance > testThreshold.value) { + break; + } + + // Stop scrolling if we have reached the end of the scroll value. + testScroll = testIsAxisX ? getScrollLeft(testElement) : getScrollTop(testElement); + hasReachedEnd = + FORWARD & scrollRequest.direction ? testScroll >= testMaxScroll : testScroll <= 0; + if (hasReachedEnd) { + break; + } + + // Scrolling can continue, let's update the values. + scrollRequest.maxValue = testMaxScroll; + scrollRequest.threshold = testThreshold.value; + scrollRequest.distance = testDistance; + scrollRequest.isEnding = false; + return true; + } + + // Before we end the request, let's see if we need to stop the scrolling + // smoothly or immediately. + if (settings.smoothStop === true && scrollRequest.speed > 0) { + if (hasReachedEnd === null) hasReachedEnd = scrollRequest.hasReachedEnd(); + scrollRequest.isEnding = hasReachedEnd ? false : true; + } else { + scrollRequest.isEnding = false; + } + + return scrollRequest.isEnding; +}; + +AutoScroller.prototype._updateRequests = function () { + var items = this._items; + var requestsX = this._requests[AXIS_X]; + var requestsY = this._requests[AXIS_Y]; + var item, reqX, reqY, checkTime, needsCheck, checkX, checkY; + + for (var i = 0; i < items.length; i++) { + item = items[i]; + checkTime = this._requestOverlapCheck[item._id]; + needsCheck = checkTime > 0 && this._tickTime - checkTime > this._overlapCheckInterval; + + checkX = true; + reqX = requestsX[item._id]; + if (reqX && reqX.isActive) { + checkX = !this._updateScrollRequest(reqX); + if (checkX) { + needsCheck = true; + this._cancelItemScroll(item, AXIS_X); + } + } + + checkY = true; + reqY = requestsY[item._id]; + if (reqY && reqY.isActive) { + checkY = !this._updateScrollRequest(reqY); + if (checkY) { + needsCheck = true; + this._cancelItemScroll(item, AXIS_Y); + } + } + + if (needsCheck) { + this._requestOverlapCheck[item._id] = 0; + this._checkItemOverlap(item, checkX, checkY); + } + } +}; + +AutoScroller.prototype._requestAction = function (request, axis) { + var actions = this._actions; + var isAxisX = axis === AXIS_X; + var action = null; + + for (var i = 0; i < actions.length; i++) { + action = actions[i]; + + // If the action's request does not match the request's -> skip. + if (request.element !== action.element) { + action = null; + continue; + } + + // If the request and action share the same element, but the request slot + // for the requested axis is already reserved let's ignore and cancel this + // request. + if (isAxisX ? action.requestX : action.requestY) { + this._cancelItemScroll(request.item, axis); + return; + } + + // Seems like we have found our action, let's break the loop. + break; + } + + if (!action) action = this._actionPool.pick(); + action.element = request.element; + action.addRequest(request); + + request.tick(this._tickDeltaTime); + actions.push(action); +}; + +AutoScroller.prototype._updateActions = function () { + var items = this._items; + var requests = this._requests; + var actions = this._actions; + var itemId; + var reqX; + var reqY; + var i; + + // Generate actions. + for (i = 0; i < items.length; i++) { + itemId = items[i]._id; + reqX = requests[AXIS_X][itemId]; + reqY = requests[AXIS_Y][itemId]; + if (reqX) this._requestAction(reqX, AXIS_X); + if (reqY) this._requestAction(reqY, AXIS_Y); + } + + // Compute actions' scroll values. + for (i = 0; i < actions.length; i++) { + actions[i].computeScrollValues(); + } +}; + +AutoScroller.prototype._applyActions = function () { + var actions = this._actions; + var items = this._items; + var i; + + // No actions -> no scrolling. + if (!actions.length) return; + + // Scroll all the required elements. + for (i = 0; i < actions.length; i++) { + actions[i].scroll(); + this._actionPool.release(actions[i]); + } + + // Reset actions. + actions.length = 0; + + // Sync the item position immediately after all the auto-scrolling business is + // finished. Without this procedure the items will jitter during auto-scroll + // (in some cases at least) since the drag scroll handler is async (bound to + // raf tick). Note that this procedure should not emit any dragScroll events, + // because otherwise they would be emitted twice for the same event. + for (i = 0; i < items.length; i++) prepareItemScrollSync(items[i]); + for (i = 0; i < items.length; i++) applyItemScrollSync(items[i]); +}; + +AutoScroller.prototype._updateDragDirection = function (item) { + var dragPositions = this._dragPositions[item._id]; + var dragDirections = this._dragDirections[item._id]; + var x1 = item._drag._left; + var y1 = item._drag._top; + if (dragPositions.length) { + var x2 = dragPositions[0]; + var y2 = dragPositions[1]; + dragDirections[0] = x1 > x2 ? RIGHT : x1 < x2 ? LEFT : dragDirections[0] || 0; + dragDirections[1] = y1 > y2 ? DOWN : y1 < y2 ? UP : dragDirections[1] || 0; + } + dragPositions[0] = x1; + dragPositions[1] = y1; +}; + +AutoScroller.prototype.addItem = function (item) { + if (this._isDestroyed) return; + var index = this._items.indexOf(item); + if (index === -1) { + this._items.push(item); + this._requestOverlapCheck[item._id] = this._tickTime; + this._dragDirections[item._id] = [0, 0]; + this._dragPositions[item._id] = []; + if (!this._isTicking) this._startTicking(); + } +}; + +AutoScroller.prototype.updateItem = function (item) { + if (this._isDestroyed) return; + this._updateDragDirection(item); + if (!this._requestOverlapCheck[item._id]) { + this._requestOverlapCheck[item._id] = this._tickTime; + } +}; + +AutoScroller.prototype.removeItem = function (item) { + if (this._isDestroyed) return; + + var index = this._items.indexOf(item); + if (index === -1) return; + + var itemId = item._id; + + var reqX = this._requests[AXIS_X][itemId]; + if (reqX) { + this._cancelItemScroll(item, AXIS_X); + delete this._requests[AXIS_X][itemId]; + } + + var reqY = this._requests[AXIS_Y][itemId]; + if (reqY) { + this._cancelItemScroll(item, AXIS_Y); + delete this._requests[AXIS_Y][itemId]; + } + + delete this._requestOverlapCheck[itemId]; + delete this._dragPositions[itemId]; + delete this._dragDirections[itemId]; + this._items.splice(index, 1); + + if (this._isTicking && !this._items.length) { + this._stopTicking(); + } +}; + +AutoScroller.prototype.isItemScrollingX = function (item) { + var reqX = this._requests[AXIS_X][item._id]; + return !!(reqX && reqX.isActive); +}; + +AutoScroller.prototype.isItemScrollingY = function (item) { + var reqY = this._requests[AXIS_Y][item._id]; + return !!(reqY && reqY.isActive); +}; + +AutoScroller.prototype.isItemScrolling = function (item) { + return this.isItemScrollingX(item) || this.isItemScrollingY(item); +}; + +AutoScroller.prototype.destroy = function () { + if (this._isDestroyed) return; + + var items = this._items.slice(0); + for (var i = 0; i < items.length; i++) { + this.removeItem(items[i]); + } + + this._actions.length = 0; + this._requestPool.reset(); + this._actionPool.reset(); + + this._isDestroyed = true; +}; + +var ElProto = window.Element.prototype; +var matchesFn = + ElProto.matches || + ElProto.matchesSelector || + ElProto.webkitMatchesSelector || + ElProto.mozMatchesSelector || + ElProto.msMatchesSelector || + ElProto.oMatchesSelector || + function () { + return false; + }; + +/** + * Check if element matches a CSS selector. + * + * @param {Element} el + * @param {String} selector + * @returns {Boolean} + */ +function elementMatches(el, selector) { + return matchesFn.call(el, selector); +} + +/** + * Add class to an element. + * + * @param {HTMLElement} element + * @param {String} className + */ +function addClass(element, className) { + if (!className) return; + + if (element.classList) { + element.classList.add(className); + } else { + if (!elementMatches(element, '.' + className)) { + element.className += ' ' + className; + } + } +} + +var tempArray = []; +var numberType = 'number'; + +/** + * Insert an item or an array of items to array to a specified index. Mutates + * the array. The index can be negative in which case the items will be added + * to the end of the array. + * + * @param {Array} array + * @param {*} items + * @param {Number} [index=-1] + */ +function arrayInsert(array, items, index) { + var startIndex = typeof index === numberType ? index : -1; + if (startIndex < 0) startIndex = array.length - startIndex + 1; + + array.splice.apply(array, tempArray.concat(startIndex, 0, items)); + tempArray.length = 0; +} + +/** + * Normalize array index. Basically this function makes sure that the provided + * array index is within the bounds of the provided array and also transforms + * negative index to the matching positive index. The third (optional) argument + * allows you to define offset for array's length in case you are adding items + * to the array or removing items from the array. + * + * @param {Array} array + * @param {Number} index + * @param {Number} [sizeOffset] + */ +function normalizeArrayIndex(array, index, sizeOffset) { + var maxIndex = Math.max(0, array.length - 1 + (sizeOffset || 0)); + return index > maxIndex ? maxIndex : index < 0 ? Math.max(maxIndex + index + 1, 0) : index; +} + +/** + * Move array item to another index. + * + * @param {Array} array + * @param {Number} fromIndex + * - Index (positive or negative) of the item that will be moved. + * @param {Number} toIndex + * - Index (positive or negative) where the item should be moved to. + */ +function arrayMove(array, fromIndex, toIndex) { + // Make sure the array has two or more items. + if (array.length < 2) return; + + // Normalize the indices. + var from = normalizeArrayIndex(array, fromIndex); + var to = normalizeArrayIndex(array, toIndex); + + // Add target item to the new position. + if (from !== to) { + array.splice(to, 0, array.splice(from, 1)[0]); + } +} + +/** + * Swap array items. + * + * @param {Array} array + * @param {Number} index + * - Index (positive or negative) of the item that will be swapped. + * @param {Number} withIndex + * - Index (positive or negative) of the other item that will be swapped. + */ +function arraySwap(array, index, withIndex) { + // Make sure the array has two or more items. + if (array.length < 2) return; + + // Normalize the indices. + var indexA = normalizeArrayIndex(array, index); + var indexB = normalizeArrayIndex(array, withIndex); + var temp; + + // Swap the items. + if (indexA !== indexB) { + temp = array[indexA]; + array[indexA] = array[indexB]; + array[indexB] = temp; + } +} + +var transformProp = getPrefixedPropName(document.documentElement.style, 'transform') || 'transform'; + +var styleNameRegEx = /([A-Z])/g; +var prefixRegex = /^(webkit-|moz-|ms-|o-)/; +var msPrefixRegex = /^(-m-s-)/; + +/** + * Transforms a camel case style property to kebab case style property. Handles + * vendor prefixed properties elegantly as well, e.g. "WebkitTransform" and + * "webkitTransform" are both transformed into "-webkit-transform". + * + * @param {String} property + * @returns {String} + */ +function getStyleName(property) { + // Initial slicing, turns "fooBarProp" into "foo-bar-prop". + var styleName = property.replace(styleNameRegEx, '-$1').toLowerCase(); + + // Handle properties that start with "webkit", "moz", "ms" or "o" prefix (we + // need to add an extra '-' to the beginnig). + styleName = styleName.replace(prefixRegex, '-$1'); + + // Handle properties that start with "MS" prefix (we need to transform the + // "-m-s-" into "-ms-"). + styleName = styleName.replace(msPrefixRegex, '-ms-'); + + return styleName; +} + +var transformStyle = getStyleName(transformProp); + +var transformNone = 'none'; +var displayInline = 'inline'; +var displayNone = 'none'; +var displayStyle = 'display'; + +/** + * Returns true if element is transformed, false if not. In practice the + * element's display value must be anything else than "none" or "inline" as + * well as have a valid transform value applied in order to be counted as a + * transformed element. + * + * Borrowed from Mezr (v0.6.1): + * https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L661 + * + * @param {HTMLElement} element + * @returns {Boolean} + */ +function isTransformed(element) { + var transform = getStyle(element, transformStyle); + if (!transform || transform === transformNone) return false; + + var display = getStyle(element, displayStyle); + if (display === displayInline || display === displayNone) return false; + + return true; +} + +/** + * Returns an absolute positioned element's containing block, which is + * considered to be the closest ancestor element that the target element's + * positioning is relative to. Disclaimer: this only works as intended for + * absolute positioned elements. + * + * @param {HTMLElement} element + * @returns {(Document|Element)} + */ +function getContainingBlock(element) { + // As long as the containing block is an element, static and not + // transformed, try to get the element's parent element and fallback to + // document. https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L339 + var doc = document; + var res = element || doc; + while (res && res !== doc && getStyle(res, 'position') === 'static' && !isTransformed(res)) { + res = res.parentElement || doc; + } + return res; +} + +var offsetA = {}; +var offsetB = {}; +var offsetDiff = {}; + +/** + * Returns the element's document offset, which in practice means the vertical + * and horizontal distance between the element's northwest corner and the + * document's northwest corner. Note that this function always returns the same + * object so be sure to read the data from it instead using it as a reference. + * + * @param {(Document|Element|Window)} element + * @param {Object} [offsetData] + * - Optional data object where the offset data will be inserted to. If not + * provided a new object will be created for the return data. + * @returns {Object} + */ +function getOffset(element, offsetData) { + var offset = offsetData || {}; + var rect; + + // Set up return data. + offset.left = 0; + offset.top = 0; + + // Document's offsets are always 0. + if (element === document) return offset; + + // Add viewport scroll left/top to the respective offsets. + offset.left = window.pageXOffset || 0; + offset.top = window.pageYOffset || 0; + + // Window's offsets are the viewport scroll left/top values. + if (element.self === window.self) return offset; + + // Add element's client rects to the offsets. + rect = element.getBoundingClientRect(); + offset.left += rect.left; + offset.top += rect.top; + + // Exclude element's borders from the offset. + offset.left += getStyleAsFloat(element, 'border-left-width'); + offset.top += getStyleAsFloat(element, 'border-top-width'); + + return offset; +} + +/** + * Calculate the offset difference two elements. + * + * @param {HTMLElement} elemA + * @param {HTMLElement} elemB + * @param {Boolean} [compareContainingBlocks=false] + * - When this is set to true the containing blocks of the provided elements + * will be used for calculating the difference. Otherwise the provided + * elements will be compared directly. + * @returns {Object} + */ +function getOffsetDiff(elemA, elemB, compareContainingBlocks) { + offsetDiff.left = 0; + offsetDiff.top = 0; + + // If elements are same let's return early. + if (elemA === elemB) return offsetDiff; + + // Compare containing blocks if necessary. + if (compareContainingBlocks) { + elemA = getContainingBlock(elemA); + elemB = getContainingBlock(elemB); + + // If containing blocks are identical, let's return early. + if (elemA === elemB) return offsetDiff; + } + + // Finally, let's calculate the offset diff. + getOffset(elemA, offsetA); + getOffset(elemB, offsetB); + offsetDiff.left = offsetB.left - offsetA.left; + offsetDiff.top = offsetB.top - offsetA.top; + + return offsetDiff; +} + +/** + * Check if overflow style value is scrollable. + * + * @param {String} value + * @returns {Boolean} + */ +function isScrollableOverflow(value) { + return value === 'auto' || value === 'scroll' || value === 'overlay'; +} + +/** + * Check if an element is scrollable. + * + * @param {HTMLElement} element + * @returns {Boolean} + */ +function isScrollable(element) { + return ( + isScrollableOverflow(getStyle(element, 'overflow')) || + isScrollableOverflow(getStyle(element, 'overflow-x')) || + isScrollableOverflow(getStyle(element, 'overflow-y')) + ); +} + +/** + * Collect element's ancestors that are potentially scrollable elements. The + * provided element is also also included in the check, meaning that if it is + * scrollable it is added to the result array. + * + * @param {HTMLElement} element + * @param {Array} [result] + * @returns {Array} + */ +function getScrollableAncestors(element, result) { + result = result || []; + + // Find scroll parents. + while (element && element !== document) { + // If element is inside ShadowDOM let's get it's host node from the real + // DOM and continue looping. + if (element.getRootNode && element instanceof DocumentFragment) { + element = element.getRootNode().host; + continue; + } + + // If element is scrollable let's add it to the scrollable list. + if (isScrollable(element)) { + result.push(element); + } + + element = element.parentNode; + } + + // Always add window to the results. + result.push(window); + + return result; +} + +var translateValue = {}; +var transformNone$1 = 'none'; +var rxMat3d = /^matrix3d/; +var rxMatTx = /([^,]*,){4}/; +var rxMat3dTx = /([^,]*,){12}/; +var rxNextItem = /[^,]*,/; + +/** + * Returns the element's computed translateX and translateY values as a floats. + * The returned object is always the same object and updated every time this + * function is called. + * + * @param {HTMLElement} element + * @returns {Object} + */ +function getTranslate(element) { + translateValue.x = 0; + translateValue.y = 0; + + var transform = getStyle(element, transformStyle); + if (!transform || transform === transformNone$1) { + return translateValue; + } + + // Transform style can be in either matrix3d(...) or matrix(...). + var isMat3d = rxMat3d.test(transform); + var tX = transform.replace(isMat3d ? rxMat3dTx : rxMatTx, ''); + var tY = tX.replace(rxNextItem, ''); + + translateValue.x = parseFloat(tX) || 0; + translateValue.y = parseFloat(tY) || 0; + + return translateValue; +} + +/** + * Remove class from an element. + * + * @param {HTMLElement} element + * @param {String} className + */ +function removeClass(element, className) { + if (!className) return; + + if (element.classList) { + element.classList.remove(className); + } else { + if (elementMatches(element, '.' + className)) { + element.className = (' ' + element.className + ' ') + .replace(' ' + className + ' ', ' ') + .trim(); + } + } +} + +var START_PREDICATE_INACTIVE = 0; +var START_PREDICATE_PENDING = 1; +var START_PREDICATE_RESOLVED = 2; +var SCROLL_LISTENER_OPTIONS = hasPassiveEvents() ? { passive: true } : false; + +/** + * Bind touch interaction to an item. + * + * @class + * @param {Item} item + */ +function ItemDrag(item) { + var element = item._element; + var grid = item.getGrid(); + var settings = grid._settings; + + this._item = item; + this._gridId = grid._id; + this._isDestroyed = false; + this._isMigrating = false; + + // Start predicate data. + this._startPredicate = isFunction(settings.dragStartPredicate) + ? settings.dragStartPredicate + : ItemDrag.defaultStartPredicate; + this._startPredicateState = START_PREDICATE_INACTIVE; + this._startPredicateResult = undefined; + + // Data for drag sort predicate heuristics. + this._isSortNeeded = false; + this._sortTimer = undefined; + this._blockedSortIndex = null; + this._sortX1 = 0; + this._sortX2 = 0; + this._sortY1 = 0; + this._sortY2 = 0; + + // Setup item's initial drag data. + this._reset(); + + // Bind the methods that needs binding. + this._preStartCheck = this._preStartCheck.bind(this); + this._preEndCheck = this._preEndCheck.bind(this); + this._onScroll = this._onScroll.bind(this); + this._prepareStart = this._prepareStart.bind(this); + this._applyStart = this._applyStart.bind(this); + this._prepareMove = this._prepareMove.bind(this); + this._applyMove = this._applyMove.bind(this); + this._prepareScroll = this._prepareScroll.bind(this); + this._applyScroll = this._applyScroll.bind(this); + this._handleSort = this._handleSort.bind(this); + this._handleSortDelayed = this._handleSortDelayed.bind(this); + + // Get drag handle element. + this._handle = (settings.dragHandle && element.querySelector(settings.dragHandle)) || element; + + // Init dragger. + this._dragger = new Dragger(this._handle, settings.dragCssProps); + this._dragger.on('start', this._preStartCheck); + this._dragger.on('move', this._preStartCheck); + this._dragger.on('cancel', this._preEndCheck); + this._dragger.on('end', this._preEndCheck); +} + +/** + * Public properties + * ***************** + */ + +/** + * @public + * @static + * @type {AutoScroller} + */ +ItemDrag.autoScroller = new AutoScroller(); + +/** + * Public static methods + * ********************* + */ + +/** + * Default drag start predicate handler that handles anchor elements + * gracefully. The return value of this function defines if the drag is + * started, rejected or pending. When true is returned the dragging is started + * and when false is returned the dragging is rejected. If nothing is returned + * the predicate will be called again on the next drag movement. + * + * @public + * @static + * @param {Item} item + * @param {Object} event + * @param {Object} [options] + * - An optional options object which can be used to pass the predicate + * it's options manually. By default the predicate retrieves the options + * from the grid's settings. + * @returns {(Boolean|undefined)} + */ +ItemDrag.defaultStartPredicate = function (item, event, options) { + var drag = item._drag; + + // Make sure left button is pressed on mouse. + if (event.isFirst && event.srcEvent.button) { + return false; + } + + // If the start event is trusted, non-cancelable and it's default action has + // not been prevented it is in most cases a sign that the gesture would be + // cancelled anyways right after it has started (e.g. starting drag while + // the page is scrolling). + if ( + event.isFirst && + event.srcEvent.isTrusted === true && + event.srcEvent.defaultPrevented === false && + event.srcEvent.cancelable === false + ) { + return false; + } + + // Final event logic. At this stage return value does not matter anymore, + // the predicate is either resolved or it's not and there's nothing to do + // about it. Here we just reset data and if the item element is a link + // we follow it (if there has only been slight movement). + if (event.isFinal) { + drag._finishStartPredicate(event); + return; + } + + // Setup predicate data from options if not already set. + var predicate = drag._startPredicateData; + if (!predicate) { + var config = options || drag._getGrid()._settings.dragStartPredicate || {}; + drag._startPredicateData = predicate = { + distance: Math.max(config.distance, 0) || 0, + delay: Math.max(config.delay, 0) || 0, + }; + } + + // If delay is defined let's keep track of the latest event and initiate + // delay if it has not been done yet. + if (predicate.delay) { + predicate.event = event; + if (!predicate.delayTimer) { + predicate.delayTimer = window.setTimeout(function () { + predicate.delay = 0; + if (drag._resolveStartPredicate(predicate.event)) { + drag._forceResolveStartPredicate(predicate.event); + drag._resetStartPredicate(); + } + }, predicate.delay); + } + } + + return drag._resolveStartPredicate(event); +}; + +/** + * Default drag sort predicate. + * + * @public + * @static + * @param {Item} item + * @param {Object} [options] + * @param {Number} [options.threshold=50] + * @param {String} [options.action='move'] + * @returns {?Object} + * - Returns `null` if no valid index was found. Otherwise returns drag sort + * command. + */ +ItemDrag.defaultSortPredicate = (function () { + var itemRect = {}; + var targetRect = {}; + var returnData = {}; + var gridsArray = []; + var minThreshold = 1; + var maxThreshold = 100; + + function getTargetGrid(item, rootGrid, threshold) { + var target = null; + var dragSort = rootGrid._settings.dragSort; + var bestScore = -1; + var gridScore; + var grids; + var grid; + var container; + var containerRect; + var left; + var top; + var right; + var bottom; + var i; + + // Get potential target grids. + if (dragSort === true) { + gridsArray[0] = rootGrid; + grids = gridsArray; + } else if (isFunction(dragSort)) { + grids = dragSort.call(rootGrid, item); + } + + // Return immediately if there are no grids. + if (!grids || !Array.isArray(grids) || !grids.length) { + return target; + } + + // Loop through the grids and get the best match. + for (i = 0; i < grids.length; i++) { + grid = grids[i]; + + // Filter out all destroyed grids. + if (grid._isDestroyed) continue; + + // Compute the grid's client rect an clamp the initial boundaries to + // viewport dimensions. + grid._updateBoundingRect(); + left = Math.max(0, grid._left); + top = Math.max(0, grid._top); + right = Math.min(window.innerWidth, grid._right); + bottom = Math.min(window.innerHeight, grid._bottom); + + // The grid might be inside one or more elements that clip it's visibility + // (e.g overflow scroll/hidden) so we want to find out the visible portion + // of the grid in the viewport and use that in our calculations. + container = grid._element.parentNode; + while ( + container && + container !== document && + container !== document.documentElement && + container !== document.body + ) { + if (container.getRootNode && container instanceof DocumentFragment) { + container = container.getRootNode().host; + continue; + } + + if (getStyle(container, 'overflow') !== 'visible') { + containerRect = container.getBoundingClientRect(); + left = Math.max(left, containerRect.left); + top = Math.max(top, containerRect.top); + right = Math.min(right, containerRect.right); + bottom = Math.min(bottom, containerRect.bottom); + } + + if (getStyle(container, 'position') === 'fixed') { + break; + } + + container = container.parentNode; + } + + // No need to go further if target rect does not have visible area. + if (left >= right || top >= bottom) continue; + + // Check how much dragged element overlaps the container element. + targetRect.left = left; + targetRect.top = top; + targetRect.width = right - left; + targetRect.height = bottom - top; + gridScore = getIntersectionScore(itemRect, targetRect); + + // Check if this grid is the best match so far. + if (gridScore > threshold && gridScore > bestScore) { + bestScore = gridScore; + target = grid; + } + } + + // Always reset grids array. + gridsArray.length = 0; + + return target; + } + + return function (item, options) { + var drag = item._drag; + var rootGrid = drag._getGrid(); + + // Get drag sort predicate settings. + var sortThreshold = options && typeof options.threshold === 'number' ? options.threshold : 50; + var sortAction = options && options.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; + var migrateAction = + options && options.migrateAction === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; + + // Sort threshold must be a positive number capped to a max value of 100. If + // that's not the case this function will not work correctly. So let's clamp + // the threshold just in case. + sortThreshold = Math.min(Math.max(sortThreshold, minThreshold), maxThreshold); + + // Populate item rect data. + itemRect.width = item._width; + itemRect.height = item._height; + itemRect.left = drag._clientX; + itemRect.top = drag._clientY; + + // Calculate the target grid. + var grid = getTargetGrid(item, rootGrid, sortThreshold); + + // Return early if we found no grid container element that overlaps the + // dragged item enough. + if (!grid) return null; + + var isMigration = item.getGrid() !== grid; + var gridOffsetLeft = 0; + var gridOffsetTop = 0; + var matchScore = 0; + var matchIndex = -1; + var hasValidTargets = false; + var target; + var score; + var i; + + // If item is moved within it's originating grid adjust item's left and + // top props. Otherwise if item is moved to/within another grid get the + // container element's offset (from the element's content edge). + if (grid === rootGrid) { + itemRect.left = drag._gridX + item._marginLeft; + itemRect.top = drag._gridY + item._marginTop; + } else { + grid._updateBorders(1, 0, 1, 0); + gridOffsetLeft = grid._left + grid._borderLeft; + gridOffsetTop = grid._top + grid._borderTop; + } + + // Loop through the target grid items and try to find the best match. + for (i = 0; i < grid._items.length; i++) { + target = grid._items[i]; + + // If the target item is not active or the target item is the dragged + // item let's skip to the next item. + if (!target._isActive || target === item) { + continue; + } + + // Mark the grid as having valid target items. + hasValidTargets = true; + + // Calculate the target's overlap score with the dragged item. + targetRect.width = target._width; + targetRect.height = target._height; + targetRect.left = target._left + target._marginLeft + gridOffsetLeft; + targetRect.top = target._top + target._marginTop + gridOffsetTop; + score = getIntersectionScore(itemRect, targetRect); + + // Update best match index and score if the target's overlap score with + // the dragged item is higher than the current best match score. + if (score > matchScore) { + matchIndex = i; + matchScore = score; + } + } + + // If there is no valid match and the dragged item is being moved into + // another grid we need to do some guess work here. If there simply are no + // valid targets (which means that the dragged item will be the only active + // item in the new grid) we can just add it as the first item. If we have + // valid items in the new grid and the dragged item is overlapping one or + // more of the items in the new grid let's make an exception with the + // threshold and just pick the item which the dragged item is overlapping + // most. However, if the dragged item is not overlapping any of the valid + // items in the new grid let's position it as the last item in the grid. + if (isMigration && matchScore < sortThreshold) { + matchIndex = hasValidTargets ? matchIndex : 0; + matchScore = sortThreshold; + } + + // Check if the best match overlaps enough to justify a placement switch. + if (matchScore >= sortThreshold) { + returnData.grid = grid; + returnData.index = matchIndex; + returnData.action = isMigration ? migrateAction : sortAction; + return returnData; + } + + return null; + }; +})(); + +/** + * Public prototype methods + * ************************ + */ + +/** + * Abort dragging and reset drag data. + * + * @public + */ +ItemDrag.prototype.stop = function () { + if (!this._isActive) return; + + // If the item is being dropped into another grid, finish it up and return + // immediately. + if (this._isMigrating) { + this._finishMigration(); + return; + } + + // Cancel queued ticks. + var itemId = this._item._id; + cancelDragStartTick(itemId); + cancelDragMoveTick(itemId); + cancelDragScrollTick(itemId); + + // Cancel sort procedure. + this._cancelSort(); + + if (this._isStarted) { + // Remove scroll listeners. + this._unbindScrollListeners(); + + var element = item._element; + var grid = this._getGrid(); + var draggingClass = grid._settings.itemDraggingClass; + + // Append item element to the container if it's not it's child. Also make + // sure the translate values are adjusted to account for the DOM shift. + if (element.parentNode !== grid._element) { + grid._element.appendChild(element); + item._setTranslate(this._gridX, this._gridY); + + // We need to do forced reflow to make sure the dragging class is removed + // gracefully. + // eslint-disable-next-line + if (draggingClass) element.clientWidth; + } + + // Remove dragging class. + removeClass(element, draggingClass); + } + + // Reset drag data. + this._reset(); +}; + +/** + * Manually trigger drag sort. This is only needed for special edge cases where + * e.g. you have disabled sort and want to trigger a sort right after enabling + * it (and don't want to wait for the next move/scroll event). + * + * @private + * @param {Boolean} [force=false] + */ +ItemDrag.prototype.sort = function (force) { + var item = this._item; + if (item._isActive && this._dragMoveEvent) { + if (force === true) { + this._handleSort(); + } else { + addDragSortTick(item._id, this._handleSort); + } + } +}; + +/** + * Destroy instance. + * + * @public + */ +ItemDrag.prototype.destroy = function () { + if (this._isDestroyed) return; + this.stop(); + this._dragger.destroy(); + ItemDrag.autoScroller.removeItem(this._item); + this._isDestroyed = true; +}; + +/** + * Private prototype methods + * ************************* + */ + +/** + * Get Grid instance. + * + * @private + * @returns {?Grid} + */ +ItemDrag.prototype._getGrid = function () { + return GRID_INSTANCES[this._gridId] || null; +}; + +/** + * Setup/reset drag data. + * + * @private + */ +ItemDrag.prototype._reset = function () { + this._isActive = false; + this._isStarted = false; + + // The dragged item's container element. + this._container = null; + + // The dragged item's containing block. + this._containingBlock = null; + + // Drag/scroll event data. + this._dragStartEvent = null; + this._dragMoveEvent = null; + this._dragPrevMoveEvent = null; + this._scrollEvent = null; + + // All the elements which need to be listened for scroll events during + // dragging. + this._scrollers = []; + + // The current translateX/translateY position. + this._left = 0; + this._top = 0; + + // Dragged element's current position within the grid. + this._gridX = 0; + this._gridY = 0; + + // Dragged element's current offset from window's northwest corner. Does + // not account for element's margins. + this._clientX = 0; + this._clientY = 0; + + // Keep track of the clientX/Y diff for scrolling. + this._scrollDiffX = 0; + this._scrollDiffY = 0; + + // Keep track of the clientX/Y diff for moving. + this._moveDiffX = 0; + this._moveDiffY = 0; + + // Offset difference between the dragged element's temporary drag + // container and it's original container. + this._containerDiffX = 0; + this._containerDiffY = 0; +}; + +/** + * Bind drag scroll handlers to all scrollable ancestor elements of the + * dragged element and the drag container element. + * + * @private + */ +ItemDrag.prototype._bindScrollListeners = function () { + var gridContainer = this._getGrid()._element; + var dragContainer = this._container; + var scrollers = this._scrollers; + var gridScrollers; + var i; + + // Get dragged element's scrolling parents. + scrollers.length = 0; + getScrollableAncestors(this._item._element.parentNode, scrollers); + + // If drag container is defined and it's not the same element as grid + // container then we need to add the grid container and it's scroll parents + // to the elements which are going to be listener for scroll events. + if (dragContainer !== gridContainer) { + gridScrollers = []; + getScrollableAncestors(gridContainer, gridScrollers); + for (i = 0; i < gridScrollers.length; i++) { + if (scrollers.indexOf(gridScrollers[i]) < 0) { + scrollers.push(gridScrollers[i]); + } + } + } + + // Bind scroll listeners. + for (i = 0; i < scrollers.length; i++) { + scrollers[i].addEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS); + } +}; + +/** + * Unbind currently bound drag scroll handlers from all scrollable ancestor + * elements of the dragged element and the drag container element. + * + * @private + */ +ItemDrag.prototype._unbindScrollListeners = function () { + var scrollers = this._scrollers; + var i; + + for (i = 0; i < scrollers.length; i++) { + scrollers[i].removeEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS); + } + + scrollers.length = 0; +}; + +/** + * Unbind currently bound drag scroll handlers from all scrollable ancestor + * elements of the dragged element and the drag container element. + * + * @private + * @param {Object} event + * @returns {Boolean} + */ +ItemDrag.prototype._resolveStartPredicate = function (event) { + var predicate = this._startPredicateData; + if (event.distance < predicate.distance || predicate.delay) return; + this._resetStartPredicate(); + return true; +}; + +/** + * Forcefully resolve drag start predicate. + * + * @private + * @param {Object} event + */ +ItemDrag.prototype._forceResolveStartPredicate = function (event) { + if (!this._isDestroyed && this._startPredicateState === START_PREDICATE_PENDING) { + this._startPredicateState = START_PREDICATE_RESOLVED; + this._onStart(event); + } +}; + +/** + * Finalize start predicate. + * + * @private + * @param {Object} event + */ +ItemDrag.prototype._finishStartPredicate = function (event) { + var element = this._item._element; + + // Check if this is a click (very subjective heuristics). + var isClick = Math.abs(event.deltaX) < 2 && Math.abs(event.deltaY) < 2 && event.deltaTime < 200; + + // Reset predicate. + this._resetStartPredicate(); + + // If the gesture can be interpreted as click let's try to open the element's + // href url (if it is an anchor element). + if (isClick) openAnchorHref(element); +}; + +/** + * Reset drag sort heuristics. + * + * @private + * @param {Number} x + * @param {Number} y + */ +ItemDrag.prototype._resetHeuristics = function (x, y) { + this._blockedSortIndex = null; + this._sortX1 = this._sortX2 = x; + this._sortY1 = this._sortY2 = y; +}; + +/** + * Run heuristics and return true if overlap check can be performed, and false + * if it can not. + * + * @private + * @param {Number} x + * @param {Number} y + * @returns {Boolean} + */ +ItemDrag.prototype._checkHeuristics = function (x, y) { + var settings = this._getGrid()._settings.dragSortHeuristics; + var minDist = settings.minDragDistance; + + // Skip heuristics if not needed. + if (minDist <= 0) { + this._blockedSortIndex = null; + return true; + } + + var diffX = x - this._sortX2; + var diffY = y - this._sortY2; + + // If we can't do proper bounce back check make sure that the blocked index + // is not set. + var canCheckBounceBack = minDist > 3 && settings.minBounceBackAngle > 0; + if (!canCheckBounceBack) { + this._blockedSortIndex = null; + } + + if (Math.abs(diffX) > minDist || Math.abs(diffY) > minDist) { + // Reset blocked index if angle changed enough. This check requires a + // minimum value of 3 for minDragDistance to function properly. + if (canCheckBounceBack) { + var angle = Math.atan2(diffX, diffY); + var prevAngle = Math.atan2(this._sortX2 - this._sortX1, this._sortY2 - this._sortY1); + var deltaAngle = Math.atan2(Math.sin(angle - prevAngle), Math.cos(angle - prevAngle)); + if (Math.abs(deltaAngle) > settings.minBounceBackAngle) { + this._blockedSortIndex = null; + } + } + + // Update points. + this._sortX1 = this._sortX2; + this._sortY1 = this._sortY2; + this._sortX2 = x; + this._sortY2 = y; + + return true; + } + + return false; +}; + +/** + * Reset for default drag start predicate function. + * + * @private + */ +ItemDrag.prototype._resetStartPredicate = function () { + var predicate = this._startPredicateData; + if (predicate) { + if (predicate.delayTimer) { + predicate.delayTimer = window.clearTimeout(predicate.delayTimer); + } + this._startPredicateData = null; + } +}; + +/** + * Handle the sorting procedure. Manage drag sort heuristics/interval and + * check overlap when necessary. + * + * @private + */ +ItemDrag.prototype._handleSort = function () { + var settings = this._getGrid()._settings; + + // No sorting when drag sort is disabled. Also, account for the scenario where + // dragSort is temporarily disabled during drag procedure so we need to reset + // sort timer heuristics state too. + if ( + !settings.dragSort || + (!settings.dragAutoScroll.sortDuringScroll && ItemDrag.autoScroller.isItemScrolling(this._item)) + ) { + this._sortX1 = this._sortX2 = this._gridX; + this._sortY1 = this._sortY2 = this._gridY; + // We set this to true intentionally so that overlap check would be + // triggered as soon as possible after sort becomes enabled again. + this._isSortNeeded = true; + if (this._sortTimer !== undefined) { + this._sortTimer = window.clearTimeout(this._sortTimer); + } + return; + } + + // If sorting is enabled we always need to run the heuristics check to keep + // the tracked coordinates updated. We also allow an exception when the sort + // timer is finished because the heuristics are intended to prevent overlap + // checks based on the dragged element's immediate movement and a delayed + // overlap check is valid if it comes through, because it was valid when it + // was invoked. + var shouldSort = this._checkHeuristics(this._gridX, this._gridY); + if (!this._isSortNeeded && !shouldSort) return; + + var sortInterval = settings.dragSortHeuristics.sortInterval; + if (sortInterval <= 0 || this._isSortNeeded) { + this._isSortNeeded = false; + if (this._sortTimer !== undefined) { + this._sortTimer = window.clearTimeout(this._sortTimer); + } + this._checkOverlap(); + } else if (this._sortTimer === undefined) { + this._sortTimer = window.setTimeout(this._handleSortDelayed, sortInterval); + } +}; + +/** + * Delayed sort handler. + * + * @private + */ +ItemDrag.prototype._handleSortDelayed = function () { + this._isSortNeeded = true; + this._sortTimer = undefined; + addDragSortTick(this._item._id, this._handleSort); +}; + +/** + * Cancel and reset sort procedure. + * + * @private + */ +ItemDrag.prototype._cancelSort = function () { + this._isSortNeeded = false; + if (this._sortTimer !== undefined) { + this._sortTimer = window.clearTimeout(this._sortTimer); + } + cancelDragSortTick(this._item._id); +}; + +/** + * Handle the ending of the drag procedure for sorting. + * + * @private + */ +ItemDrag.prototype._finishSort = function () { + var isSortEnabled = this._getGrid()._settings.dragSort; + var needsFinalCheck = isSortEnabled && (this._isSortNeeded || this._sortTimer !== undefined); + this._cancelSort(); + if (needsFinalCheck) this._checkOverlap(); +}; + +/** + * Check (during drag) if an item is overlapping other items and based on + * the configuration layout the items. + * + * @private + */ +ItemDrag.prototype._checkOverlap = function () { + if (!this._isActive) return; + + var item = this._item; + var settings = this._getGrid()._settings; + var result; + var currentGrid; + var currentIndex; + var targetGrid; + var targetIndex; + var targetItem; + var sortAction; + var isMigration; + + // Get overlap check result. + if (isFunction(settings.dragSortPredicate)) { + result = settings.dragSortPredicate(item, this._dragMoveEvent); + } else { + result = ItemDrag.defaultSortPredicate(item, settings.dragSortPredicate); + } + + // Let's make sure the result object has a valid index before going further. + if (!result || typeof result.index !== 'number') return; + + sortAction = result.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; + currentGrid = item.getGrid(); + targetGrid = result.grid || currentGrid; + isMigration = currentGrid !== targetGrid; + currentIndex = currentGrid._items.indexOf(item); + targetIndex = normalizeArrayIndex( + targetGrid._items, + result.index, + isMigration && sortAction === ACTION_MOVE ? 1 : 0 + ); + + // Prevent position bounce. + if (!isMigration && targetIndex === this._blockedSortIndex) { + return; + } + + // If the item was moved within it's current grid. + if (!isMigration) { + // Make sure the target index is not the current index. + if (currentIndex !== targetIndex) { + this._blockedSortIndex = currentIndex; + + // Do the sort. + (sortAction === ACTION_SWAP ? arraySwap : arrayMove)( + currentGrid._items, + currentIndex, + targetIndex + ); + + // Emit move event. + if (currentGrid._hasListeners(EVENT_MOVE)) { + currentGrid._emit(EVENT_MOVE, { + item: item, + fromIndex: currentIndex, + toIndex: targetIndex, + action: sortAction, + }); + } + + // Layout the grid. + currentGrid.layout(); + } + } + + // If the item was moved to another grid. + else { + this._blockedSortIndex = null; + + // Let's fetch the target item when it's still in it's original index. + targetItem = targetGrid._items[targetIndex]; + + // Emit beforeSend event. + if (currentGrid._hasListeners(EVENT_BEFORE_SEND)) { + currentGrid._emit(EVENT_BEFORE_SEND, { + item: item, + fromGrid: currentGrid, + fromIndex: currentIndex, + toGrid: targetGrid, + toIndex: targetIndex, + }); + } + + // Emit beforeReceive event. + if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) { + targetGrid._emit(EVENT_BEFORE_RECEIVE, { + item: item, + fromGrid: currentGrid, + fromIndex: currentIndex, + toGrid: targetGrid, + toIndex: targetIndex, + }); + } + + // Update item's grid id reference. + item._gridId = targetGrid._id; + + // Update drag instance's migrating indicator. + this._isMigrating = item._gridId !== this._gridId; + + // Move item instance from current grid to target grid. + currentGrid._items.splice(currentIndex, 1); + arrayInsert(targetGrid._items, item, targetIndex); + + // Reset sort data. + item._sortData = null; + + // Emit send event. + if (currentGrid._hasListeners(EVENT_SEND)) { + currentGrid._emit(EVENT_SEND, { + item: item, + fromGrid: currentGrid, + fromIndex: currentIndex, + toGrid: targetGrid, + toIndex: targetIndex, + }); + } + + // Emit receive event. + if (targetGrid._hasListeners(EVENT_RECEIVE)) { + targetGrid._emit(EVENT_RECEIVE, { + item: item, + fromGrid: currentGrid, + fromIndex: currentIndex, + toGrid: targetGrid, + toIndex: targetIndex, + }); + } + + // If the sort action is "swap" let's respect it and send the target item + // (if it exists) from the target grid to the originating grid. This process + // is done on purpose after the dragged item placed within the target grid + // so that we can keep this implementation as simple as possible utilizing + // the existing API. + if (sortAction === ACTION_SWAP && targetItem && targetItem.isActive()) { + // Sanity check to make sure that the target item is still part of the + // target grid. It could have been manipulated in the event handlers. + if (targetGrid._items.indexOf(targetItem) > -1) { + targetGrid.send(targetItem, currentGrid, currentIndex, { + appendTo: this._container || document.body, + layoutSender: false, + layoutReceiver: false, + }); + } + } + + // Layout both grids. + currentGrid.layout(); + targetGrid.layout(); + } +}; + +/** + * If item is dragged into another grid, finish the migration process + * gracefully. + * + * @private + */ +ItemDrag.prototype._finishMigration = function () { + var item = this._item; + var release = item._dragRelease; + var element = item._element; + var isActive = item._isActive; + var targetGrid = item.getGrid(); + var targetGridElement = targetGrid._element; + var targetSettings = targetGrid._settings; + var targetContainer = targetSettings.dragContainer || targetGridElement; + var currentSettings = this._getGrid()._settings; + var currentContainer = element.parentNode; + var currentVisClass = isActive + ? currentSettings.itemVisibleClass + : currentSettings.itemHiddenClass; + var nextVisClass = isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass; + var translate; + var offsetDiff; + + // Destroy current drag. Note that we need to set the migrating flag to + // false first, because otherwise we create an infinite loop between this + // and the drag.stop() method. + this._isMigrating = false; + this.destroy(); + + // Update item class. + if (currentSettings.itemClass !== targetSettings.itemClass) { + removeClass(element, currentSettings.itemClass); + addClass(element, targetSettings.itemClass); + } + + // Update visibility class. + if (currentVisClass !== nextVisClass) { + removeClass(element, currentVisClass); + addClass(element, nextVisClass); + } + + // Move the item inside the target container if it's different than the + // current container. + if (targetContainer !== currentContainer) { + targetContainer.appendChild(element); + offsetDiff = getOffsetDiff(currentContainer, targetContainer, true); + translate = getTranslate(element); + translate.x -= offsetDiff.left; + translate.y -= offsetDiff.top; + } + + // Update item's cached dimensions. + item._refreshDimensions(); + + // Calculate the offset difference between target's drag container (if any) + // and actual grid container element. We save it later for the release + // process. + offsetDiff = getOffsetDiff(targetContainer, targetGridElement, true); + release._containerDiffX = offsetDiff.left; + release._containerDiffY = offsetDiff.top; + + // Recreate item's drag handler. + item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null; + + // Adjust the position of the item element if it was moved from a container + // to another. + if (targetContainer !== currentContainer) { + item._setTranslate(translate.x, translate.y); + } + + // Update child element's styles to reflect the current visibility state. + item._visibility.setStyles(isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles); + + // Start the release. + release.start(); +}; + +/** + * Drag pre-start handler. + * + * @private + * @param {Object} event + */ +ItemDrag.prototype._preStartCheck = function (event) { + // Let's activate drag start predicate state. + if (this._startPredicateState === START_PREDICATE_INACTIVE) { + this._startPredicateState = START_PREDICATE_PENDING; + } + + // If predicate is pending try to resolve it. + if (this._startPredicateState === START_PREDICATE_PENDING) { + this._startPredicateResult = this._startPredicate(this._item, event); + if (this._startPredicateResult === true) { + this._startPredicateState = START_PREDICATE_RESOLVED; + this._onStart(event); + } else if (this._startPredicateResult === false) { + this._resetStartPredicate(event); + this._dragger._reset(); + this._startPredicateState = START_PREDICATE_INACTIVE; + } + } + + // Otherwise if predicate is resolved and drag is active, move the item. + else if (this._startPredicateState === START_PREDICATE_RESOLVED && this._isActive) { + this._onMove(event); + } +}; + +/** + * Drag pre-end handler. + * + * @private + * @param {Object} event + */ +ItemDrag.prototype._preEndCheck = function (event) { + var isResolved = this._startPredicateState === START_PREDICATE_RESOLVED; + + // Do final predicate check to allow user to unbind stuff for the current + // drag procedure within the predicate callback. The return value of this + // check will have no effect to the state of the predicate. + this._startPredicate(this._item, event); + + this._startPredicateState = START_PREDICATE_INACTIVE; + + if (!isResolved || !this._isActive) return; + + if (this._isStarted) { + this._onEnd(event); + } else { + this.stop(); + } +}; + +/** + * Drag start handler. + * + * @private + * @param {Object} event + */ +ItemDrag.prototype._onStart = function (event) { + var item = this._item; + if (!item._isActive) return; + + this._isActive = true; + this._dragStartEvent = event; + ItemDrag.autoScroller.addItem(item); + + addDragStartTick(item._id, this._prepareStart, this._applyStart); +}; + +/** + * Prepare item to be dragged. + * + * @private + * ItemDrag.prototype + */ +ItemDrag.prototype._prepareStart = function () { + var item = this._item; + if (!item._isActive) return; + + var element = item._element; + var grid = this._getGrid(); + var settings = grid._settings; + var gridContainer = grid._element; + var dragContainer = settings.dragContainer || gridContainer; + var containingBlock = getContainingBlock(dragContainer); + var translate = getTranslate(element); + var elementRect = element.getBoundingClientRect(); + var hasDragContainer = dragContainer !== gridContainer; + + this._container = dragContainer; + this._containingBlock = containingBlock; + this._clientX = elementRect.left; + this._clientY = elementRect.top; + this._left = this._gridX = translate.x; + this._top = this._gridY = translate.y; + this._scrollDiffX = this._scrollDiffY = 0; + this._moveDiffX = this._moveDiffY = 0; + + this._resetHeuristics(this._gridX, this._gridY); + + // If a specific drag container is set and it is different from the + // grid's container element we store the offset between containers. + if (hasDragContainer) { + var offsetDiff = getOffsetDiff(containingBlock, gridContainer); + this._containerDiffX = offsetDiff.left; + this._containerDiffY = offsetDiff.top; + } +}; + +/** + * Start drag for the item. + * + * @private + */ +ItemDrag.prototype._applyStart = function () { + var item = this._item; + if (!item._isActive) return; + + var grid = this._getGrid(); + var element = item._element; + var release = item._dragRelease; + var migrate = item._migrate; + var hasDragContainer = this._container !== grid._element; + + if (item.isPositioning()) { + item._layout.stop(true, this._left, this._top); + } + + if (migrate._isActive) { + this._left -= migrate._containerDiffX; + this._top -= migrate._containerDiffY; + this._gridX -= migrate._containerDiffX; + this._gridY -= migrate._containerDiffY; + migrate.stop(true, this._left, this._top); + } + + if (item.isReleasing()) { + release._reset(); + } + + if (grid._settings.dragPlaceholder.enabled) { + item._dragPlaceholder.create(); + } + + this._isStarted = true; + + grid._emit(EVENT_DRAG_INIT, item, this._dragStartEvent); + + if (hasDragContainer) { + // If the dragged element is a child of the drag container all we need to + // do is setup the relative drag position data. + if (element.parentNode === this._container) { + this._gridX -= this._containerDiffX; + this._gridY -= this._containerDiffY; + } + // Otherwise we need to append the element inside the correct container, + // setup the actual drag position data and adjust the element's translate + // values to account for the DOM position shift. + else { + this._left += this._containerDiffX; + this._top += this._containerDiffY; + this._container.appendChild(element); + item._setTranslate(this._left, this._top); + } + } + + addClass(element, grid._settings.itemDraggingClass); + this._bindScrollListeners(); + grid._emit(EVENT_DRAG_START, item, this._dragStartEvent); +}; + +/** + * Drag move handler. + * + * @private + * @param {Object} event + */ +ItemDrag.prototype._onMove = function (event) { + var item = this._item; + + if (!item._isActive) { + this.stop(); + return; + } + + this._dragMoveEvent = event; + addDragMoveTick(item._id, this._prepareMove, this._applyMove); + addDragSortTick(item._id, this._handleSort); +}; + +/** + * Prepare dragged item for moving. + * + * @private + */ +ItemDrag.prototype._prepareMove = function () { + var item = this._item; + + if (!item._isActive) return; + + var settings = this._getGrid()._settings; + var axis = settings.dragAxis; + var nextEvent = this._dragMoveEvent; + var prevEvent = this._dragPrevMoveEvent || this._dragStartEvent || nextEvent; + + // Update horizontal position data. + if (axis !== 'y') { + var moveDiffX = nextEvent.clientX - prevEvent.clientX; + this._left = this._left - this._moveDiffX + moveDiffX; + this._gridX = this._gridX - this._moveDiffX + moveDiffX; + this._clientX = this._clientX - this._moveDiffX + moveDiffX; + this._moveDiffX = moveDiffX; + } + + // Update vertical position data. + if (axis !== 'x') { + var moveDiffY = nextEvent.clientY - prevEvent.clientY; + this._top = this._top - this._moveDiffY + moveDiffY; + this._gridY = this._gridY - this._moveDiffY + moveDiffY; + this._clientY = this._clientY - this._moveDiffY + moveDiffY; + this._moveDiffY = moveDiffY; + } + + this._dragPrevMoveEvent = nextEvent; +}; + +/** + * Apply movement to dragged item. + * + * @private + */ +ItemDrag.prototype._applyMove = function () { + var item = this._item; + if (!item._isActive) return; + + this._moveDiffX = this._moveDiffY = 0; + item._setTranslate(this._left, this._top); + this._getGrid()._emit(EVENT_DRAG_MOVE, item, this._dragMoveEvent); + ItemDrag.autoScroller.updateItem(item); +}; + +/** + * Drag scroll handler. + * + * @private + * @param {Object} event + */ +ItemDrag.prototype._onScroll = function (event) { + var item = this._item; + + if (!item._isActive) { + this.stop(); + return; + } + + this._scrollEvent = event; + addDragScrollTick(item._id, this._prepareScroll, this._applyScroll); + addDragSortTick(item._id, this._handleSort); +}; + +/** + * Prepare dragged item for scrolling. + * + * @private + */ +ItemDrag.prototype._prepareScroll = function () { + var item = this._item; + + // If item is not active do nothing. + if (!item._isActive) return; + + var element = item._element; + var grid = this._getGrid(); + var gridContainer = grid._element; + var axis = grid._settings.dragAxis; + var moveX = axis !== 'y'; + var moveY = axis !== 'x'; + var rect = element.getBoundingClientRect(); + + // Update container diff. + if (this._container !== gridContainer) { + var offsetDiff = getOffsetDiff(this._containingBlock, gridContainer); + this._containerDiffX = offsetDiff.left; + this._containerDiffY = offsetDiff.top; + } + + // Update horizontal position data. + if (moveX) { + var scrollDiffX = this._clientX - this._moveDiffX - this._scrollDiffX - rect.left; + this._left = this._left - this._scrollDiffX + scrollDiffX; + this._scrollDiffX = scrollDiffX; + } + + // Update vertical position data. + if (moveY) { + var scrollDiffY = this._clientY - this._moveDiffY - this._scrollDiffY - rect.top; + this._top = this._top - this._scrollDiffY + scrollDiffY; + this._scrollDiffY = scrollDiffY; + } + + // Update grid position. + this._gridX = this._left - this._containerDiffX; + this._gridY = this._top - this._containerDiffY; +}; + +/** + * Apply scroll to dragged item. + * + * @private + */ +ItemDrag.prototype._applyScroll = function () { + var item = this._item; + if (!item._isActive) return; + + this._scrollDiffX = this._scrollDiffY = 0; + item._setTranslate(this._left, this._top); + this._getGrid()._emit(EVENT_DRAG_SCROLL, item, this._scrollEvent); +}; + +/** + * Drag end handler. + * + * @private + * @param {Object} event + */ +ItemDrag.prototype._onEnd = function (event) { + var item = this._item; + var element = item._element; + var grid = this._getGrid(); + var settings = grid._settings; + var release = item._dragRelease; + + // If item is not active, reset drag. + if (!item._isActive) { + this.stop(); + return; + } + + // Cancel queued ticks. + cancelDragStartTick(item._id); + cancelDragMoveTick(item._id); + cancelDragScrollTick(item._id); + + // Finish sort procedure (does final overlap check if needed). + this._finishSort(); + + // Remove scroll listeners. + this._unbindScrollListeners(); + + // Setup release data. + release._containerDiffX = this._containerDiffX; + release._containerDiffY = this._containerDiffY; + + // Reset drag data. + this._reset(); + + // Remove drag class name from element. + removeClass(element, settings.itemDraggingClass); + + // Stop auto-scroll. + ItemDrag.autoScroller.removeItem(item); + + // Emit dragEnd event. + grid._emit(EVENT_DRAG_END, item, event); + + // Finish up the migration process or start the release process. + this._isMigrating ? this._finishMigration() : release.start(); +}; + +/** + * Private helpers + * *************** + */ + +/** + * Check if an element is an anchor element and open the href url if possible. + * + * @param {HTMLElement} element + */ +function openAnchorHref(element) { + // Make sure the element is anchor element. + if (element.tagName.toLowerCase() !== 'a') return; + + // Get href and make sure it exists. + var href = element.getAttribute('href'); + if (!href) return; + + // Finally let's navigate to the link href. + var target = element.getAttribute('target'); + if (target && target !== '_self') { + window.open(href, target); + } else { + window.location.href = href; + } +} + +/** + * Get current values of the provided styles definition object or array. + * + * @param {HTMLElement} element + * @param {(Object|Array} styles + * @return {Object} + */ +function getCurrentStyles(element, styles) { + var result = {}; + var prop, i; + + if (Array.isArray(styles)) { + for (i = 0; i < styles.length; i++) { + prop = styles[i]; + result[prop] = getStyle(element, getStyleName(prop)); + } + } else { + for (prop in styles) { + result[prop] = getStyle(element, getStyleName(prop)); + } + } + + return result; +} + +var unprefixRegEx = /^(webkit|moz|ms|o|Webkit|Moz|MS|O)(?=[A-Z])/; +var cache$2 = {}; + +/** + * Remove any potential vendor prefixes from a property name. + * + * @param {String} prop + * @returns {String} + */ +function getUnprefixedPropName(prop) { + var result = cache$2[prop]; + if (result) return result; + + result = prop.replace(unprefixRegEx, ''); + + if (result !== prop) { + result = result[0].toLowerCase() + result.slice(1); + } + + cache$2[prop] = result; + + return result; +} + +var nativeCode = '[native code]'; + +/** + * Check if a value (e.g. a method or constructor) is native code. Good for + * detecting when a polyfill is used and when not. + * + * @param {*} feat + * @returns {Boolean} + */ +function isNative(feat) { + var S = window.Symbol; + return !!( + feat && + isFunction(S) && + isFunction(S.toString) && + S(feat).toString().indexOf(nativeCode) > -1 + ); +} + +/** + * Set inline styles to an element. + * + * @param {HTMLElement} element + * @param {Object} styles + */ +function setStyles(element, styles) { + for (var prop in styles) { + element.style[prop] = styles[prop]; + } +} + +var HAS_WEB_ANIMATIONS = !!(Element && isFunction(Element.prototype.animate)); +var HAS_NATIVE_WEB_ANIMATIONS = !!(Element && isNative(Element.prototype.animate)); + +/** + * Item animation handler powered by Web Animations API. + * + * @class + * @param {HTMLElement} element + */ +function Animator(element) { + this._element = element; + this._animation = null; + this._duration = 0; + this._easing = ''; + this._callback = null; + this._props = []; + this._values = []; + this._isDestroyed = false; + this._onFinish = this._onFinish.bind(this); +} + +/** + * Public prototype methods + * ************************ + */ + +/** + * Start instance's animation. Automatically stops current animation if it is + * running. + * + * @public + * @param {Object} propsFrom + * @param {Object} propsTo + * @param {Object} [options] + * @param {Number} [options.duration=300] + * @param {String} [options.easing='ease'] + * @param {Function} [options.onFinish] + */ +Animator.prototype.start = function (propsFrom, propsTo, options) { + if (this._isDestroyed) return; + + var element = this._element; + var opts = options || {}; + + // If we don't have web animations available let's not animate. + if (!HAS_WEB_ANIMATIONS) { + setStyles(element, propsTo); + this._callback = isFunction(opts.onFinish) ? opts.onFinish : null; + this._onFinish(); + return; + } + + var animation = this._animation; + var currentProps = this._props; + var currentValues = this._values; + var duration = opts.duration || 300; + var easing = opts.easing || 'ease'; + var cancelAnimation = false; + var propName, propCount, propIndex; + + // If we have an existing animation running, let's check if it needs to be + // cancelled or if it can continue running. + if (animation) { + propCount = 0; + + // Cancel animation if duration or easing has changed. + if (duration !== this._duration || easing !== this._easing) { + cancelAnimation = true; + } + + // Check if the requested animation target props and values match with the + // current props and values. + if (!cancelAnimation) { + for (propName in propsTo) { + ++propCount; + propIndex = currentProps.indexOf(propName); + if (propIndex === -1 || propsTo[propName] !== currentValues[propIndex]) { + cancelAnimation = true; + break; + } + } + + // Check if the target props count matches current props count. This is + // needed for the edge case scenario where target props contain the same + // styles as current props, but the current props have some additional + // props. + if (propCount !== currentProps.length) { + cancelAnimation = true; + } + } + } + + // Cancel animation (if required). + if (cancelAnimation) animation.cancel(); + + // Store animation callback. + this._callback = isFunction(opts.onFinish) ? opts.onFinish : null; + + // If we have a running animation that does not need to be cancelled, let's + // call it a day here and let it run. + if (animation && !cancelAnimation) return; + + // Store target props and values to instance. + currentProps.length = currentValues.length = 0; + for (propName in propsTo) { + currentProps.push(propName); + currentValues.push(propsTo[propName]); + } + + // Start the animation. We need to provide unprefixed property names to the + // Web Animations polyfill if it is being used. If we have native Web + // Animations available we need to provide prefixed properties instead. + this._duration = duration; + this._easing = easing; + this._animation = element.animate( + [ + createFrame(propsFrom, HAS_NATIVE_WEB_ANIMATIONS), + createFrame(propsTo, HAS_NATIVE_WEB_ANIMATIONS), + ], + { + duration: duration, + easing: easing, + } + ); + this._animation.onfinish = this._onFinish; + + // Set the end styles. This makes sure that the element stays at the end + // values after animation is finished. + setStyles(element, propsTo); +}; + +/** + * Stop instance's current animation if running. + * + * @public + */ +Animator.prototype.stop = function () { + if (this._isDestroyed || !this._animation) return; + this._animation.cancel(); + this._animation = this._callback = null; + this._props.length = this._values.length = 0; +}; + +/** + * Read the current values of the element's animated styles from the DOM. + * + * @public + * @return {Object} + */ +Animator.prototype.getCurrentStyles = function () { + return getCurrentStyles(element, currentProps); +}; + +/** + * Check if the item is being animated currently. + * + * @public + * @return {Boolean} + */ +Animator.prototype.isAnimating = function () { + return !!this._animation; +}; + +/** + * Destroy the instance and stop current animation if it is running. + * + * @public + */ +Animator.prototype.destroy = function () { + if (this._isDestroyed) return; + this.stop(); + this._element = null; + this._isDestroyed = true; +}; + +/** + * Private prototype methods + * ************************* + */ + +/** + * Animation end handler. + * + * @private + */ +Animator.prototype._onFinish = function () { + var callback = this._callback; + this._animation = this._callback = null; + this._props.length = this._values.length = 0; + callback && callback(); +}; + +/** + * Private helpers + * *************** + */ + +function createFrame(props, prefix) { + var frame = {}; + for (var prop in props) { + frame[prefix ? prop : getUnprefixedPropName(prop)] = props[prop]; + } + return frame; +} + +/** + * Transform translateX and translateY value into CSS transform style + * property's value. + * + * @param {Number} x + * @param {Number} y + * @returns {String} + */ +function getTranslateString(x, y) { + return 'translateX(' + x + 'px) translateY(' + y + 'px)'; +} + +/** + * Drag placeholder. + * + * @class + * @param {Item} item + */ +function ItemDragPlaceholder(item) { + this._item = item; + this._animation = new Animator(); + this._element = null; + this._className = ''; + this._didMigrate = false; + this._resetAfterLayout = false; + this._left = 0; + this._top = 0; + this._transX = 0; + this._transY = 0; + this._nextTransX = 0; + this._nextTransY = 0; + + // Bind animation handlers. + this._setupAnimation = this._setupAnimation.bind(this); + this._startAnimation = this._startAnimation.bind(this); + this._updateDimensions = this._updateDimensions.bind(this); + + // Bind event handlers. + this._onLayoutStart = this._onLayoutStart.bind(this); + this._onLayoutEnd = this._onLayoutEnd.bind(this); + this._onReleaseEnd = this._onReleaseEnd.bind(this); + this._onMigrate = this._onMigrate.bind(this); + this._onHide = this._onHide.bind(this); +} + +/** + * Private prototype methods + * ************************* + */ + +/** + * Update placeholder's dimensions to match the item's dimensions. + * + * @private + */ +ItemDragPlaceholder.prototype._updateDimensions = function () { + if (!this.isActive()) return; + setStyles(this._element, { + width: this._item._width + 'px', + height: this._item._height + 'px', + }); +}; + +/** + * Move placeholder to a new position. + * + * @private + * @param {Item[]} items + * @param {Boolean} isInstant + */ +ItemDragPlaceholder.prototype._onLayoutStart = function (items, isInstant) { + var item = this._item; + + // If the item is not part of the layout anymore reset placeholder. + if (items.indexOf(item) === -1) { + this.reset(); + return; + } + + var nextLeft = item._left; + var nextTop = item._top; + var currentLeft = this._left; + var currentTop = this._top; + + // Keep track of item layout position. + this._left = nextLeft; + this._top = nextTop; + + // If item's position did not change, and the item did not migrate and the + // layout is not instant and we can safely skip layout. + if (!isInstant && !this._didMigrate && currentLeft === nextLeft && currentTop === nextTop) { + return; + } + + // Slots data is calculated with item margins added to them so we need to add + // item's left and top margin to the slot data to get the placeholder's + // next position. + var nextX = nextLeft + item._marginLeft; + var nextY = nextTop + item._marginTop; + + // Just snap to new position without any animations if no animation is + // required or if placeholder moves between grids. + var grid = item.getGrid(); + var animEnabled = !isInstant && grid._settings.layoutDuration > 0; + if (!animEnabled || this._didMigrate) { + // Cancel potential (queued) layout tick. + cancelPlaceholderLayoutTick(item._id); + + // Snap placeholder to correct position. + this._element.style[transformProp] = getTranslateString(nextX, nextY); + this._animation.stop(); + + // Move placeholder inside correct container after migration. + if (this._didMigrate) { + grid.getElement().appendChild(this._element); + this._didMigrate = false; + } + + return; + } + + // Start the placeholder's layout animation in the next tick. We do this to + // avoid layout thrashing. + this._nextTransX = nextX; + this._nextTransY = nextY; + addPlaceholderLayoutTick(item._id, this._setupAnimation, this._startAnimation); +}; + +/** + * Prepare placeholder for layout animation. + * + * @private + */ +ItemDragPlaceholder.prototype._setupAnimation = function () { + if (!this.isActive()) return; + + var translate = getTranslate(this._element); + this._transX = translate.x; + this._transY = translate.y; +}; + +/** + * Start layout animation. + * + * @private + */ +ItemDragPlaceholder.prototype._startAnimation = function () { + if (!this.isActive()) return; + + var animation = this._animation; + var currentX = this._transX; + var currentY = this._transY; + var nextX = this._nextTransX; + var nextY = this._nextTransY; + + // If placeholder is already in correct position let's just stop animation + // and be done with it. + if (currentX === nextX && currentY === nextY) { + if (animation.isAnimating()) { + this._element.style[transformProp] = getTranslateString(nextX, nextY); + animation.stop(); + } + return; + } + + // Otherwise let's start the animation. + var settings = this._item.getGrid()._settings; + var currentStyles = {}; + var targetStyles = {}; + currentStyles[transformProp] = getTranslateString(currentX, currentY); + targetStyles[transformProp] = getTranslateString(nextX, nextY); + animation.start(currentStyles, targetStyles, { + duration: settings.layoutDuration, + easing: settings.layoutEasing, + onFinish: this._onLayoutEnd, + }); +}; + +/** + * Layout end handler. + * + * @private + */ +ItemDragPlaceholder.prototype._onLayoutEnd = function () { + if (this._resetAfterLayout) { + this.reset(); + } +}; + +/** + * Drag end handler. This handler is called when dragReleaseEnd event is + * emitted and receives the event data as it's argument. + * + * @private + * @param {Item} item + */ +ItemDragPlaceholder.prototype._onReleaseEnd = function (item) { + if (item._id === this._item._id) { + // If the placeholder is not animating anymore we can safely reset it. + if (!this._animation.isAnimating()) { + this.reset(); + return; + } + + // If the placeholder item is still animating here, let's wait for it to + // finish it's animation. + this._resetAfterLayout = true; + } +}; + +/** + * Migration start handler. This handler is called when beforeSend event is + * emitted and receives the event data as it's argument. + * + * @private + * @param {Object} data + * @param {Item} data.item + * @param {Grid} data.fromGrid + * @param {Number} data.fromIndex + * @param {Grid} data.toGrid + * @param {Number} data.toIndex + */ +ItemDragPlaceholder.prototype._onMigrate = function (data) { + // Make sure we have a matching item. + if (data.item !== this._item) return; + + var grid = this._item.getGrid(); + var nextGrid = data.toGrid; + + // Unbind listeners from current grid. + grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + grid.off(EVENT_LAYOUT_START, this._onLayoutStart); + grid.off(EVENT_BEFORE_SEND, this._onMigrate); + grid.off(EVENT_HIDE_START, this._onHide); + + // Bind listeners to the next grid. + nextGrid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + nextGrid.on(EVENT_LAYOUT_START, this._onLayoutStart); + nextGrid.on(EVENT_BEFORE_SEND, this._onMigrate); + nextGrid.on(EVENT_HIDE_START, this._onHide); + + // Mark the item as migrated. + this._didMigrate = true; +}; + +/** + * Reset placeholder if the associated item is hidden. + * + * @private + * @param {Item[]} items + */ +ItemDragPlaceholder.prototype._onHide = function (items) { + if (items.indexOf(this._item) > -1) this.reset(); +}; + +/** + * Public prototype methods + * ************************ + */ + +/** + * Create placeholder. Note that this method only writes to DOM and does not + * read anything from DOM so it should not cause any additional layout + * thrashing when it's called at the end of the drag start procedure. + * + * @public + */ +ItemDragPlaceholder.prototype.create = function () { + // If we already have placeholder set up we can skip the initiation logic. + if (this.isActive()) { + this._resetAfterLayout = false; + return; + } + + var item = this._item; + var grid = item.getGrid(); + var settings = grid._settings; + var animation = this._animation; + + // Keep track of layout position. + this._left = item._left; + this._top = item._top; + + // Create placeholder element. + var element; + if (isFunction(settings.dragPlaceholder.createElement)) { + element = settings.dragPlaceholder.createElement(item); + } else { + element = document.createElement('div'); + } + this._element = element; + + // Update element to animation instance. + animation._element = element; + + // Add placeholder class to the placeholder element. + this._className = settings.itemPlaceholderClass || ''; + if (this._className) { + addClass(element, this._className); + } + + // Set initial styles. + setStyles(element, { + position: 'absolute', + left: '0px', + top: '0px', + width: item._width + 'px', + height: item._height + 'px', + }); + + // Set initial position. + element.style[transformProp] = getTranslateString( + item._left + item._marginLeft, + item._top + item._marginTop + ); + + // Bind event listeners. + grid.on(EVENT_LAYOUT_START, this._onLayoutStart); + grid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + grid.on(EVENT_BEFORE_SEND, this._onMigrate); + grid.on(EVENT_HIDE_START, this._onHide); + + // onCreate hook. + if (isFunction(settings.dragPlaceholder.onCreate)) { + settings.dragPlaceholder.onCreate(item, element); + } + + // Insert the placeholder element to the grid. + grid.getElement().appendChild(element); +}; + +/** + * Reset placeholder data. + * + * @public + */ +ItemDragPlaceholder.prototype.reset = function () { + if (!this.isActive()) return; + + var element = this._element; + var item = this._item; + var grid = item.getGrid(); + var settings = grid._settings; + var animation = this._animation; + + // Reset flag. + this._resetAfterLayout = false; + + // Cancel potential (queued) layout tick. + cancelPlaceholderLayoutTick(item._id); + cancelPlaceholderResizeTick(item._id); + + // Reset animation instance. + animation.stop(); + animation._element = null; + + // Unbind event listeners. + grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + grid.off(EVENT_LAYOUT_START, this._onLayoutStart); + grid.off(EVENT_BEFORE_SEND, this._onMigrate); + grid.off(EVENT_HIDE_START, this._onHide); + + // Remove placeholder class from the placeholder element. + if (this._className) { + removeClass(element, this._className); + this._className = ''; + } + + // Remove element. + element.parentNode.removeChild(element); + this._element = null; + + // onRemove hook. Note that here we use the current grid's onRemove callback + // so if the item has migrated during drag the onRemove method will not be + // the originating grid's method. + if (isFunction(settings.dragPlaceholder.onRemove)) { + settings.dragPlaceholder.onRemove(item, element); + } +}; + +/** + * Check if placeholder is currently active (visible). + * + * @public + * @returns {Boolean} + */ +ItemDragPlaceholder.prototype.isActive = function () { + return !!this._element; +}; + +/** + * Get placeholder element. + * + * @public + * @returns {?HTMLElement} + */ +ItemDragPlaceholder.prototype.getElement = function () { + return this._element; +}; + +/** + * Update placeholder's dimensions to match the item's dimensions. Note that + * the updating is done asynchronously in the next tick to avoid layout + * thrashing. + * + * @public + */ +ItemDragPlaceholder.prototype.updateDimensions = function () { + if (!this.isActive()) return; + addPlaceholderResizeTick(this._item._id, this._updateDimensions); +}; + +/** + * Destroy placeholder instance. + * + * @public + */ +ItemDragPlaceholder.prototype.destroy = function () { + this.reset(); + this._animation.destroy(); + this._item = this._animation = null; +}; + +/** + * The release process handler constructor. Although this might seem as proper + * fit for the drag process this needs to be separated into it's own logic + * because there might be a scenario where drag is disabled, but the release + * process still needs to be implemented (dragging from a grid to another). + * + * @class + * @param {Item} item + */ +function ItemDragRelease(item) { + this._item = item; + this._isActive = false; + this._isDestroyed = false; + this._isPositioningStarted = false; + this._containerDiffX = 0; + this._containerDiffY = 0; +} + +/** + * Public prototype methods + * ************************ + */ + +/** + * Start the release process of an item. + * + * @public + */ +ItemDragRelease.prototype.start = function () { + if (this._isDestroyed || this._isActive) return; + + var item = this._item; + var grid = item.getGrid(); + var settings = grid._settings; + + this._isActive = true; + addClass(item._element, settings.itemReleasingClass); + if (!settings.dragRelease.useDragContainer) { + this._placeToGrid(); + } + grid._emit(EVENT_DRAG_RELEASE_START, item); + + // Let's start layout manually _only_ if there is no unfinished layout in + // about to finish. + if (!grid._nextLayoutData) item._layout.start(false); +}; + +/** + * End the release process of an item. This method can be used to abort an + * ongoing release process (animation) or finish the release process. + * + * @public + * @param {Boolean} [abort=false] + * - Should the release be aborted? When true, the release end event won't be + * emitted. Set to true only when you need to abort the release process + * while the item is animating to it's position. + * @param {Number} [left] + * - The element's current translateX value (optional). + * @param {Number} [top] + * - The element's current translateY value (optional). + */ +ItemDragRelease.prototype.stop = function (abort, left, top) { + if (this._isDestroyed || !this._isActive) return; + + var item = this._item; + var grid = item.getGrid(); + + if (!abort && (left === undefined || top === undefined)) { + left = item._left; + top = item._top; + } + + var didReparent = this._placeToGrid(left, top); + this._reset(didReparent); + + if (!abort) grid._emit(EVENT_DRAG_RELEASE_END, item); +}; + +ItemDragRelease.prototype.isJustReleased = function () { + return this._isActive && this._isPositioningStarted === false; +}; + +/** + * Destroy instance. + * + * @public + */ +ItemDragRelease.prototype.destroy = function () { + if (this._isDestroyed) return; + this.stop(true); + this._item = null; + this._isDestroyed = true; +}; + +/** + * Private prototype methods + * ************************* + */ + +/** + * Move the element back to the grid container element if it does not exist + * there already. + * + * @private + * @param {Number} [left] + * - The element's current translateX value (optional). + * @param {Number} [top] + * - The element's current translateY value (optional). + * @returns {Boolean} + * - Returns `true` if the element was reparented. + */ +ItemDragRelease.prototype._placeToGrid = function (left, top) { + if (this._isDestroyed) return; + + var item = this._item; + var element = item._element; + var container = item.getGrid()._element; + var didReparent = false; + + if (element.parentNode !== container) { + if (left === undefined || top === undefined) { + var translate = getTranslate(element); + left = translate.x - this._containerDiffX; + top = translate.y - this._containerDiffY; + } + + container.appendChild(element); + item._setTranslate(left, top); + didReparent = true; + } + + this._containerDiffX = 0; + this._containerDiffY = 0; + + return didReparent; +}; + +/** + * Reset data and remove releasing class. + * + * @private + * @param {Boolean} [needsReflow] + */ +ItemDragRelease.prototype._reset = function (needsReflow) { + if (this._isDestroyed) return; + + var item = this._item; + var releasingClass = item.getGrid()._settings.itemReleasingClass; + + this._isActive = false; + this._isPositioningStarted = false; + this._containerDiffX = 0; + this._containerDiffY = 0; + + // If the element was just reparented we need to do a forced reflow to remove + // the class gracefully. + if (releasingClass) { + // eslint-disable-next-line + if (needsReflow) item._element.clientWidth; + removeClass(item._element, releasingClass); + } +}; + +var MIN_ANIMATION_DISTANCE = 2; + +/** + * Layout manager for Item instance, handles the positioning of an item. + * + * @class + * @param {Item} item + */ +function ItemLayout(item) { + var element = item._element; + var elementStyle = element.style; + + this._item = item; + this._isActive = false; + this._isDestroyed = false; + this._isInterrupted = false; + this._currentStyles = {}; + this._targetStyles = {}; + this._nextLeft = 0; + this._nextTop = 0; + this._offsetLeft = 0; + this._offsetTop = 0; + this._skipNextAnimation = false; + this._animOptions = { + onFinish: this._finish.bind(this), + duration: 0, + easing: 0, + }; + + // Set element's initial position styles. + elementStyle.left = '0px'; + elementStyle.top = '0px'; + item._setTranslate(0, 0); + + this._animation = new Animator(element); + this._queue = 'layout-' + item._id; + + // Bind animation handlers and finish method. + this._setupAnimation = this._setupAnimation.bind(this); + this._startAnimation = this._startAnimation.bind(this); +} + +/** + * Public prototype methods + * ************************ + */ + +/** + * Start item layout based on it's current data. + * + * @public + * @param {Boolean} instant + * @param {Function} [onFinish] + */ +ItemLayout.prototype.start = function (instant, onFinish) { + if (this._isDestroyed) return; + + var item = this._item; + var release = item._dragRelease; + var gridSettings = item.getGrid()._settings; + var isPositioning = this._isActive; + var isJustReleased = release.isJustReleased(); + var animDuration = isJustReleased + ? gridSettings.dragRelease.duration + : gridSettings.layoutDuration; + var animEasing = isJustReleased ? gridSettings.dragRelease.easing : gridSettings.layoutEasing; + var animEnabled = !instant && !this._skipNextAnimation && animDuration > 0; + + // If the item is currently positioning cancel potential queued layout tick + // and process current layout callback queue with interrupted flag on. + if (isPositioning) { + cancelLayoutTick(item._id); + item._emitter.burst(this._queue, true, item); + } + + // Mark release positioning as started. + if (isJustReleased) release._isPositioningStarted = true; + + // Push the callback to the callback queue. + if (isFunction(onFinish)) { + item._emitter.on(this._queue, onFinish); + } + + // Reset animation skipping flag. + this._skipNextAnimation = false; + + // If no animations are needed, easy peasy! + if (!animEnabled) { + this._updateOffsets(); + item._setTranslate(this._nextLeft, this._nextTop); + this._animation.stop(); + this._finish(); + return; + } + + // Kick off animation to be started in the next tick. + this._isActive = true; + this._animOptions.easing = animEasing; + this._animOptions.duration = animDuration; + this._isInterrupted = isPositioning; + addLayoutTick(item._id, this._setupAnimation, this._startAnimation); +}; + +/** + * Stop item's position animation if it is currently animating. + * + * @public + * @param {Boolean} processCallbackQueue + * @param {Number} [left] + * @param {Number} [top] + */ +ItemLayout.prototype.stop = function (processCallbackQueue, left, top) { + if (this._isDestroyed || !this._isActive) return; + + var item = this._item; + + // Cancel animation init. + cancelLayoutTick(item._id); + + // Stop animation. + if (this._animation.isAnimating()) { + if (left === undefined || top === undefined) { + var translate = getTranslate(item._element); + left = translate.x; + top = translate.y; + } + item._setTranslate(left, top); + this._animation.stop(); + } + + // Remove positioning class. + removeClass(item._element, item.getGrid()._settings.itemPositioningClass); + + // Reset active state. + this._isActive = false; + + // Process callback queue if needed. + if (processCallbackQueue) { + item._emitter.burst(this._queue, true, item); + } +}; + +/** + * Destroy the instance and stop current animation if it is running. + * + * @public + */ +ItemLayout.prototype.destroy = function () { + if (this._isDestroyed) return; + + var elementStyle = this._item._element.style; + + this.stop(true, 0, 0); + this._item._emitter.clear(this._queue); + this._animation.destroy(); + + elementStyle[transformProp] = ''; + elementStyle.left = ''; + elementStyle.top = ''; + + this._item = null; + this._currentStyles = null; + this._targetStyles = null; + this._animOptions = null; + this._isDestroyed = true; +}; + +/** + * Private prototype methods + * ************************* + */ + +/** + * Calculate and update item's current layout offset data. + * + * @private + */ +ItemLayout.prototype._updateOffsets = function () { + if (this._isDestroyed) return; + + var item = this._item; + var migrate = item._migrate; + var release = item._dragRelease; + + this._offsetLeft = release._isActive + ? release._containerDiffX + : migrate._isActive + ? migrate._containerDiffX + : 0; + + this._offsetTop = release._isActive + ? release._containerDiffY + : migrate._isActive + ? migrate._containerDiffY + : 0; + + this._nextLeft = this._item._left + this._offsetLeft; + this._nextTop = this._item._top + this._offsetTop; +}; + +/** + * Finish item layout procedure. + * + * @private + */ +ItemLayout.prototype._finish = function () { + if (this._isDestroyed) return; + + var item = this._item; + var migrate = item._migrate; + var release = item._dragRelease; + + // Update internal translate values. + item._tX = this._nextLeft; + item._tY = this._nextTop; + + // Mark the item as inactive and remove positioning classes. + if (this._isActive) { + this._isActive = false; + removeClass(item._element, item.getGrid()._settings.itemPositioningClass); + } + + // Finish up release and migration. + if (release._isActive) release.stop(); + if (migrate._isActive) migrate.stop(); + + // Process the callback queue. + item._emitter.burst(this._queue, false, item); +}; + +/** + * Prepare item for layout animation. + * + * @private + */ +ItemLayout.prototype._setupAnimation = function () { + var item = this._item; + if (item._tX === undefined || item._tY === undefined) { + var translate = getTranslate(item._element); + item._tX = translate.x; + item._tY = translate.y; + } +}; + +/** + * Start layout animation. + * + * @private + */ +ItemLayout.prototype._startAnimation = function () { + var item = this._item; + var settings = item.getGrid()._settings; + var isInstant = this._animOptions.duration <= 0; + + // Let's update the offset data and target styles. + this._updateOffsets(); + + var xDiff = Math.abs(item._left - (item._tX - this._offsetLeft)); + var yDiff = Math.abs(item._top - (item._tY - this._offsetTop)); + + // If there is no need for animation or if the item is already in correct + // position (or near it) let's finish the process early. + if (isInstant || (xDiff < MIN_ANIMATION_DISTANCE && yDiff < MIN_ANIMATION_DISTANCE)) { + if (xDiff || yDiff || this._isInterrupted) { + item._setTranslate(this._nextLeft, this._nextTop); + } + this._animation.stop(); + this._finish(); + return; + } + + // Set item's positioning class if needed. + if (!this._isInterrupted) { + addClass(item._element, settings.itemPositioningClass); + } + + // Get current/next styles for animation. + this._currentStyles[transformProp] = getTranslateString(item._tX, item._tY); + this._targetStyles[transformProp] = getTranslateString(this._nextLeft, this._nextTop); + + // Set internal translation values to undefined for the duration of the + // animation since they will be changing on each animation frame for the + // duration of the animation and tracking them would mean reading the DOM on + // each frame, which is pretty darn expensive. + item._tX = item._tY = undefined; + + // Start animation. + this._animation.start(this._currentStyles, this._targetStyles, this._animOptions); +}; + +/** + * The migrate process handler constructor. + * + * @class + * @param {Item} item + */ +function ItemMigrate(item) { + // Private props. + this._item = item; + this._isActive = false; + this._isDestroyed = false; + this._container = false; + this._containerDiffX = 0; + this._containerDiffY = 0; +} + +/** + * Public prototype methods + * ************************ + */ + +/** + * Start the migrate process of an item. + * + * @public + * @param {Grid} targetGrid + * @param {(HTMLElement|Number|Item)} position + * @param {HTMLElement} [container] + */ +ItemMigrate.prototype.start = function (targetGrid, position, container) { + if (this._isDestroyed) return; + + var item = this._item; + var element = item._element; + var isActive = item.isActive(); + var isVisible = item.isVisible(); + var grid = item.getGrid(); + var settings = grid._settings; + var targetSettings = targetGrid._settings; + var targetElement = targetGrid._element; + var targetItems = targetGrid._items; + var currentIndex = grid._items.indexOf(item); + var targetContainer = container || document.body; + var targetIndex; + var targetItem; + var currentContainer; + var offsetDiff; + var containerDiff; + var translate; + var translateX; + var translateY; + var currentVisClass; + var nextVisClass; + + // Get target index. + if (typeof position === 'number') { + targetIndex = normalizeArrayIndex(targetItems, position, 1); + } else { + targetItem = targetGrid.getItem(position); + if (!targetItem) return; + targetIndex = targetItems.indexOf(targetItem); + } + + // Get current translateX and translateY values if needed. + if (item.isPositioning() || this._isActive || item.isReleasing()) { + translate = getTranslate(element); + translateX = translate.x; + translateY = translate.y; + } + + // Abort current positioning. + if (item.isPositioning()) { + item._layout.stop(true, translateX, translateY); + } + + // Abort current migration. + if (this._isActive) { + translateX -= this._containerDiffX; + translateY -= this._containerDiffY; + this.stop(true, translateX, translateY); + } + + // Abort current release. + if (item.isReleasing()) { + translateX -= item._dragRelease._containerDiffX; + translateY -= item._dragRelease._containerDiffY; + item._dragRelease.stop(true, translateX, translateY); + } + + // Stop current visibility animation. + item._visibility.stop(true); + + // Destroy current drag. + if (item._drag) item._drag.destroy(); + + // Emit beforeSend event. + if (grid._hasListeners(EVENT_BEFORE_SEND)) { + grid._emit(EVENT_BEFORE_SEND, { + item: item, + fromGrid: grid, + fromIndex: currentIndex, + toGrid: targetGrid, + toIndex: targetIndex, + }); + } + + // Emit beforeReceive event. + if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) { + targetGrid._emit(EVENT_BEFORE_RECEIVE, { + item: item, + fromGrid: grid, + fromIndex: currentIndex, + toGrid: targetGrid, + toIndex: targetIndex, + }); + } + + // Update item class. + if (settings.itemClass !== targetSettings.itemClass) { + removeClass(element, settings.itemClass); + addClass(element, targetSettings.itemClass); + } + + // Update visibility class. + currentVisClass = isVisible ? settings.itemVisibleClass : settings.itemHiddenClass; + nextVisClass = isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass; + if (currentVisClass !== nextVisClass) { + removeClass(element, currentVisClass); + addClass(element, nextVisClass); + } + + // Move item instance from current grid to target grid. + grid._items.splice(currentIndex, 1); + arrayInsert(targetItems, item, targetIndex); + + // Update item's grid id reference. + item._gridId = targetGrid._id; + + // If item is active we need to move the item inside the target container for + // the duration of the (potential) animation if it's different than the + // current container. + if (isActive) { + currentContainer = element.parentNode; + if (targetContainer !== currentContainer) { + targetContainer.appendChild(element); + offsetDiff = getOffsetDiff(targetContainer, currentContainer, true); + if (!translate) { + translate = getTranslate(element); + translateX = translate.x; + translateY = translate.y; + } + item._setTranslate(translateX + offsetDiff.left, translateY + offsetDiff.top); + } + } + // If item is not active let's just append it to the target grid's element. + else { + targetElement.appendChild(element); + } + + // Update child element's styles to reflect the current visibility state. + item._visibility.setStyles( + isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles + ); + + // Get offset diff for the migration data, if the item is active. + if (isActive) { + containerDiff = getOffsetDiff(targetContainer, targetElement, true); + } + + // Update item's cached dimensions. + item._refreshDimensions(); + + // Reset item's sort data. + item._sortData = null; + + // Create new drag handler. + item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null; + + // Setup migration data. + if (isActive) { + this._isActive = true; + this._container = targetContainer; + this._containerDiffX = containerDiff.left; + this._containerDiffY = containerDiff.top; + } else { + this._isActive = false; + this._container = null; + this._containerDiffX = 0; + this._containerDiffY = 0; + } + + // Emit send event. + if (grid._hasListeners(EVENT_SEND)) { + grid._emit(EVENT_SEND, { + item: item, + fromGrid: grid, + fromIndex: currentIndex, + toGrid: targetGrid, + toIndex: targetIndex, + }); + } + + // Emit receive event. + if (targetGrid._hasListeners(EVENT_RECEIVE)) { + targetGrid._emit(EVENT_RECEIVE, { + item: item, + fromGrid: grid, + fromIndex: currentIndex, + toGrid: targetGrid, + toIndex: targetIndex, + }); + } +}; + +/** + * End the migrate process of an item. This method can be used to abort an + * ongoing migrate process (animation) or finish the migrate process. + * + * @public + * @param {Boolean} [abort=false] + * - Should the migration be aborted? + * @param {Number} [left] + * - The element's current translateX value (optional). + * @param {Number} [top] + * - The element's current translateY value (optional). + */ +ItemMigrate.prototype.stop = function (abort, left, top) { + if (this._isDestroyed || !this._isActive) return; + + var item = this._item; + var element = item._element; + var grid = item.getGrid(); + var gridElement = grid._element; + var translate; + + if (this._container !== gridElement) { + if (left === undefined || top === undefined) { + if (abort) { + translate = getTranslate(element); + left = translate.x - this._containerDiffX; + top = translate.y - this._containerDiffY; + } else { + left = item._left; + top = item._top; + } + } + + gridElement.appendChild(element); + item._setTranslate(left, top); + } + + this._isActive = false; + this._container = null; + this._containerDiffX = 0; + this._containerDiffY = 0; +}; + +/** + * Destroy instance. + * + * @public + */ +ItemMigrate.prototype.destroy = function () { + if (this._isDestroyed) return; + this.stop(true); + this._item = null; + this._isDestroyed = true; +}; + +/** + * Visibility manager for Item instance, handles visibility of an item. + * + * @class + * @param {Item} item + */ +function ItemVisibility(item) { + var isActive = item._isActive; + var element = item._element; + var childElement = element.children[0]; + var settings = item.getGrid()._settings; + + if (!childElement) { + throw new Error('No valid child element found within item element.'); + } + + this._item = item; + this._isDestroyed = false; + this._isHidden = !isActive; + this._isHiding = false; + this._isShowing = false; + this._childElement = childElement; + this._currentStyleProps = []; + this._animation = new Animator(childElement); + this._queue = 'visibility-' + item._id; + this._finishShow = this._finishShow.bind(this); + this._finishHide = this._finishHide.bind(this); + + element.style.display = isActive ? '' : 'none'; + addClass(element, isActive ? settings.itemVisibleClass : settings.itemHiddenClass); + this.setStyles(isActive ? settings.visibleStyles : settings.hiddenStyles); +} + +/** + * Public prototype methods + * ************************ + */ + +/** + * Show item. + * + * @public + * @param {Boolean} instant + * @param {Function} [onFinish] + */ +ItemVisibility.prototype.show = function (instant, onFinish) { + if (this._isDestroyed) return; + + var item = this._item; + var element = item._element; + var callback = isFunction(onFinish) ? onFinish : null; + var grid = item.getGrid(); + var settings = grid._settings; + + // If item is visible call the callback and be done with it. + if (!this._isShowing && !this._isHidden) { + callback && callback(false, item); + return; + } + + // If item is showing and does not need to be shown instantly, let's just + // push callback to the callback queue and be done with it. + if (this._isShowing && !instant) { + callback && item._emitter.on(this._queue, callback); + return; + } + + // If the item is hiding or hidden process the current visibility callback + // queue with the interrupted flag active, update classes and set display + // to block if necessary. + if (!this._isShowing) { + item._emitter.burst(this._queue, true, item); + removeClass(element, settings.itemHiddenClass); + addClass(element, settings.itemVisibleClass); + if (!this._isHiding) element.style.display = ''; + } + + // Push callback to the callback queue. + callback && item._emitter.on(this._queue, callback); + + // Update visibility states. + this._isShowing = true; + this._isHiding = this._isHidden = false; + + // Finally let's start show animation. + this._startAnimation(true, instant, this._finishShow); +}; + +/** + * Hide item. + * + * @public + * @param {Boolean} instant + * @param {Function} [onFinish] + */ +ItemVisibility.prototype.hide = function (instant, onFinish) { + if (this._isDestroyed) return; + + var item = this._item; + var element = item._element; + var callback = isFunction(onFinish) ? onFinish : null; + var grid = item.getGrid(); + var settings = grid._settings; + + // If item is already hidden call the callback and be done with it. + if (!this._isHiding && this._isHidden) { + callback && callback(false, item); + return; + } + + // If item is hiding and does not need to be hidden instantly, let's just + // push callback to the callback queue and be done with it. + if (this._isHiding && !instant) { + callback && item._emitter.on(this._queue, callback); + return; + } + + // If the item is showing or visible process the current visibility callback + // queue with the interrupted flag active, update classes and set display + // to block if necessary. + if (!this._isHiding) { + item._emitter.burst(this._queue, true, item); + addClass(element, settings.itemHiddenClass); + removeClass(element, settings.itemVisibleClass); + } + + // Push callback to the callback queue. + callback && item._emitter.on(this._queue, callback); + + // Update visibility states. + this._isHidden = this._isHiding = true; + this._isShowing = false; + + // Finally let's start hide animation. + this._startAnimation(false, instant, this._finishHide); +}; + +/** + * Stop current hiding/showing process. + * + * @public + * @param {Boolean} processCallbackQueue + */ +ItemVisibility.prototype.stop = function (processCallbackQueue) { + if (this._isDestroyed) return; + if (!this._isHiding && !this._isShowing) return; + + var item = this._item; + + cancelVisibilityTick(item._id); + this._animation.stop(); + if (processCallbackQueue) { + item._emitter.burst(this._queue, true, item); + } +}; + +/** + * Reset all existing visibility styles and apply new visibility styles to the + * visibility element. This method should be used to set styles when there is a + * chance that the current style properties differ from the new ones (basically + * on init and on migrations). + * + * @public + * @param {Object} styles + */ +ItemVisibility.prototype.setStyles = function (styles) { + var childElement = this._childElement; + var currentStyleProps = this._currentStyleProps; + this._removeCurrentStyles(); + for (var prop in styles) { + currentStyleProps.push(prop); + childElement.style[prop] = styles[prop]; + } +}; + +/** + * Destroy the instance and stop current animation if it is running. + * + * @public + */ +ItemVisibility.prototype.destroy = function () { + if (this._isDestroyed) return; + + var item = this._item; + var element = item._element; + var grid = item.getGrid(); + var settings = grid._settings; + + this.stop(true); + item._emitter.clear(this._queue); + this._animation.destroy(); + this._removeCurrentStyles(); + removeClass(element, settings.itemVisibleClass); + removeClass(element, settings.itemHiddenClass); + element.style.display = ''; + + // Reset state. + this._isHiding = this._isShowing = false; + this._isDestroyed = this._isHidden = true; +}; + +/** + * Private prototype methods + * ************************* + */ + +/** + * Start visibility animation. + * + * @private + * @param {Boolean} toVisible + * @param {Boolean} [instant] + * @param {Function} [onFinish] + */ +ItemVisibility.prototype._startAnimation = function (toVisible, instant, onFinish) { + if (this._isDestroyed) return; + + var item = this._item; + var animation = this._animation; + var childElement = this._childElement; + var settings = item.getGrid()._settings; + var targetStyles = toVisible ? settings.visibleStyles : settings.hiddenStyles; + var duration = toVisible ? settings.showDuration : settings.hideDuration; + var easing = toVisible ? settings.showEasing : settings.hideEasing; + var isInstant = instant || duration <= 0; + var currentStyles; + + // No target styles? Let's quit early. + if (!targetStyles) { + onFinish && onFinish(); + return; + } + + // Cancel queued visibility tick. + cancelVisibilityTick(item._id); + + // If we need to apply the styles instantly without animation. + if (isInstant) { + setStyles(childElement, targetStyles); + animation.stop(); + onFinish && onFinish(); + return; + } + + // Start the animation in the next tick (to avoid layout thrashing). + addVisibilityTick( + item._id, + function () { + currentStyles = getCurrentStyles(childElement, targetStyles); + }, + function () { + animation.start(currentStyles, targetStyles, { + duration: duration, + easing: easing, + onFinish: onFinish, + }); + } + ); +}; + +/** + * Finish show procedure. + * + * @private + */ +ItemVisibility.prototype._finishShow = function () { + if (this._isHidden) return; + this._isShowing = false; + this._item._emitter.burst(this._queue, false, this._item); +}; + +/** + * Finish hide procedure. + * + * @private + */ +ItemVisibility.prototype._finishHide = function () { + if (!this._isHidden) return; + var item = this._item; + this._isHiding = false; + item._layout.stop(true, 0, 0); + item._element.style.display = 'none'; + item._emitter.burst(this._queue, false, item); +}; + +/** + * Remove currently applied visibility related inline style properties. + * + * @private + */ +ItemVisibility.prototype._removeCurrentStyles = function () { + var childElement = this._childElement; + var currentStyleProps = this._currentStyleProps; + + for (var i = 0; i < currentStyleProps.length; i++) { + childElement.style[currentStyleProps[i]] = ''; + } + + currentStyleProps.length = 0; +}; + +var id = 0; + +/** + * Returns a unique numeric id (increments a base value on every call). + * @returns {Number} + */ +function createUid() { + return ++id; +} + +/** + * Creates a new Item instance for a Grid instance. + * + * @class + * @param {Grid} grid + * @param {HTMLElement} element + * @param {Boolean} [isActive] + */ +function Item(grid, element, isActive) { + var settings = grid._settings; + + // Store item/element pair to a map (for faster item querying by element). + if (ITEM_ELEMENT_MAP) { + if (ITEM_ELEMENT_MAP.has(element)) { + throw new Error('You can only create one Muuri Item per element!'); + } else { + ITEM_ELEMENT_MAP.set(element, this); + } + } + + this._id = createUid(); + this._gridId = grid._id; + this._element = element; + this._isDestroyed = false; + this._left = 0; + this._top = 0; + this._width = 0; + this._height = 0; + this._marginLeft = 0; + this._marginRight = 0; + this._marginTop = 0; + this._marginBottom = 0; + this._tX = undefined; + this._tY = undefined; + this._sortData = null; + this._emitter = new Emitter(); + + // If the provided item element is not a direct child of the grid container + // element, append it to the grid container. Note, we are indeed reading the + // DOM here but it's a property that does not cause reflowing. + if (element.parentNode !== grid._element) { + grid._element.appendChild(element); + } + + // Set item class. + addClass(element, settings.itemClass); + + // If isActive is not defined, let's try to auto-detect it. Note, we are + // indeed reading the DOM here but it's a property that does not cause + // reflowing. + if (typeof isActive !== 'boolean') { + isActive = getStyle(element, 'display') !== 'none'; + } + + // Set up active state (defines if the item is considered part of the layout + // or not). + this._isActive = isActive; + + // Setup visibility handler. + this._visibility = new ItemVisibility(this); + + // Set up layout handler. + this._layout = new ItemLayout(this); + + // Set up migration handler data. + this._migrate = new ItemMigrate(this); + + // Set up drag handler. + this._drag = settings.dragEnabled ? new ItemDrag(this) : null; + + // Set up release handler. Note that although this is fully linked to dragging + // this still needs to be always instantiated to handle migration scenarios + // correctly. + this._dragRelease = new ItemDragRelease(this); + + // Set up drag placeholder handler. Note that although this is fully linked to + // dragging this still needs to be always instantiated to handle migration + // scenarios correctly. + this._dragPlaceholder = new ItemDragPlaceholder(this); + + // Note! You must call the following methods before you start using the + // instance. They are deliberately not called in the end as it would cause + // potentially a massive amount of reflows if multiple items were instantiated + // in a loop. + // this._refreshDimensions(); + // this._refreshSortData(); +} + +/** + * Public prototype methods + * ************************ + */ + +/** + * Get the instance grid reference. + * + * @public + * @returns {Grid} + */ +Item.prototype.getGrid = function () { + return GRID_INSTANCES[this._gridId]; +}; + +/** + * Get the instance element. + * + * @public + * @returns {HTMLElement} + */ +Item.prototype.getElement = function () { + return this._element; +}; + +/** + * Get instance element's cached width. + * + * @public + * @returns {Number} + */ +Item.prototype.getWidth = function () { + return this._width; +}; + +/** + * Get instance element's cached height. + * + * @public + * @returns {Number} + */ +Item.prototype.getHeight = function () { + return this._height; +}; + +/** + * Get instance element's cached margins. + * + * @public + * @returns {Object} + * - The returned object contains left, right, top and bottom properties + * which indicate the item element's cached margins. + */ +Item.prototype.getMargin = function () { + return { + left: this._marginLeft, + right: this._marginRight, + top: this._marginTop, + bottom: this._marginBottom, + }; +}; + +/** + * Get instance element's cached position. + * + * @public + * @returns {Object} + * - The returned object contains left and top properties which indicate the + * item element's cached position in the grid. + */ +Item.prototype.getPosition = function () { + return { + left: this._left, + top: this._top, + }; +}; + +/** + * Is the item active? + * + * @public + * @returns {Boolean} + */ +Item.prototype.isActive = function () { + return this._isActive; +}; + +/** + * Is the item visible? + * + * @public + * @returns {Boolean} + */ +Item.prototype.isVisible = function () { + return !!this._visibility && !this._visibility._isHidden; +}; + +/** + * Is the item being animated to visible? + * + * @public + * @returns {Boolean} + */ +Item.prototype.isShowing = function () { + return !!(this._visibility && this._visibility._isShowing); +}; + +/** + * Is the item being animated to hidden? + * + * @public + * @returns {Boolean} + */ +Item.prototype.isHiding = function () { + return !!(this._visibility && this._visibility._isHiding); +}; + +/** + * Is the item positioning? + * + * @public + * @returns {Boolean} + */ +Item.prototype.isPositioning = function () { + return !!(this._layout && this._layout._isActive); +}; + +/** + * Is the item being dragged (or queued for dragging)? + * + * @public + * @returns {Boolean} + */ +Item.prototype.isDragging = function () { + return !!(this._drag && this._drag._isActive); +}; + +/** + * Is the item being released? + * + * @public + * @returns {Boolean} + */ +Item.prototype.isReleasing = function () { + return !!(this._dragRelease && this._dragRelease._isActive); +}; + +/** + * Is the item destroyed? + * + * @public + * @returns {Boolean} + */ +Item.prototype.isDestroyed = function () { + return this._isDestroyed; +}; + +/** + * Private prototype methods + * ************************* + */ + +/** + * Recalculate item's dimensions. + * + * @private + * @param {Boolean} [force=false] + */ +Item.prototype._refreshDimensions = function (force) { + if (this._isDestroyed) return; + if (force !== true && this._visibility._isHidden) return; + + var element = this._element; + var dragPlaceholder = this._dragPlaceholder; + var rect = element.getBoundingClientRect(); + + // Calculate width and height. + this._width = rect.width; + this._height = rect.height; + + // Calculate margins (ignore negative margins). + this._marginLeft = Math.max(0, getStyleAsFloat(element, 'margin-left')); + this._marginRight = Math.max(0, getStyleAsFloat(element, 'margin-right')); + this._marginTop = Math.max(0, getStyleAsFloat(element, 'margin-top')); + this._marginBottom = Math.max(0, getStyleAsFloat(element, 'margin-bottom')); + + // Keep drag placeholder's dimensions synced with the item's. + if (dragPlaceholder) dragPlaceholder.updateDimensions(); +}; + +/** + * Fetch and store item's sort data. + * + * @private + */ +Item.prototype._refreshSortData = function () { + if (this._isDestroyed) return; + + var data = (this._sortData = {}); + var getters = this.getGrid()._settings.sortData; + var prop; + + for (prop in getters) { + data[prop] = getters[prop](this, this._element); + } +}; + +/** + * Add item to layout. + * + * @private + */ +Item.prototype._addToLayout = function (left, top) { + if (this._isActive === true) return; + this._isActive = true; + this._left = left || 0; + this._top = top || 0; +}; + +/** + * Remove item from layout. + * + * @private + */ +Item.prototype._removeFromLayout = function () { + if (this._isActive === false) return; + this._isActive = false; + this._left = 0; + this._top = 0; +}; + +/** + * Check if the layout procedure can be skipped for the item. + * + * @private + * @param {Number} left + * @param {Number} top + * @returns {Boolean} + */ +Item.prototype._canSkipLayout = function (left, top) { + return ( + this._left === left && + this._top === top && + !this._migrate._isActive && + !this._layout._skipNextAnimation && + !this._dragRelease.isJustReleased() + ); +}; + +/** + * Set the provided left and top arguments as the item element's translate + * values in the DOM. This method keeps track of the currently applied + * translate values and skips the update operation if the provided values are + * identical to the currently applied values. Returns `false` if there was no + * need for update and `true` if the translate value was updated. + * + * @private + * @param {Number} left + * @param {Number} top + * @returns {Boolean} + */ +Item.prototype._setTranslate = function (left, top) { + if (this._tX === left && this._tY === top) return false; + this._tX = left; + this._tY = top; + this._element.style[transformProp] = getTranslateString(left, top); + return true; +}; + +/** + * Destroy item instance. + * + * @private + * @param {Boolean} [removeElement=false] + */ +Item.prototype._destroy = function (removeElement) { + if (this._isDestroyed) return; + + var element = this._element; + var grid = this.getGrid(); + var settings = grid._settings; + + // Destroy handlers. + this._dragPlaceholder.destroy(); + this._dragRelease.destroy(); + this._migrate.destroy(); + this._layout.destroy(); + this._visibility.destroy(); + if (this._drag) this._drag.destroy(); + + // Destroy emitter. + this._emitter.destroy(); + + // Remove item class. + removeClass(element, settings.itemClass); + + // Remove element from DOM. + if (removeElement) element.parentNode.removeChild(element); + + // Remove item/element pair from map. + if (ITEM_ELEMENT_MAP) ITEM_ELEMENT_MAP.delete(element); + + // Reset state. + this._isActive = false; + this._isDestroyed = true; +}; + +function createPackerProcessor(isWorker = false) { + var FILL_GAPS = 1; + var HORIZONTAL = 2; + var ALIGN_RIGHT = 4; + var ALIGN_BOTTOM = 8; + var ROUNDING = 16; + + var EPS = 0.001; + var MIN_SLOT_SIZE = 0.5; + + // Rounds number first to three decimal precision and then floors the result + // to two decimal precision. + // Math.floor(Math.round(number * 1000) / 10) / 100 + function roundNumber(number) { + return ((((number * 1000 + 0.5) << 0) / 10) << 0) / 100; + } + + /** + * @class + */ + function PackerProcessor() { + this.currentRects = []; + this.nextRects = []; + this.rectTarget = {}; + this.rectStore = []; + this.slotSizes = []; + this.rectId = 0; + this.slotIndex = -1; + this.slotData = { left: 0, top: 0, width: 0, height: 0 }; + this.sortRectsLeftTop = this.sortRectsLeftTop.bind(this); + this.sortRectsTopLeft = this.sortRectsTopLeft.bind(this); + } + + /** + * Takes a layout object as an argument and computes positions (slots) for the + * layout items. Also computes the final width and height of the layout. The + * provided layout object's slots array is mutated as well as the width and + * height properties. + * + * @param {Object} layout + * @param {Number} layout.width + * - The start (current) width of the layout in pixels. + * @param {Number} layout.height + * - The start (current) height of the layout in pixels. + * @param {(Item[]|Number[])} layout.items + * - List of Muuri.Item instances or a list of item dimensions + * (e.g [ item1Width, item1Height, item2Width, item2Height, ... ]). + * @param {(Array|Float32Array)} layout.slots + * - An Array/Float32Array instance which's length should equal to + * the amount of items times two. The position (width and height) of each + * item will be written into this array. + * @param {Number} settings + * - The layout's settings as bitmasks. + * @returns {Object} + */ + PackerProcessor.prototype.computeLayout = function (layout, settings) { + var items = layout.items; + var slots = layout.slots; + var fillGaps = !!(settings & FILL_GAPS); + var horizontal = !!(settings & HORIZONTAL); + var alignRight = !!(settings & ALIGN_RIGHT); + var alignBottom = !!(settings & ALIGN_BOTTOM); + var rounding = !!(settings & ROUNDING); + var isPreProcessed = typeof items[0] === 'number'; + var i, bump, item, slotWidth, slotHeight, slot; + + // No need to go further if items do not exist. + if (!items.length) return layout; + + // Compute slots for the items. + bump = isPreProcessed ? 2 : 1; + for (i = 0; i < items.length; i += bump) { + // If items are pre-processed it means that items array contains only + // the raw dimensions of the items. Otherwise we assume it is an array + // of normal Muuri items. + if (isPreProcessed) { + slotWidth = items[i]; + slotHeight = items[i + 1]; + } else { + item = items[i]; + slotWidth = item._width + item._marginLeft + item._marginRight; + slotHeight = item._height + item._marginTop + item._marginBottom; + } + + // If rounding is enabled let's round the item's width and height to + // make the layout algorithm a bit more stable. This has a performance + // cost so don't use this if not necessary. + if (rounding) { + slotWidth = roundNumber(slotWidth); + slotHeight = roundNumber(slotHeight); + } + + // Get slot data. + slot = this.computeNextSlot(layout, slotWidth, slotHeight, fillGaps, horizontal); + + // Update layout width/height. + if (horizontal) { + if (slot.left + slot.width > layout.width) { + layout.width = slot.left + slot.width; + } + } else { + if (slot.top + slot.height > layout.height) { + layout.height = slot.top + slot.height; + } + } + + // Add item slot data to layout slots. + slots[++this.slotIndex] = slot.left; + slots[++this.slotIndex] = slot.top; + + // Store the size too (for later usage) if needed. + if (alignRight || alignBottom) { + this.slotSizes.push(slot.width, slot.height); + } + } + + // If the alignment is set to right we need to adjust the results. + if (alignRight) { + for (i = 0; i < slots.length; i += 2) { + slots[i] = layout.width - (slots[i] + this.slotSizes[i]); + } + } + + // If the alignment is set to bottom we need to adjust the results. + if (alignBottom) { + for (i = 1; i < slots.length; i += 2) { + slots[i] = layout.height - (slots[i] + this.slotSizes[i]); + } + } + + // Reset stuff. + this.slotSizes.length = 0; + this.currentRects.length = 0; + this.nextRects.length = 0; + this.rectId = 0; + this.slotIndex = -1; + + return layout; + }; + + /** + * Calculate next slot in the layout. Returns a slot object with position and + * dimensions data. The returned object is reused between calls. + * + * @param {Object} layout + * @param {Number} slotWidth + * @param {Number} slotHeight + * @param {Boolean} fillGaps + * @param {Boolean} horizontal + * @returns {Object} + */ + PackerProcessor.prototype.computeNextSlot = function ( + layout, + slotWidth, + slotHeight, + fillGaps, + horizontal + ) { + var slot = this.slotData; + var currentRects = this.currentRects; + var nextRects = this.nextRects; + var ignoreCurrentRects = false; + var rect; + var rectId; + var shards; + var i; + var j; + + // Reset new slots. + nextRects.length = 0; + + // Set item slot initial data. + slot.left = null; + slot.top = null; + slot.width = slotWidth; + slot.height = slotHeight; + + // Try to find position for the slot from the existing free spaces in the + // layout. + for (i = 0; i < currentRects.length; i++) { + rectId = currentRects[i]; + if (!rectId) continue; + rect = this.getRect(rectId); + if (slot.width <= rect.width + EPS && slot.height <= rect.height + EPS) { + slot.left = rect.left; + slot.top = rect.top; + break; + } + } + + // If no position was found for the slot let's position the slot to + // the bottom left (in vertical mode) or top right (in horizontal mode) of + // the layout. + if (slot.left === null) { + if (horizontal) { + slot.left = layout.width; + slot.top = 0; + } else { + slot.left = 0; + slot.top = layout.height; + } + + // If gaps don't need filling let's throw away all the current free spaces + // (currentRects). + if (!fillGaps) { + ignoreCurrentRects = true; + } + } + + // In vertical mode, if the slot's bottom overlaps the layout's bottom. + if (!horizontal && slot.top + slot.height > layout.height + EPS) { + // If slot is not aligned to the left edge, create a new free space to the + // left of the slot. + if (slot.left > MIN_SLOT_SIZE) { + nextRects.push(this.addRect(0, layout.height, slot.left, Infinity)); + } + + // If slot is not aligned to the right edge, create a new free space to + // the right of the slot. + if (slot.left + slot.width < layout.width - MIN_SLOT_SIZE) { + nextRects.push( + this.addRect( + slot.left + slot.width, + layout.height, + layout.width - slot.left - slot.width, + Infinity + ) + ); + } + + // Update layout height. + layout.height = slot.top + slot.height; + } + + // In horizontal mode, if the slot's right overlaps the layout's right edge. + if (horizontal && slot.left + slot.width > layout.width + EPS) { + // If slot is not aligned to the top, create a new free space above the + // slot. + if (slot.top > MIN_SLOT_SIZE) { + nextRects.push(this.addRect(layout.width, 0, Infinity, slot.top)); + } + + // If slot is not aligned to the bottom, create a new free space below + // the slot. + if (slot.top + slot.height < layout.height - MIN_SLOT_SIZE) { + nextRects.push( + this.addRect( + layout.width, + slot.top + slot.height, + Infinity, + layout.height - slot.top - slot.height + ) + ); + } + + // Update layout width. + layout.width = slot.left + slot.width; + } + + // Clean up the current free spaces making sure none of them overlap with + // the slot. Split all overlapping free spaces into smaller shards that do + // not overlap with the slot. + if (!ignoreCurrentRects) { + if (fillGaps) i = 0; + for (; i < currentRects.length; i++) { + rectId = currentRects[i]; + if (!rectId) continue; + rect = this.getRect(rectId); + shards = this.splitRect(rect, slot); + for (j = 0; j < shards.length; j++) { + rectId = shards[j]; + rect = this.getRect(rectId); + // Make sure that the free space is within the boundaries of the + // layout. This routine is critical to the algorithm as it makes sure + // that there are no leftover spaces with infinite height/width. + // It's also essential that we don't compare values absolutely to each + // other but leave a little headroom (EPSILON) to get rid of false + // positives. + if ( + horizontal ? rect.left + EPS < layout.width - EPS : rect.top + EPS < layout.height - EPS + ) { + nextRects.push(rectId); + } + } + } + } + + // Sanitize and sort all the new free spaces that will be used in the next + // iteration. This procedure is critical to make the bin-packing algorithm + // work. The free spaces have to be in correct order in the beginning of the + // next iteration. + if (nextRects.length > 1) { + this.purgeRects(nextRects).sort(horizontal ? this.sortRectsLeftTop : this.sortRectsTopLeft); + } + + // Finally we need to make sure that `this.currentRects` points to + // `nextRects` array as that is used in the next iteration's beginning when + // we try to find a space for the next slot. + this.currentRects = nextRects; + this.nextRects = currentRects; + + return slot; + }; + + /** + * Add a new rectangle to the rectangle store. Returns the id of the new + * rectangle. + * + * @param {Number} left + * @param {Number} top + * @param {Number} width + * @param {Number} height + * @returns {Number} + */ + PackerProcessor.prototype.addRect = function (left, top, width, height) { + var rectId = ++this.rectId; + this.rectStore[rectId] = left || 0; + this.rectStore[++this.rectId] = top || 0; + this.rectStore[++this.rectId] = width || 0; + this.rectStore[++this.rectId] = height || 0; + return rectId; + }; + + /** + * Get rectangle data from the rectangle store by id. Optionally you can + * provide a target object where the rectangle data will be written in. By + * default an internal object is reused as a target object. + * + * @param {Number} id + * @param {Object} [target] + * @returns {Object} + */ + PackerProcessor.prototype.getRect = function (id, target) { + if (!target) target = this.rectTarget; + target.left = this.rectStore[id] || 0; + target.top = this.rectStore[++id] || 0; + target.width = this.rectStore[++id] || 0; + target.height = this.rectStore[++id] || 0; + return target; + }; + + /** + * Punch a hole into a rectangle and return the shards (1-4). + * + * @param {Object} rect + * @param {Object} hole + * @returns {Number[]} + */ + PackerProcessor.prototype.splitRect = (function () { + var shards = []; + var width = 0; + var height = 0; + return function (rect, hole) { + // Reset old shards. + shards.length = 0; + + // If the slot does not overlap with the hole add slot to the return data + // as is. Note that in this case we are eager to keep the slot as is if + // possible so we use the EPSILON in favour of that logic. + if ( + rect.left + rect.width <= hole.left + EPS || + hole.left + hole.width <= rect.left + EPS || + rect.top + rect.height <= hole.top + EPS || + hole.top + hole.height <= rect.top + EPS + ) { + shards.push(this.addRect(rect.left, rect.top, rect.width, rect.height)); + return shards; + } + + // Left split. + width = hole.left - rect.left; + if (width >= MIN_SLOT_SIZE) { + shards.push(this.addRect(rect.left, rect.top, width, rect.height)); + } + + // Right split. + width = rect.left + rect.width - (hole.left + hole.width); + if (width >= MIN_SLOT_SIZE) { + shards.push(this.addRect(hole.left + hole.width, rect.top, width, rect.height)); + } + + // Top split. + height = hole.top - rect.top; + if (height >= MIN_SLOT_SIZE) { + shards.push(this.addRect(rect.left, rect.top, rect.width, height)); + } + + // Bottom split. + height = rect.top + rect.height - (hole.top + hole.height); + if (height >= MIN_SLOT_SIZE) { + shards.push(this.addRect(rect.left, hole.top + hole.height, rect.width, height)); + } + + return shards; + }; + })(); + + /** + * Check if a rectangle is fully within another rectangle. + * + * @param {Object} a + * @param {Object} b + * @returns {Boolean} + */ + PackerProcessor.prototype.isRectAWithinRectB = function (a, b) { + return ( + a.left + EPS >= b.left && + a.top + EPS >= b.top && + a.left + a.width - EPS <= b.left + b.width && + a.top + a.height - EPS <= b.top + b.height + ); + }; + + /** + * Loops through an array of rectangle ids and resets all that are fully + * within another rectangle in the array. Resetting in this case means that + * the rectangle id value is replaced with zero. + * + * @param {Number[]} rectIds + * @returns {Number[]} + */ + PackerProcessor.prototype.purgeRects = (function () { + var rectA = {}; + var rectB = {}; + return function (rectIds) { + var i = rectIds.length; + var j; + + while (i--) { + j = rectIds.length; + if (!rectIds[i]) continue; + this.getRect(rectIds[i], rectA); + while (j--) { + if (!rectIds[j] || i === j) continue; + this.getRect(rectIds[j], rectB); + if (this.isRectAWithinRectB(rectA, rectB)) { + rectIds[i] = 0; + break; + } + } + } + + return rectIds; + }; + })(); + + /** + * Sort rectangles with top-left gravity. + * + * @param {Number} aId + * @param {Number} bId + * @returns {Number} + */ + PackerProcessor.prototype.sortRectsTopLeft = (function () { + var rectA = {}; + var rectB = {}; + return function (aId, bId) { + this.getRect(aId, rectA); + this.getRect(bId, rectB); + + return rectA.top < rectB.top && rectA.top + EPS < rectB.top + ? -1 + : rectA.top > rectB.top && rectA.top - EPS > rectB.top + ? 1 + : rectA.left < rectB.left && rectA.left + EPS < rectB.left + ? -1 + : rectA.left > rectB.left && rectA.left - EPS > rectB.left + ? 1 + : 0; + }; + })(); + + /** + * Sort rectangles with left-top gravity. + * + * @param {Number} aId + * @param {Number} bId + * @returns {Number} + */ + PackerProcessor.prototype.sortRectsLeftTop = (function () { + var rectA = {}; + var rectB = {}; + return function (aId, bId) { + this.getRect(aId, rectA); + this.getRect(bId, rectB); + return rectA.left < rectB.left && rectA.left + EPS < rectB.left + ? -1 + : rectA.left > rectB.left && rectA.left - EPS < rectB.left + ? 1 + : rectA.top < rectB.top && rectA.top + EPS < rectB.top + ? -1 + : rectA.top > rectB.top && rectA.top - EPS > rectB.top + ? 1 + : 0; + }; + })(); + + if (isWorker) { + var PACKET_INDEX_WIDTH = 1; + var PACKET_INDEX_HEIGHT = 2; + var PACKET_INDEX_OPTIONS = 3; + var PACKET_HEADER_SLOTS = 4; + var processor = new PackerProcessor(); + + self.onmessage = function (msg) { + var data = new Float32Array(msg.data); + var items = data.subarray(PACKET_HEADER_SLOTS, data.length); + var slots = new Float32Array(items.length); + var settings = data[PACKET_INDEX_OPTIONS]; + var layout = { + items: items, + slots: slots, + width: data[PACKET_INDEX_WIDTH], + height: data[PACKET_INDEX_HEIGHT], + }; + + // Compute the layout (width / height / slots). + processor.computeLayout(layout, settings); + + // Copy layout data to the return data. + data[PACKET_INDEX_WIDTH] = layout.width; + data[PACKET_INDEX_HEIGHT] = layout.height; + data.set(layout.slots, PACKET_HEADER_SLOTS); + + // Send layout back to the main thread. + postMessage(data.buffer, [data.buffer]); + }; + } + + return PackerProcessor; +} + +var PackerProcessor = createPackerProcessor(); + +// +// WORKER UTILS +// + +var blobUrl = null; +var activeWorkers = []; + +function createWorkerProcessors(amount, onmessage) { + var workers = []; + + if (amount > 0) { + if (!blobUrl) { + blobUrl = URL.createObjectURL( + new Blob(['(' + createPackerProcessor.toString() + ')(true)'], { + type: 'application/javascript', + }) + ); + } + + for (var i = 0, worker; i < amount; i++) { + worker = new Worker(blobUrl); + if (onmessage) worker.onmessage = onmessage; + workers.push(worker); + activeWorkers.push(worker); + } + } + + return workers; +} + +function destroyWorkerProcessors(workers) { + var worker; + var index; + + for (var i = 0; i < workers.length; i++) { + worker = workers[i]; + worker.onmessage = null; + worker.onerror = null; + worker.onmessageerror = null; + worker.terminate(); + + index = activeWorkers.indexOf(worker); + if (index > -1) activeWorkers.splice(index, 1); + } + + if (blobUrl && !activeWorkers.length) { + URL.revokeObjectURL(blobUrl); + blobUrl = null; + } +} + +function isWorkerProcessorsSupported() { + return !!(window.Worker && window.URL && window.Blob); +} + +var FILL_GAPS = 1; +var HORIZONTAL = 2; +var ALIGN_RIGHT = 4; +var ALIGN_BOTTOM = 8; +var ROUNDING = 16; +var PACKET_INDEX_ID = 0; +var PACKET_INDEX_WIDTH = 1; +var PACKET_INDEX_HEIGHT = 2; +var PACKET_INDEX_OPTIONS = 3; +var PACKET_HEADER_SLOTS = 4; + +/** + * @class + * @param {Number} [numWorkers=0] + * @param {Object} [options] + * @param {Boolean} [options.fillGaps=false] + * @param {Boolean} [options.horizontal=false] + * @param {Boolean} [options.alignRight=false] + * @param {Boolean} [options.alignBottom=false] + * @param {Boolean} [options.rounding=false] + */ +function Packer(numWorkers, options) { + this._options = 0; + this._processor = null; + this._layoutQueue = []; + this._layouts = {}; + this._layoutCallbacks = {}; + this._layoutWorkers = {}; + this._layoutWorkerData = {}; + this._workers = []; + this._onWorkerMessage = this._onWorkerMessage.bind(this); + + // Set initial options. + this.setOptions(options); + + // Init the worker(s) or the processor if workers can't be used. + numWorkers = typeof numWorkers === 'number' ? Math.max(0, numWorkers) : 0; + if (numWorkers && isWorkerProcessorsSupported()) { + try { + this._workers = createWorkerProcessors(numWorkers, this._onWorkerMessage); + } catch (e) { + this._processor = new PackerProcessor(); + } + } else { + this._processor = new PackerProcessor(); + } +} + +Packer.prototype._sendToWorker = function () { + if (!this._layoutQueue.length || !this._workers.length) return; + + var layoutId = this._layoutQueue.shift(); + var worker = this._workers.pop(); + var data = this._layoutWorkerData[layoutId]; + + delete this._layoutWorkerData[layoutId]; + this._layoutWorkers[layoutId] = worker; + worker.postMessage(data.buffer, [data.buffer]); +}; + +Packer.prototype._onWorkerMessage = function (msg) { + var data = new Float32Array(msg.data); + var layoutId = data[PACKET_INDEX_ID]; + var layout = this._layouts[layoutId]; + var callback = this._layoutCallbacks[layoutId]; + var worker = this._layoutWorkers[layoutId]; + + if (layout) delete this._layoutCallbacks[layoutId]; + if (callback) delete this._layoutCallbacks[layoutId]; + if (worker) delete this._layoutWorkers[layoutId]; + + if (layout && callback) { + layout.width = data[PACKET_INDEX_WIDTH]; + layout.height = data[PACKET_INDEX_HEIGHT]; + layout.slots = data.subarray(PACKET_HEADER_SLOTS, data.length); + this._finalizeLayout(layout); + callback(layout); + } + + if (worker) { + this._workers.push(worker); + this._sendToWorker(); + } +}; + +Packer.prototype._finalizeLayout = function (layout) { + var grid = layout._grid; + var isHorizontal = layout._settings & HORIZONTAL; + var isBorderBox = grid._boxSizing === 'border-box'; + + delete layout._grid; + delete layout._settings; + + layout.styles = {}; + + if (isHorizontal) { + layout.styles.width = + (isBorderBox ? layout.width + grid._borderLeft + grid._borderRight : layout.width) + 'px'; + } else { + layout.styles.height = + (isBorderBox ? layout.height + grid._borderTop + grid._borderBottom : layout.height) + 'px'; + } + + return layout; +}; + +/** + * @public + * @param {Object} [options] + * @param {Boolean} [options.fillGaps] + * @param {Boolean} [options.horizontal] + * @param {Boolean} [options.alignRight] + * @param {Boolean} [options.alignBottom] + * @param {Boolean} [options.rounding] + */ +Packer.prototype.setOptions = function (options) { + if (!options) return; + + var fillGaps; + if (typeof options.fillGaps === 'boolean') { + fillGaps = options.fillGaps ? FILL_GAPS : 0; + } else { + fillGaps = this._options & FILL_GAPS; + } + + var horizontal; + if (typeof options.horizontal === 'boolean') { + horizontal = options.horizontal ? HORIZONTAL : 0; + } else { + horizontal = this._options & HORIZONTAL; + } + + var alignRight; + if (typeof options.alignRight === 'boolean') { + alignRight = options.alignRight ? ALIGN_RIGHT : 0; + } else { + alignRight = this._options & ALIGN_RIGHT; + } + + var alignBottom; + if (typeof options.alignBottom === 'boolean') { + alignBottom = options.alignBottom ? ALIGN_BOTTOM : 0; + } else { + alignBottom = this._options & ALIGN_BOTTOM; + } + + var rounding; + if (typeof options.rounding === 'boolean') { + rounding = options.rounding ? ROUNDING : 0; + } else { + rounding = this._options & ROUNDING; + } + + this._options = fillGaps | horizontal | alignRight | alignBottom | rounding; +}; + +/** + * @public + * @param {Grid} grid + * @param {Number} layoutId + * @param {Item[]} items + * @param {Number} width + * @param {Number} height + * @param {Function} callback + * @returns {?Function} + */ +Packer.prototype.createLayout = function (grid, layoutId, items, width, height, callback) { + if (this._layouts[layoutId]) { + throw new Error('A layout with the provided id is currently being processed.'); + } + + var horizontal = this._options & HORIZONTAL; + var layout = { + id: layoutId, + items: items, + slots: null, + width: horizontal ? 0 : width, + height: !horizontal ? 0 : height, + // Temporary data, which will be removed before sending the layout data + // outside of Packer's context. + _grid: grid, + _settings: this._options, + }; + + // If there are no items let's call the callback immediately. + if (!items.length) { + layout.slots = []; + this._finalizeLayout(layout); + callback(layout); + return; + } + + // Create layout synchronously if needed. + if (this._processor) { + layout.slots = window.Float32Array + ? new Float32Array(items.length * 2) + : new Array(items.length * 2); + this._processor.computeLayout(layout, layout._settings); + this._finalizeLayout(layout); + callback(layout); + return; + } + + // Worker data. + var data = new Float32Array(PACKET_HEADER_SLOTS + items.length * 2); + + // Worker data header. + data[PACKET_INDEX_ID] = layoutId; + data[PACKET_INDEX_WIDTH] = layout.width; + data[PACKET_INDEX_HEIGHT] = layout.height; + data[PACKET_INDEX_OPTIONS] = layout._settings; + + // Worker data items. + var i, j, item; + for (i = 0, j = PACKET_HEADER_SLOTS - 1, item; i < items.length; i++) { + item = items[i]; + data[++j] = item._width + item._marginLeft + item._marginRight; + data[++j] = item._height + item._marginTop + item._marginBottom; + } + + this._layoutQueue.push(layoutId); + this._layouts[layoutId] = layout; + this._layoutCallbacks[layoutId] = callback; + this._layoutWorkerData[layoutId] = data; + + this._sendToWorker(); + + return this.cancelLayout.bind(this, layoutId); +}; + +/** + * @public + * @param {Number} layoutId + */ +Packer.prototype.cancelLayout = function (layoutId) { + var layout = this._layouts[layoutId]; + if (!layout) return; + + delete this._layouts[layoutId]; + delete this._layoutCallbacks[layoutId]; + + if (this._layoutWorkerData[layoutId]) { + delete this._layoutWorkerData[layoutId]; + var queueIndex = this._layoutQueue.indexOf(layoutId); + if (queueIndex > -1) this._layoutQueue.splice(queueIndex, 1); + } +}; + +/** + * @public + */ +Packer.prototype.destroy = function () { + // Move all currently used workers back in the workers array. + for (var key in this._layoutWorkers) { + this._workers.push(this._layoutWorkers[key]); + } + + // Destroy all instance's workers. + destroyWorkerProcessors(this._workers); + + // Reset data. + this._workers.length = 0; + this._layoutQueue.length = 0; + this._layouts = {}; + this._layoutCallbacks = {}; + this._layoutWorkers = {}; + this._layoutWorkerData = {}; +}; + +var debounceId = 0; + +/** + * Returns a function, that, as long as it continues to be invoked, will not + * be triggered. The function will be called after it stops being called for + * N milliseconds. The returned function accepts one argument which, when + * being `true`, cancels the debounce function immediately. When the debounce + * function is canceled it cannot be invoked again. + * + * @param {Function} fn + * @param {Number} durationMs + * @returns {Function} + */ +function debounce(fn, durationMs) { + var id = ++debounceId; + var timer = 0; + var lastTime = 0; + var isCanceled = false; + var tick = function (time) { + if (isCanceled) return; + + if (lastTime) timer -= time - lastTime; + lastTime = time; + + if (timer > 0) { + addDebounceTick(id, tick); + } else { + timer = lastTime = 0; + fn(); + } + }; + + return function (cancel) { + if (isCanceled) return; + + if (durationMs <= 0) { + if (cancel !== true) fn(); + return; + } + + if (cancel === true) { + isCanceled = true; + timer = lastTime = 0; + tick = undefined; + cancelDebounceTick(id); + return; + } + + if (timer <= 0) { + timer = durationMs; + tick(0); + } else { + timer = durationMs; + } + }; +} + +var htmlCollectionType = '[object HTMLCollection]'; +var nodeListType = '[object NodeList]'; + +/** + * Check if a value is a node list or a html collection. + * + * @param {*} val + * @returns {Boolean} + */ +function isNodeList(val) { + var type = Object.prototype.toString.call(val); + return type === htmlCollectionType || type === nodeListType; +} + +var objectType = 'object'; +var objectToStringType = '[object Object]'; +var toString = Object.prototype.toString; + +/** + * Check if a value is a plain object. + * + * @param {*} val + * @returns {Boolean} + */ +function isPlainObject(val) { + return typeof val === objectType && toString.call(val) === objectToStringType; +} + +function noop() {} + +/** + * Converts a value to an array or clones an array. + * + * @param {*} val + * @returns {Array} + */ +function toArray(val) { + return isNodeList(val) ? Array.prototype.slice.call(val) : Array.prototype.concat(val); +} + +var NUMBER_TYPE = 'number'; +var STRING_TYPE = 'string'; +var INSTANT_LAYOUT = 'instant'; +var layoutId = 0; + +/** + * Creates a new Grid instance. + * + * @class + * @param {(HTMLElement|String)} element + * @param {Object} [options] + * @param {(String|HTMLElement[]|NodeList|HTMLCollection)} [options.items="*"] + * @param {Number} [options.showDuration=300] + * @param {String} [options.showEasing="ease"] + * @param {Object} [options.visibleStyles={opacity: "1", transform: "scale(1)"}] + * @param {Number} [options.hideDuration=300] + * @param {String} [options.hideEasing="ease"] + * @param {Object} [options.hiddenStyles={opacity: "0", transform: "scale(0.5)"}] + * @param {(Function|Object)} [options.layout] + * @param {Boolean} [options.layout.fillGaps=false] + * @param {Boolean} [options.layout.horizontal=false] + * @param {Boolean} [options.layout.alignRight=false] + * @param {Boolean} [options.layout.alignBottom=false] + * @param {Boolean} [options.layout.rounding=false] + * @param {(Boolean|Number)} [options.layoutOnResize=150] + * @param {Boolean} [options.layoutOnInit=true] + * @param {Number} [options.layoutDuration=300] + * @param {String} [options.layoutEasing="ease"] + * @param {?Object} [options.sortData=null] + * @param {Boolean} [options.dragEnabled=false] + * @param {?String} [options.dragHandle=null] + * @param {?HtmlElement} [options.dragContainer=null] + * @param {?Function} [options.dragStartPredicate] + * @param {Number} [options.dragStartPredicate.distance=0] + * @param {Number} [options.dragStartPredicate.delay=0] + * @param {String} [options.dragAxis="xy"] + * @param {(Boolean|Function)} [options.dragSort=true] + * @param {Object} [options.dragSortHeuristics] + * @param {Number} [options.dragSortHeuristics.sortInterval=100] + * @param {Number} [options.dragSortHeuristics.minDragDistance=10] + * @param {Number} [options.dragSortHeuristics.minBounceBackAngle=1] + * @param {(Function|Object)} [options.dragSortPredicate] + * @param {Number} [options.dragSortPredicate.threshold=50] + * @param {String} [options.dragSortPredicate.action="move"] + * @param {String} [options.dragSortPredicate.migrateAction="move"] + * @param {Object} [options.dragRelease] + * @param {Number} [options.dragRelease.duration=300] + * @param {String} [options.dragRelease.easing="ease"] + * @param {Boolean} [options.dragRelease.useDragContainer=true] + * @param {Object} [options.dragCssProps] + * @param {Object} [options.dragPlaceholder] + * @param {Boolean} [options.dragPlaceholder.enabled=false] + * @param {?Function} [options.dragPlaceholder.createElement=null] + * @param {?Function} [options.dragPlaceholder.onCreate=null] + * @param {?Function} [options.dragPlaceholder.onRemove=null] + * @param {Object} [options.dragAutoScroll] + * @param {(Function|Array)} [options.dragAutoScroll.targets=[]] + * @param {?Function} [options.dragAutoScroll.handle=null] + * @param {Number} [options.dragAutoScroll.threshold=50] + * @param {Number} [options.dragAutoScroll.safeZone=0.2] + * @param {(Function|Number)} [options.dragAutoScroll.speed] + * @param {Boolean} [options.dragAutoScroll.sortDuringScroll=true] + * @param {Boolean} [options.dragAutoScroll.smoothStop=false] + * @param {?Function} [options.dragAutoScroll.onStart=null] + * @param {?Function} [options.dragAutoScroll.onStop=null] + * @param {String} [options.containerClass="muuri"] + * @param {String} [options.itemClass="muuri-item"] + * @param {String} [options.itemVisibleClass="muuri-item-visible"] + * @param {String} [options.itemHiddenClass="muuri-item-hidden"] + * @param {String} [options.itemPositioningClass="muuri-item-positioning"] + * @param {String} [options.itemDraggingClass="muuri-item-dragging"] + * @param {String} [options.itemReleasingClass="muuri-item-releasing"] + * @param {String} [options.itemPlaceholderClass="muuri-item-placeholder"] + */ +function Grid(element, options) { + // Allow passing element as selector string + if (typeof element === STRING_TYPE) { + element = document.querySelector(element); + } + + // Throw an error if the container element is not body element or does not + // exist within the body element. + var isElementInDom = element.getRootNode + ? element.getRootNode({ composed: true }) === document + : document.body.contains(element); + if (!isElementInDom || element === document.documentElement) { + throw new Error('Container element must be an existing DOM element.'); + } + + // Create instance settings by merging the options with default options. + var settings = mergeSettings(Grid.defaultOptions, options); + settings.visibleStyles = normalizeStyles(settings.visibleStyles); + settings.hiddenStyles = normalizeStyles(settings.hiddenStyles); + if (!isFunction(settings.dragSort)) { + settings.dragSort = !!settings.dragSort; + } + + this._id = createUid(); + this._element = element; + this._settings = settings; + this._isDestroyed = false; + this._items = []; + this._layout = { + id: 0, + items: [], + slots: [], + }; + this._isLayoutFinished = true; + this._nextLayoutData = null; + this._emitter = new Emitter(); + this._onLayoutDataReceived = this._onLayoutDataReceived.bind(this); + + // Store grid instance to the grid instances collection. + GRID_INSTANCES[this._id] = this; + + // Add container element's class name. + addClass(element, settings.containerClass); + + // If layoutOnResize option is a valid number sanitize it and bind the resize + // handler. + bindLayoutOnResize(this, settings.layoutOnResize); + + // Add initial items. + this.add(getInitialGridElements(element, settings.items), { layout: false }); + + // Layout on init if necessary. + if (settings.layoutOnInit) { + this.layout(true); + } +} + +/** + * Public properties + * ***************** + */ + +/** + * @public + * @static + * @see Item + */ +Grid.Item = Item; + +/** + * @public + * @static + * @see ItemLayout + */ +Grid.ItemLayout = ItemLayout; + +/** + * @public + * @static + * @see ItemVisibility + */ +Grid.ItemVisibility = ItemVisibility; + +/** + * @public + * @static + * @see ItemMigrate + */ +Grid.ItemMigrate = ItemMigrate; + +/** + * @public + * @static + * @see ItemDrag + */ +Grid.ItemDrag = ItemDrag; + +/** + * @public + * @static + * @see ItemDragRelease + */ +Grid.ItemDragRelease = ItemDragRelease; + +/** + * @public + * @static + * @see ItemDragPlaceholder + */ +Grid.ItemDragPlaceholder = ItemDragPlaceholder; + +/** + * @public + * @static + * @see Emitter + */ +Grid.Emitter = Emitter; + +/** + * @public + * @static + * @see Animator + */ +Grid.Animator = Animator; + +/** + * @public + * @static + * @see Dragger + */ +Grid.Dragger = Dragger; + +/** + * @public + * @static + * @see Packer + */ +Grid.Packer = Packer; + +/** + * @public + * @static + * @see AutoScroller + */ +Grid.AutoScroller = AutoScroller; + +/** + * The default Packer instance used by default for all layouts. + * + * @public + * @static + * @type {Packer} + */ +Grid.defaultPacker = new Packer(2); + +/** + * Default options for Grid instance. + * + * @public + * @static + * @type {Object} + */ +Grid.defaultOptions = { + // Initial item elements + items: '*', + + // Default show animation + showDuration: 300, + showEasing: 'ease', + + // Default hide animation + hideDuration: 300, + hideEasing: 'ease', + + // Item's visible/hidden state styles + visibleStyles: { + opacity: '1', + transform: 'scale(1)', + }, + hiddenStyles: { + opacity: '0', + transform: 'scale(0.5)', + }, + + // Layout + layout: { + fillGaps: false, + horizontal: false, + alignRight: false, + alignBottom: false, + rounding: false, + }, + layoutOnResize: 150, + layoutOnInit: true, + layoutDuration: 300, + layoutEasing: 'ease', + + // Sorting + sortData: null, + + // Drag & Drop + dragEnabled: false, + dragContainer: null, + dragHandle: null, + dragStartPredicate: { + distance: 0, + delay: 0, + }, + dragAxis: 'xy', + dragSort: true, + dragSortHeuristics: { + sortInterval: 100, + minDragDistance: 10, + minBounceBackAngle: 1, + }, + dragSortPredicate: { + threshold: 50, + action: ACTION_MOVE, + migrateAction: ACTION_MOVE, + }, + dragRelease: { + duration: 300, + easing: 'ease', + useDragContainer: true, + }, + dragCssProps: { + touchAction: 'none', + userSelect: 'none', + userDrag: 'none', + tapHighlightColor: 'rgba(0, 0, 0, 0)', + touchCallout: 'none', + contentZooming: 'none', + }, + dragPlaceholder: { + enabled: false, + createElement: null, + onCreate: null, + onRemove: null, + }, + dragAutoScroll: { + targets: [], + handle: null, + threshold: 50, + safeZone: 0.2, + speed: AutoScroller.smoothSpeed(1000, 2000, 2500), + sortDuringScroll: true, + smoothStop: false, + onStart: null, + onStop: null, + }, + + // Classnames + containerClass: 'muuri', + itemClass: 'muuri-item', + itemVisibleClass: 'muuri-item-shown', + itemHiddenClass: 'muuri-item-hidden', + itemPositioningClass: 'muuri-item-positioning', + itemDraggingClass: 'muuri-item-dragging', + itemReleasingClass: 'muuri-item-releasing', + itemPlaceholderClass: 'muuri-item-placeholder', +}; + +/** + * Public prototype methods + * ************************ + */ + +/** + * Bind an event listener. + * + * @public + * @param {String} event + * @param {Function} listener + * @returns {Grid} + */ +Grid.prototype.on = function (event, listener) { + this._emitter.on(event, listener); + return this; +}; + +/** + * Unbind an event listener. + * + * @public + * @param {String} event + * @param {Function} listener + * @returns {Grid} + */ +Grid.prototype.off = function (event, listener) { + this._emitter.off(event, listener); + return this; +}; + +/** + * Get the container element. + * + * @public + * @returns {HTMLElement} + */ +Grid.prototype.getElement = function () { + return this._element; +}; + +/** + * Get instance's item by element or by index. Target can also be an Item + * instance in which case the function returns the item if it exists within + * related Grid instance. If nothing is found with the provided target, null + * is returned. + * + * @private + * @param {(HtmlElement|Number|Item)} [target] + * @returns {?Item} + */ +Grid.prototype.getItem = function (target) { + // If no target is specified or the instance is destroyed, return null. + if (this._isDestroyed || (!target && target !== 0)) { + return null; + } + + // If target is number return the item in that index. If the number is lower + // than zero look for the item starting from the end of the items array. For + // example -1 for the last item, -2 for the second last item, etc. + if (typeof target === NUMBER_TYPE) { + return this._items[target > -1 ? target : this._items.length + target] || null; + } + + // If the target is an instance of Item return it if it is attached to this + // Grid instance, otherwise return null. + if (target instanceof Item) { + return target._gridId === this._id ? target : null; + } + + // In other cases let's assume that the target is an element, so let's try + // to find an item that matches the element and return it. If item is not + // found return null. + if (ITEM_ELEMENT_MAP) { + var item = ITEM_ELEMENT_MAP.get(target); + return item && item._gridId === this._id ? item : null; + } else { + for (var i = 0; i < this._items.length; i++) { + if (this._items[i]._element === target) { + return this._items[i]; + } + } + } + + return null; +}; + +/** + * Get all items. Optionally you can provide specific targets (elements, + * indices and item instances). All items that are not found are omitted from + * the returned array. + * + * @public + * @param {(HtmlElement|Number|Item|Array)} [targets] + * @returns {Item[]} + */ +Grid.prototype.getItems = function (targets) { + // Return all items immediately if no targets were provided or if the + // instance is destroyed. + if (this._isDestroyed || targets === undefined) { + return this._items.slice(0); + } + + var items = []; + var i, item; + + if (Array.isArray(targets) || isNodeList(targets)) { + for (i = 0; i < targets.length; i++) { + item = this.getItem(targets[i]); + if (item) items.push(item); + } + } else { + item = this.getItem(targets); + if (item) items.push(item); + } + + return items; +}; + +/** + * Update the cached dimensions of the instance's items. By default all the + * items are refreshed, but you can also provide an array of target items as the + * first argument if you want to refresh specific items. Note that all hidden + * items are not refreshed by default since their "display" property is "none" + * and their dimensions are therefore not readable from the DOM. However, if you + * do want to force update hidden item dimensions too you can provide `true` + * as the second argument, which makes the elements temporarily visible while + * their dimensions are being read. + * + * @public + * @param {Item[]} [items] + * @param {Boolean} [force=false] + * @returns {Grid} + */ +Grid.prototype.refreshItems = function (items, force) { + if (this._isDestroyed) return this; + + var targets = items || this._items; + var i, item, style, hiddenItemStyles; + + if (force === true) { + hiddenItemStyles = []; + for (i = 0; i < targets.length; i++) { + item = targets[i]; + if (!item.isVisible() && !item.isHiding()) { + style = item.getElement().style; + style.visibility = 'hidden'; + style.display = ''; + hiddenItemStyles.push(style); + } + } + } + + for (i = 0; i < targets.length; i++) { + targets[i]._refreshDimensions(force); + } + + if (force === true) { + for (i = 0; i < hiddenItemStyles.length; i++) { + style = hiddenItemStyles[i]; + style.visibility = ''; + style.display = 'none'; + } + hiddenItemStyles.length = 0; + } + + return this; +}; + +/** + * Update the sort data of the instance's items. By default all the items are + * refreshed, but you can also provide an array of target items if you want to + * refresh specific items. + * + * @public + * @param {Item[]} [items] + * @returns {Grid} + */ +Grid.prototype.refreshSortData = function (items) { + if (this._isDestroyed) return this; + + var targets = items || this._items; + for (var i = 0; i < targets.length; i++) { + targets[i]._refreshSortData(); + } + + return this; +}; + +/** + * Synchronize the item elements to match the order of the items in the DOM. + * This comes handy if you need to keep the DOM structure matched with the + * order of the items. Note that if an item's element is not currently a child + * of the container element (if it is dragged for example) it is ignored and + * left untouched. + * + * @public + * @returns {Grid} + */ +Grid.prototype.synchronize = function () { + if (this._isDestroyed) return this; + + var items = this._items; + if (!items.length) return this; + + var fragment; + var element; + + for (var i = 0; i < items.length; i++) { + element = items[i]._element; + if (element.parentNode === this._element) { + fragment = fragment || document.createDocumentFragment(); + fragment.appendChild(element); + } + } + + if (!fragment) return this; + + this._element.appendChild(fragment); + this._emit(EVENT_SYNCHRONIZE); + + return this; +}; + +/** + * Calculate and apply item positions. + * + * @public + * @param {Boolean} [instant=false] + * @param {Function} [onFinish] + * @returns {Grid} + */ +Grid.prototype.layout = function (instant, onFinish) { + if (this._isDestroyed) return this; + + // Cancel unfinished layout algorithm if possible. + var unfinishedLayout = this._nextLayoutData; + if (unfinishedLayout && isFunction(unfinishedLayout.cancel)) { + unfinishedLayout.cancel(); + } + + // Compute layout id (let's stay in Float32 range). + layoutId = (layoutId % MAX_SAFE_FLOAT32_INTEGER) + 1; + var nextLayoutId = layoutId; + + // Store data for next layout. + this._nextLayoutData = { + id: nextLayoutId, + instant: instant, + onFinish: onFinish, + cancel: null, + }; + + // Collect layout items (all active grid items). + var items = this._items; + var layoutItems = []; + for (var i = 0; i < items.length; i++) { + if (items[i]._isActive) layoutItems.push(items[i]); + } + + // Compute new layout. + this._refreshDimensions(); + var gridWidth = this._width - this._borderLeft - this._borderRight; + var gridHeight = this._height - this._borderTop - this._borderBottom; + var layoutSettings = this._settings.layout; + var cancelLayout; + if (isFunction(layoutSettings)) { + cancelLayout = layoutSettings( + this, + nextLayoutId, + layoutItems, + gridWidth, + gridHeight, + this._onLayoutDataReceived + ); + } else { + Grid.defaultPacker.setOptions(layoutSettings); + cancelLayout = Grid.defaultPacker.createLayout( + this, + nextLayoutId, + layoutItems, + gridWidth, + gridHeight, + this._onLayoutDataReceived + ); + } + + // Store layout cancel method if available. + if ( + isFunction(cancelLayout) && + this._nextLayoutData && + this._nextLayoutData.id === nextLayoutId + ) { + this._nextLayoutData.cancel = cancelLayout; + } + + return this; +}; + +/** + * Add new items by providing the elements you wish to add to the instance and + * optionally provide the index where you want the items to be inserted into. + * All elements that are not already children of the container element will be + * automatically appended to the container element. If an element has it's CSS + * display property set to "none" it will be marked as inactive during the + * initiation process. As long as the item is inactive it will not be part of + * the layout, but it will retain it's index. You can activate items at any + * point with grid.show() method. This method will automatically call + * grid.layout() if one or more of the added elements are visible. If only + * hidden items are added no layout will be called. All the new visible items + * are positioned without animation during their first layout. + * + * @public + * @param {(HTMLElement|HTMLElement[])} elements + * @param {Object} [options] + * @param {Number} [options.index=-1] + * @param {Boolean} [options.active] + * @param {(Boolean|Function|String)} [options.layout=true] + * @returns {Item[]} + */ +Grid.prototype.add = function (elements, options) { + if (this._isDestroyed || !elements) return []; + + var newItems = toArray(elements); + if (!newItems.length) return newItems; + + var opts = options || {}; + var layout = opts.layout ? opts.layout : opts.layout === undefined; + var items = this._items; + var needsLayout = false; + var fragment; + var element; + var item; + var i; + + // Collect all the elements that are not child of the grid element into a + // document fragment. + for (i = 0; i < newItems.length; i++) { + element = newItems[i]; + if (element.parentNode !== this._element) { + fragment = fragment || document.createDocumentFragment(); + fragment.appendChild(element); + } + } + + // If we have a fragment, let's append it to the grid element. We could just + // not do this and the `new Item()` instantiation would handle this for us, + // but this way we can add the elements into the DOM a bit faster. + if (fragment) { + this._element.appendChild(fragment); + } + + // Map provided elements into new grid items. + for (i = 0; i < newItems.length; i++) { + element = newItems[i]; + item = newItems[i] = new Item(this, element, opts.active); + + // If the item to be added is active, we need to do a layout. Also, we + // need to mark the item with the skipNextAnimation flag to make it + // position instantly (without animation) during the next layout. Without + // the hack the item would animate to it's new position from the northwest + // corner of the grid, which feels a bit buggy (imho). + if (item._isActive) { + needsLayout = true; + item._layout._skipNextAnimation = true; + } + } + + // Set up the items' initial dimensions and sort data. This needs to be done + // in a separate loop to avoid layout thrashing. + for (i = 0; i < newItems.length; i++) { + item = newItems[i]; + item._refreshDimensions(); + item._refreshSortData(); + } + + // Add the new items to the items collection to correct index. + arrayInsert(items, newItems, opts.index); + + // Emit add event. + if (this._hasListeners(EVENT_ADD)) { + this._emit(EVENT_ADD, newItems.slice(0)); + } + + // If layout is needed. + if (needsLayout && layout) { + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); + } + + return newItems; +}; + +/** + * Remove items from the instance. + * + * @public + * @param {Item[]} items + * @param {Object} [options] + * @param {Boolean} [options.removeElements=false] + * @param {(Boolean|Function|String)} [options.layout=true] + * @returns {Item[]} + */ +Grid.prototype.remove = function (items, options) { + if (this._isDestroyed || !items.length) return []; + + var opts = options || {}; + var layout = opts.layout ? opts.layout : opts.layout === undefined; + var needsLayout = false; + var allItems = this.getItems(); + var targetItems = []; + var indices = []; + var index; + var item; + var i; + + // Remove the individual items. + for (i = 0; i < items.length; i++) { + item = items[i]; + if (item._isDestroyed) continue; + + index = this._items.indexOf(item); + if (index === -1) continue; + + if (item._isActive) needsLayout = true; + + targetItems.push(item); + indices.push(allItems.indexOf(item)); + item._destroy(opts.removeElements); + this._items.splice(index, 1); + } + + // Emit remove event. + if (this._hasListeners(EVENT_REMOVE)) { + this._emit(EVENT_REMOVE, targetItems.slice(0), indices); + } + + // If layout is needed. + if (needsLayout && layout) { + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); + } + + return targetItems; +}; + +/** + * Show specific instance items. + * + * @public + * @param {Item[]} items + * @param {Object} [options] + * @param {Boolean} [options.instant=false] + * @param {Boolean} [options.syncWithLayout=true] + * @param {Function} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] + * @returns {Grid} + */ +Grid.prototype.show = function (items, options) { + if (!this._isDestroyed && items.length) { + this._setItemsVisibility(items, true, options); + } + return this; +}; + +/** + * Hide specific instance items. + * + * @public + * @param {Item[]} items + * @param {Object} [options] + * @param {Boolean} [options.instant=false] + * @param {Boolean} [options.syncWithLayout=true] + * @param {Function} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] + * @returns {Grid} + */ +Grid.prototype.hide = function (items, options) { + if (!this._isDestroyed && items.length) { + this._setItemsVisibility(items, false, options); + } + return this; +}; + +/** + * Filter items. Expects at least one argument, a predicate, which should be + * either a function or a string. The predicate callback is executed for every + * item in the instance. If the return value of the predicate is truthy the + * item in question will be shown and otherwise hidden. The predicate callback + * receives the item instance as it's argument. If the predicate is a string + * it is considered to be a selector and it is checked against every item + * element in the instance with the native element.matches() method. All the + * matching items will be shown and others hidden. + * + * @public + * @param {(Function|String)} predicate + * @param {Object} [options] + * @param {Boolean} [options.instant=false] + * @param {Boolean} [options.syncWithLayout=true] + * @param {FilterCallback} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] + * @returns {Grid} + */ +Grid.prototype.filter = function (predicate, options) { + if (this._isDestroyed || !this._items.length) return this; + + var itemsToShow = []; + var itemsToHide = []; + var isPredicateString = typeof predicate === STRING_TYPE; + var isPredicateFn = isFunction(predicate); + var opts = options || {}; + var isInstant = opts.instant === true; + var syncWithLayout = opts.syncWithLayout; + var layout = opts.layout ? opts.layout : opts.layout === undefined; + var onFinish = isFunction(opts.onFinish) ? opts.onFinish : null; + var tryFinishCounter = -1; + var tryFinish = noop; + var item; + var i; + + // If we have onFinish callback, let's create proper tryFinish callback. + if (onFinish) { + tryFinish = function () { + ++tryFinishCounter && onFinish(itemsToShow.slice(0), itemsToHide.slice(0)); + }; + } + + // Check which items need to be shown and which hidden. + if (isPredicateFn || isPredicateString) { + for (i = 0; i < this._items.length; i++) { + item = this._items[i]; + if (isPredicateFn ? predicate(item) : elementMatches(item._element, predicate)) { + itemsToShow.push(item); + } else { + itemsToHide.push(item); + } + } + } + + // Show items that need to be shown. + if (itemsToShow.length) { + this.show(itemsToShow, { + instant: isInstant, + syncWithLayout: syncWithLayout, + onFinish: tryFinish, + layout: false, + }); + } else { + tryFinish(); + } + + // Hide items that need to be hidden. + if (itemsToHide.length) { + this.hide(itemsToHide, { + instant: isInstant, + syncWithLayout: syncWithLayout, + onFinish: tryFinish, + layout: false, + }); + } else { + tryFinish(); + } + + // If there are any items to filter. + if (itemsToShow.length || itemsToHide.length) { + // Emit filter event. + if (this._hasListeners(EVENT_FILTER)) { + this._emit(EVENT_FILTER, itemsToShow.slice(0), itemsToHide.slice(0)); + } + + // If layout is needed. + if (layout) { + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); + } + } + + return this; +}; + +/** + * Sort items. There are three ways to sort the items. The first is simply by + * providing a function as the comparer which works identically to native + * array sort. Alternatively you can sort by the sort data you have provided + * in the instance's options. Just provide the sort data key(s) as a string + * (separated by space) and the items will be sorted based on the provided + * sort data keys. Lastly you have the opportunity to provide a presorted + * array of items which will be used to sync the internal items array in the + * same order. + * + * @public + * @param {(Function|String|Item[])} comparer + * @param {Object} [options] + * @param {Boolean} [options.descending=false] + * @param {(Boolean|Function|String)} [options.layout=true] + * @returns {Grid} + */ +Grid.prototype.sort = (function () { + var sortComparer; + var isDescending; + var origItems; + var indexMap; + + function defaultComparer(a, b) { + var result = 0; + var criteriaName; + var criteriaOrder; + var valA; + var valB; + + // Loop through the list of sort criteria. + for (var i = 0; i < sortComparer.length; i++) { + // Get the criteria name, which should match an item's sort data key. + criteriaName = sortComparer[i][0]; + criteriaOrder = sortComparer[i][1]; + + // Get items' cached sort values for the criteria. If the item has no sort + // data let's update the items sort data (this is a lazy load mechanism). + valA = (a._sortData ? a : a._refreshSortData())._sortData[criteriaName]; + valB = (b._sortData ? b : b._refreshSortData())._sortData[criteriaName]; + + // Sort the items in descending order if defined so explicitly. Otherwise + // sort items in ascending order. + if (criteriaOrder === 'desc' || (!criteriaOrder && isDescending)) { + result = valB < valA ? -1 : valB > valA ? 1 : 0; + } else { + result = valA < valB ? -1 : valA > valB ? 1 : 0; + } + + // If we have -1 or 1 as the return value, let's return it immediately. + if (result) return result; + } + + // If values are equal let's compare the item indices to make sure we + // have a stable sort. Note that this is not necessary in evergreen browsers + // because Array.sort() is nowadays stable. However, in order to guarantee + // same results in older browsers we need this. + if (!result) { + if (!indexMap) indexMap = createIndexMap(origItems); + result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b); + } + return result; + } + + function customComparer(a, b) { + var result = isDescending ? -sortComparer(a, b) : sortComparer(a, b); + if (!result) { + if (!indexMap) indexMap = createIndexMap(origItems); + result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b); + } + return result; + } + + return function (comparer, options) { + if (this._isDestroyed || this._items.length < 2) return this; + + var items = this._items; + var opts = options || {}; + var layout = opts.layout ? opts.layout : opts.layout === undefined; + + // Setup parent scope data. + isDescending = !!opts.descending; + origItems = items.slice(0); + indexMap = null; + + // If function is provided do a native array sort. + if (isFunction(comparer)) { + sortComparer = comparer; + items.sort(customComparer); + } + // Otherwise if we got a string, let's sort by the sort data as provided in + // the instance's options. + else if (typeof comparer === STRING_TYPE) { + sortComparer = comparer + .trim() + .split(' ') + .filter(function (val) { + return val; + }) + .map(function (val) { + return val.split(':'); + }); + items.sort(defaultComparer); + } + // Otherwise if we got an array, let's assume it's a presorted array of the + // items and order the items based on it. Here we blindly trust that the + // presorted array consists of the same item instances as the current + // `gird._items` array. + else if (Array.isArray(comparer)) { + items.length = 0; + items.push.apply(items, comparer); + } + // Otherwise let's throw an error. + else { + sortComparer = isDescending = origItems = indexMap = null; + throw new Error('Invalid comparer argument provided.'); + } + + // Emit sort event. + if (this._hasListeners(EVENT_SORT)) { + this._emit(EVENT_SORT, items.slice(0), origItems); + } + + // If layout is needed. + if (layout) { + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); + } + + // Reset data (to avoid mem leaks). + sortComparer = isDescending = origItems = indexMap = null; + + return this; + }; +})(); + +/** + * Move item to another index or in place of another item. + * + * @public + * @param {(HtmlElement|Number|Item)} item + * @param {(HtmlElement|Number|Item)} position + * @param {Object} [options] + * @param {String} [options.action="move"] + * - Accepts either "move" or "swap". + * - "move" moves the item in place of the other item. + * - "swap" swaps the position of the items. + * @param {(Boolean|Function|String)} [options.layout=true] + * @returns {Grid} + */ +Grid.prototype.move = function (item, position, options) { + if (this._isDestroyed || this._items.length < 2) return this; + + var items = this._items; + var opts = options || {}; + var layout = opts.layout ? opts.layout : opts.layout === undefined; + var isSwap = opts.action === ACTION_SWAP; + var action = isSwap ? ACTION_SWAP : ACTION_MOVE; + var fromItem = this.getItem(item); + var toItem = this.getItem(position); + var fromIndex; + var toIndex; + + // Make sure the items exist and are not the same. + if (fromItem && toItem && fromItem !== toItem) { + // Get the indices of the items. + fromIndex = items.indexOf(fromItem); + toIndex = items.indexOf(toItem); + + // Do the move/swap. + if (isSwap) { + arraySwap(items, fromIndex, toIndex); + } else { + arrayMove(items, fromIndex, toIndex); + } + + // Emit move event. + if (this._hasListeners(EVENT_MOVE)) { + this._emit(EVENT_MOVE, { + item: fromItem, + fromIndex: fromIndex, + toIndex: toIndex, + action: action, + }); + } + + // If layout is needed. + if (layout) { + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); + } + } + + return this; +}; + +/** + * Send item to another Grid instance. + * + * @public + * @param {(HtmlElement|Number|Item)} item + * @param {Grid} targetGrid + * @param {(HtmlElement|Number|Item)} position + * @param {Object} [options] + * @param {HTMLElement} [options.appendTo=document.body] + * @param {(Boolean|Function|String)} [options.layoutSender=true] + * @param {(Boolean|Function|String)} [options.layoutReceiver=true] + * @returns {Grid} + */ +Grid.prototype.send = function (item, targetGrid, position, options) { + if (this._isDestroyed || targetGrid._isDestroyed || this === targetGrid) return this; + + // Make sure we have a valid target item. + item = this.getItem(item); + if (!item) return this; + + var opts = options || {}; + var container = opts.appendTo || document.body; + var layoutSender = opts.layoutSender ? opts.layoutSender : opts.layoutSender === undefined; + var layoutReceiver = opts.layoutReceiver + ? opts.layoutReceiver + : opts.layoutReceiver === undefined; + + // Start the migration process. + item._migrate.start(targetGrid, position, container); + + // If migration was started successfully and the item is active, let's layout + // the grids. + if (item._migrate._isActive && item._isActive) { + if (layoutSender) { + this.layout( + layoutSender === INSTANT_LAYOUT, + isFunction(layoutSender) ? layoutSender : undefined + ); + } + if (layoutReceiver) { + targetGrid.layout( + layoutReceiver === INSTANT_LAYOUT, + isFunction(layoutReceiver) ? layoutReceiver : undefined + ); + } + } + + return this; +}; + +/** + * Destroy the instance. + * + * @public + * @param {Boolean} [removeElements=false] + * @returns {Grid} + */ +Grid.prototype.destroy = function (removeElements) { + if (this._isDestroyed) return this; + + var container = this._element; + var items = this._items.slice(0); + var layoutStyles = (this._layout && this._layout.styles) || {}; + var i, prop; + + // Unbind window resize event listener. + unbindLayoutOnResize(this); + + // Destroy items. + for (i = 0; i < items.length; i++) items[i]._destroy(removeElements); + this._items.length = 0; + + // Restore container. + removeClass(container, this._settings.containerClass); + for (prop in layoutStyles) container.style[prop] = ''; + + // Emit destroy event and unbind all events. + this._emit(EVENT_DESTROY); + this._emitter.destroy(); + + // Remove reference from the grid instances collection. + delete GRID_INSTANCES[this._id]; + + // Flag instance as destroyed. + this._isDestroyed = true; + + return this; +}; + +/** + * Private prototype methods + * ************************* + */ + +/** + * Emit a grid event. + * + * @private + * @param {String} event + * @param {...*} [arg] + */ +Grid.prototype._emit = function () { + if (this._isDestroyed) return; + this._emitter.emit.apply(this._emitter, arguments); +}; + +/** + * Check if there are any events listeners for an event. + * + * @private + * @param {String} event + * @returns {Boolean} + */ +Grid.prototype._hasListeners = function (event) { + if (this._isDestroyed) return false; + return this._emitter.countListeners(event) > 0; +}; + +/** + * Update container's width, height and offsets. + * + * @private + */ +Grid.prototype._updateBoundingRect = function () { + var element = this._element; + var rect = element.getBoundingClientRect(); + this._width = rect.width; + this._height = rect.height; + this._left = rect.left; + this._top = rect.top; + this._right = rect.right; + this._bottom = rect.bottom; +}; + +/** + * Update container's border sizes. + * + * @private + * @param {Boolean} left + * @param {Boolean} right + * @param {Boolean} top + * @param {Boolean} bottom + */ +Grid.prototype._updateBorders = function (left, right, top, bottom) { + var element = this._element; + if (left) this._borderLeft = getStyleAsFloat(element, 'border-left-width'); + if (right) this._borderRight = getStyleAsFloat(element, 'border-right-width'); + if (top) this._borderTop = getStyleAsFloat(element, 'border-top-width'); + if (bottom) this._borderBottom = getStyleAsFloat(element, 'border-bottom-width'); +}; + +/** + * Refresh all of container's internal dimensions and offsets. + * + * @private + */ +Grid.prototype._refreshDimensions = function () { + this._updateBoundingRect(); + this._updateBorders(1, 1, 1, 1); + this._boxSizing = getStyle(this._element, 'box-sizing'); +}; + +/** + * Calculate and apply item positions. + * + * @private + * @param {Object} layout + */ +Grid.prototype._onLayoutDataReceived = (function () { + var itemsToLayout = []; + return function (layout) { + if (this._isDestroyed || !this._nextLayoutData || this._nextLayoutData.id !== layout.id) return; + + var grid = this; + var instant = this._nextLayoutData.instant; + var onFinish = this._nextLayoutData.onFinish; + var numItems = layout.items.length; + var counter = numItems; + var item; + var left; + var top; + var i; + + // Reset next layout data. + this._nextLayoutData = null; + + if (!this._isLayoutFinished && this._hasListeners(EVENT_LAYOUT_ABORT)) { + this._emit(EVENT_LAYOUT_ABORT, this._layout.items.slice(0)); + } + + // Update the layout reference. + this._layout = layout; + + // Update the item positions and collect all items that need to be laid + // out. It is critical that we update the item position _before_ the + // layoutStart event as the new data might be needed in the callback. + itemsToLayout.length = 0; + for (i = 0; i < numItems; i++) { + item = layout.items[i]; + + // Make sure we have a matching item. + if (!item) { + --counter; + continue; + } + + // Get the item's new left and top values. + left = layout.slots[i * 2]; + top = layout.slots[i * 2 + 1]; + + // Let's skip the layout process if we can. Possibly avoids a lot of DOM + // operations which saves us some CPU cycles. + if (item._canSkipLayout(left, top)) { + --counter; + continue; + } + + // Update the item's position. + item._left = left; + item._top = top; + + // Only active non-dragged items need to be moved. + if (item.isActive() && !item.isDragging()) { + itemsToLayout.push(item); + } + } + + // Set layout styles to the grid element. + if (layout.styles) { + setStyles(this._element, layout.styles); + } + + // layoutStart event is intentionally emitted after the container element's + // dimensions are set, because otherwise there would be no hook for reacting + // to container dimension changes. + if (this._hasListeners(EVENT_LAYOUT_START)) { + this._emit(EVENT_LAYOUT_START, layout.items.slice(0), instant === true); + } + + function tryFinish() { + if (--counter > 0) return; + + var hasLayoutChanged = grid._layout.id !== layout.id; + var callback = isFunction(instant) ? instant : onFinish; + + if (!hasLayoutChanged) { + grid._isLayoutFinished = true; + } + + if (isFunction(callback)) { + callback(layout.items.slice(0), hasLayoutChanged); + } + + if (!hasLayoutChanged && grid._hasListeners(EVENT_LAYOUT_END)) { + grid._emit(EVENT_LAYOUT_END, layout.items.slice(0)); + } + } + + if (!itemsToLayout.length) { + tryFinish(); + return this; + } + + this._isLayoutFinished = false; + + for (i = 0; i < itemsToLayout.length; i++) { + if (this._layout.id !== layout.id) break; + itemsToLayout[i]._layout.start(instant === true, tryFinish); + } + + if (this._layout.id === layout.id) { + itemsToLayout.length = 0; + } + + return this; + }; +})(); + +/** + * Show or hide Grid instance's items. + * + * @private + * @param {Item[]} items + * @param {Boolean} toVisible + * @param {Object} [options] + * @param {Boolean} [options.instant=false] + * @param {Boolean} [options.syncWithLayout=true] + * @param {Function} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] + */ +Grid.prototype._setItemsVisibility = function (items, toVisible, options) { + var grid = this; + var targetItems = items.slice(0); + var opts = options || {}; + var isInstant = opts.instant === true; + var callback = opts.onFinish; + var layout = opts.layout ? opts.layout : opts.layout === undefined; + var counter = targetItems.length; + var startEvent = toVisible ? EVENT_SHOW_START : EVENT_HIDE_START; + var endEvent = toVisible ? EVENT_SHOW_END : EVENT_HIDE_END; + var method = toVisible ? 'show' : 'hide'; + var needsLayout = false; + var completedItems = []; + var hiddenItems = []; + var item; + var i; + + // If there are no items call the callback, but don't emit any events. + if (!counter) { + if (isFunction(callback)) callback(targetItems); + return; + } + + // Prepare the items. + for (i = 0; i < targetItems.length; i++) { + item = targetItems[i]; + + // If inactive item is shown or active item is hidden we need to do + // layout. + if ((toVisible && !item._isActive) || (!toVisible && item._isActive)) { + needsLayout = true; + } + + // If inactive item is shown we also need to do a little hack to make the + // item not animate it's next positioning (layout). + item._layout._skipNextAnimation = !!(toVisible && !item._isActive); + + // If a hidden item is being shown we need to refresh the item's + // dimensions. + if (toVisible && item._visibility._isHidden) { + hiddenItems.push(item); + } + + // Add item to layout or remove it from layout. + if (toVisible) { + item._addToLayout(); + } else { + item._removeFromLayout(); + } + } + + // Force refresh the dimensions of all hidden items. + if (hiddenItems.length) { + this.refreshItems(hiddenItems, true); + hiddenItems.length = 0; + } + + // Show the items in sync with the next layout. + function triggerVisibilityChange() { + if (needsLayout && opts.syncWithLayout !== false) { + grid.off(EVENT_LAYOUT_START, triggerVisibilityChange); + } + + if (grid._hasListeners(startEvent)) { + grid._emit(startEvent, targetItems.slice(0)); + } + + for (i = 0; i < targetItems.length; i++) { + // Make sure the item is still in the original grid. There is a chance + // that the item starts migrating before tiggerVisibilityChange is called. + if (targetItems[i]._gridId !== grid._id) { + if (--counter < 1) { + if (isFunction(callback)) callback(completedItems.slice(0)); + if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); + } + continue; + } + + targetItems[i]._visibility[method](isInstant, function (interrupted, item) { + // If the current item's animation was not interrupted add it to the + // completedItems array. + if (!interrupted) completedItems.push(item); + + // If all items have finished their animations call the callback + // and emit showEnd/hideEnd event. + if (--counter < 1) { + if (isFunction(callback)) callback(completedItems.slice(0)); + if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); + } + }); + } + } + + // Trigger the visibility change, either async with layout or instantly. + if (needsLayout && opts.syncWithLayout !== false) { + this.on(EVENT_LAYOUT_START, triggerVisibilityChange); + } else { + triggerVisibilityChange(); + } + + // Trigger layout if needed. + if (needsLayout && layout) { + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); + } +}; + +/** + * Private helpers + * *************** + */ + +/** + * Merge default settings with user settings. The returned object is a new + * object with merged values. The merging is a deep merge meaning that all + * objects and arrays within the provided settings objects will be also merged + * so that modifying the values of the settings object will have no effect on + * the returned object. + * + * @param {Object} defaultSettings + * @param {Object} [userSettings] + * @returns {Object} Returns a new object. + */ +function mergeSettings(defaultSettings, userSettings) { + // Create a fresh copy of default settings. + var settings = mergeObjects({}, defaultSettings); + + // Merge user settings to default settings. + if (userSettings) { + settings = mergeObjects(settings, userSettings); + } + + // Handle visible/hidden styles manually so that the whole object is + // overridden instead of the props. + + if (userSettings && userSettings.visibleStyles) { + settings.visibleStyles = userSettings.visibleStyles; + } else if (defaultSettings && defaultSettings.visibleStyles) { + settings.visibleStyles = defaultSettings.visibleStyles; + } + + if (userSettings && userSettings.hiddenStyles) { + settings.hiddenStyles = userSettings.hiddenStyles; + } else if (defaultSettings && defaultSettings.hiddenStyles) { + settings.hiddenStyles = defaultSettings.hiddenStyles; + } + + return settings; +} + +/** + * Merge two objects recursively (deep merge). The source object's properties + * are merged to the target object. + * + * @param {Object} target + * - The target object. + * @param {Object} source + * - The source object. + * @returns {Object} Returns the target object. + */ +function mergeObjects(target, source) { + var sourceKeys = Object.keys(source); + var length = sourceKeys.length; + var isSourceObject; + var propName; + var i; + + for (i = 0; i < length; i++) { + propName = sourceKeys[i]; + isSourceObject = isPlainObject(source[propName]); + + // If target and source values are both objects, merge the objects and + // assign the merged value to the target property. + if (isPlainObject(target[propName]) && isSourceObject) { + target[propName] = mergeObjects(mergeObjects({}, target[propName]), source[propName]); + continue; + } + + // If source's value is object and target's is not let's clone the object as + // the target's value. + if (isSourceObject) { + target[propName] = mergeObjects({}, source[propName]); + continue; + } + + // If source's value is an array let's clone the array as the target's + // value. + if (Array.isArray(source[propName])) { + target[propName] = source[propName].slice(0); + continue; + } + + // In all other cases let's just directly assign the source's value as the + // target's value. + target[propName] = source[propName]; + } + + return target; +} + +/** + * Collect and return initial items for grid. + * + * @param {HTMLElement} gridElement + * @param {?(HTMLElement[]|NodeList|HtmlCollection|String)} elements + * @returns {(HTMLElement[]|NodeList|HtmlCollection)} + */ +function getInitialGridElements(gridElement, elements) { + // If we have a wildcard selector let's return all the children. + if (elements === '*') { + return gridElement.children; + } + + // If we have some more specific selector, let's filter the elements. + if (typeof elements === STRING_TYPE) { + var result = []; + var children = gridElement.children; + for (var i = 0; i < children.length; i++) { + if (elementMatches(children[i], elements)) { + result.push(children[i]); + } + } + return result; + } + + // If we have an array of elements or a node list. + if (Array.isArray(elements) || isNodeList(elements)) { + return elements; + } + + // Otherwise just return an empty array. + return []; +} + +/** + * Bind grid's resize handler to window. + * + * @param {Grid} grid + * @param {(Number|Boolean)} delay + */ +function bindLayoutOnResize(grid, delay) { + if (typeof delay !== NUMBER_TYPE) { + delay = delay === true ? 0 : -1; + } + + if (delay >= 0) { + grid._resizeHandler = debounce(function () { + grid.refreshItems().layout(); + }, delay); + + window.addEventListener('resize', grid._resizeHandler); + } +} + +/** + * Unbind grid's resize handler from window. + * + * @param {Grid} grid + */ +function unbindLayoutOnResize(grid) { + if (grid._resizeHandler) { + grid._resizeHandler(true); + window.removeEventListener('resize', grid._resizeHandler); + grid._resizeHandler = null; + } +} + +/** + * Normalize style declaration object, returns a normalized (new) styles object + * (prefixed properties and invalid properties removed). + * + * @param {Object} styles + * @returns {Object} + */ +function normalizeStyles(styles) { + var normalized = {}; + var docElemStyle = document.documentElement.style; + var prop, prefixedProp; + + // Normalize visible styles (prefix and remove invalid). + for (prop in styles) { + if (!styles[prop]) continue; + prefixedProp = getPrefixedPropName(docElemStyle, prop); + if (!prefixedProp) continue; + normalized[prefixedProp] = styles[prop]; + } + + return normalized; +} + +/** + * Create index map from items. + * + * @param {Item[]} items + * @returns {Object} + */ +function createIndexMap(items) { + var result = {}; + for (var i = 0; i < items.length; i++) { + result[items[i]._id] = i; + } + return result; +} + +/** + * Sort comparer function for items' index map. + * + * @param {Object} indexMap + * @param {Item} itemA + * @param {Item} itemB + * @returns {Number} + */ +function compareIndexMap(indexMap, itemA, itemB) { + var indexA = indexMap[itemA._id]; + var indexB = indexMap[itemB._id]; + return indexA - indexB; +} + +export default Grid; diff --git a/gulpfile.js b/gulpfile.js index 8fff1029..9ae29302 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -4,13 +4,17 @@ const eslint = require('gulp-eslint'); const karma = require('karma'); const size = require('gulp-size'); const rimraf = require('rimraf'); -const argv = require('yargs').argv; const dotenv = require('dotenv'); const exec = require('child_process').exec; const pkg = require('./package.json'); const karmaDefaults = require('./karma.defaults.js'); +function hasArg(arg) { + const args = JSON.parse(process.env.npm_config_argv).original; + return args.indexOf(arg) > -1; +} + if (fs.existsSync('./.env')) dotenv.config(); gulp.task('lint', () => { @@ -50,10 +54,10 @@ gulp.task('clean', cb => { gulp.task('test-local', done => { // Setup browsers. const browsers = []; - argv.chrome && browsers.push('Chrome'); - argv.firefox && browsers.push('Firefox'); - argv.safari && browsers.push('Safari'); - argv.edge && browsers.push('Edge'); + hasArg('--chrome') && browsers.push('Chrome'); + hasArg('--firefox') && browsers.push('Firefox'); + hasArg('--safari') && browsers.push('Safari'); + hasArg('--edge') && browsers.push('Edge'); if (!browsers.length) browsers.push('Chrome'); new karma.Server( @@ -71,10 +75,10 @@ gulp.task('test-local', done => { gulp.task('test-local-min', done => { // Setup browsers. const browsers = []; - argv.chrome && browsers.push('Chrome'); - argv.firefox && browsers.push('Firefox'); - argv.safari && browsers.push('Safari'); - argv.edge && browsers.push('Edge'); + hasArg('--chrome') && browsers.push('Chrome'); + hasArg('--firefox') && browsers.push('Firefox'); + hasArg('--safari') && browsers.push('Safari'); + hasArg('--edge') && browsers.push('Edge'); if (!browsers.length) browsers.push('Chrome'); // Replace main file with minified version. @@ -101,10 +105,10 @@ gulp.task('test-local-min', done => { gulp.task('test-sauce', done => { // Setup browsers. const browsers = []; - argv.chrome && browsers.push('slChrome'); - argv.firefox && browsers.push('slFirefox'); - argv.safari && browsers.push('slSafari'); - argv.edge && browsers.push('slEdge'); + hasArg('--chrome') && browsers.push('slChrome'); + hasArg('--firefox') && browsers.push('slFirefox'); + hasArg('--safari') && browsers.push('slSafari'); + hasArg('--edge') && browsers.push('slEdge'); if (!browsers.length) browsers.push('slChrome', 'slFirefox', 'slSafari'); new karma.Server( @@ -122,10 +126,10 @@ gulp.task('test-sauce', done => { gulp.task('test-sauce-min', done => { // Setup browsers. const browsers = []; - argv.chrome && browsers.push('slChrome'); - argv.firefox && browsers.push('slFirefox'); - argv.safari && browsers.push('slSafari'); - argv.edge && browsers.push('slEdge'); + hasArg('--chrome') && browsers.push('slChrome'); + hasArg('--firefox') && browsers.push('slFirefox'); + hasArg('--safari') && browsers.push('slSafari'); + hasArg('--edge') && browsers.push('slEdge'); if (!browsers.length) browsers.push('slChrome', 'slFirefox', 'slSafari'); // Replace main file with minified version. @@ -149,8 +153,8 @@ gulp.task('test-sauce-min', done => { ).start(); }); -gulp.task('format-test', cb => { - exec('npm run format-test', (err, stdout, stderr) => { +gulp.task('validate-formatting', cb => { + exec('npm run validate-formatting', (err, stdout, stderr) => { console.log(stdout); console.log(stderr); cb(err); @@ -182,14 +186,14 @@ gulp.task( gulp.task( 'pre-commit', - gulp.series('lint', 'format-test', done => { + gulp.series('lint', 'validate-formatting', done => { done(); }) ); gulp.task( 'test', - gulp.series('lint', 'format-test', 'test-sauce', 'test-sauce-min', 'clean', done => { + gulp.series('lint', 'validate-formatting', 'test-sauce', 'test-sauce-min', 'clean', done => { done(); }) ); diff --git a/karma.defaults.js b/karma.defaults.js index 4a3f62b5..923715f5 100644 --- a/karma.defaults.js +++ b/karma.defaults.js @@ -25,10 +25,7 @@ module.exports = { './tests/grid-events/*.js', './tests/item-methods/*.js' ], - reporters: [ - 'story', - 'saucelabs' - ], + reporters: ['story', 'saucelabs'], colors: true, autoWatch: false, browserDisconnectTolerance: 2, @@ -63,4 +60,4 @@ module.exports = { version: 'latest' } } -}; \ No newline at end of file +}; diff --git a/package-lock.json b/package-lock.json index 5693c1d1..4ba5f977 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "muuri", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -24,44 +24,228 @@ "js-tokens": "^4.0.0" } }, - "@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "@babel/runtime": { + "version": "7.9.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz", + "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==", "dev": true }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { - "version": "12.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.2.tgz", - "integrity": "sha512-gojym4tX0FWeV2gsW4Xmzo5wxGjXGm550oVUII7f7G5o4BV6c7DBdiG1RRQd+y1bvqRyYtPfMK85UM95vsapqQ==", + "version": "14.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.10.tgz", + "integrity": "sha512-Bz23oN/5bi0rniKT24ExLf4cK0JdvN3dH/3k0whYkdN4eI4vS2ZW/2ENNn2uxHCzWcbdHIa/GRuWQytfzCjRYw==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@wdio/config": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-6.1.14.tgz", + "integrity": "sha512-MXHMHwtkAblfnIxONs9aW//T9Fq5XIw3oH+tztcBRvNTTAIXmwHd+4sOjAwjpCdBSGs0C4kM/aTpGfwDZVURvQ==", + "dev": true, + "requires": { + "@wdio/logger": "6.0.16", + "deepmerge": "^4.0.0", + "glob": "^7.1.2" + } + }, + "@wdio/logger": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-6.0.16.tgz", + "integrity": "sha512-VbH5UnQIG/3sSMV+Y38+rOdwyK9mVA9vuL7iOngoTafHwUjL1MObfN/Cex84L4mGxIgfxCu6GV48iUmSuQ7sqA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@wdio/protocols": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-6.1.14.tgz", + "integrity": "sha512-UtRLQ55i23cLQRGtFiEJty1F6AbAfiSpfIxDAiXKHbw6Rp1StwxlqHFrhNe5F48Zu4hnie46t9N/tr/cZOe0kA==", + "dev": true + }, + "@wdio/repl": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-6.1.8.tgz", + "integrity": "sha512-C647KvDIcOHYN24eFbiM2xE+etPEACvRYkEp7BPLyopEABDr0I3Qdb5MLhopC5eMAVHp70/WT27H1CE2v9iILQ==", + "dev": true, + "requires": { + "@wdio/utils": "6.1.8" + } + }, + "@wdio/utils": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-6.1.8.tgz", + "integrity": "sha512-qzvD8qCPpIpDrZ0HNOx1hTlfKY26p8WByUXgr52ll6DXxtAYXZLIJ8GAYFJUi87NVfwtv6+O7owQGSM/jtr8AQ==", + "dev": true, + "requires": { + "@wdio/logger": "6.0.16" + } + }, "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", "dev": true, "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" + "mime-types": "~2.1.24", + "negotiator": "0.6.2" } }, "acorn": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.0.tgz", - "integrity": "sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true }, "acorn-jsx": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", - "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", - "dev": true - }, - "adm-zip": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", - "integrity": "sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, "after": { @@ -71,21 +255,18 @@ "dev": true }, "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "dev": true, - "requires": { - "es6-promisify": "^5.0.0" - } + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "dev": true }, "ajv": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.1.tgz", - "integrity": "sha512-w1YQaVGNC6t2UCPjEawK/vo/dG8OOrVtUmhBT1uJJYxbl5kU2Tj3v6LGqBcsysN1yhuCStJCCA3GqdvKY8sqXQ==", + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -110,10 +291,21 @@ } }, "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } }, "ansi-gray": { "version": "0.1.1", @@ -173,6 +365,149 @@ "buffer-equal": "^1.0.0" } }, + "arch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.2.tgz", + "integrity": "sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==", + "dev": true + }, + "archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "dev": true, + "requires": { + "file-type": "^4.2.0" + }, + "dependencies": { + "file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "dev": true + } + } + }, + "archiver": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-4.0.1.tgz", + "integrity": "sha512-/YV1pU4Nhpf/rJArM23W6GTUjT0l++VbjykrCRua1TSXrn+yM8Qs7XvtwSiRse0iCe49EPNf7ktXnPsWuSb91Q==", + "dev": true, + "requires": { + "archiver-utils": "^2.1.0", + "async": "^2.6.3", + "buffer-crc32": "^0.2.1", + "glob": "^7.1.6", + "readable-stream": "^3.6.0", + "tar-stream": "^2.1.2", + "zip-stream": "^3.0.1" + }, + "dependencies": { + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + } + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "tar-stream": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", + "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", + "dev": true, + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + } + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + } + } + }, "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", @@ -315,20 +650,12 @@ "dev": true }, "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "dev": true, "requires": { - "lodash": "^4.17.11" - }, - "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - } + "lodash": "^4.17.14" } }, "async-done": { @@ -350,9 +677,9 @@ "dev": true }, "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "dev": true }, "async-settle": { @@ -364,6 +691,12 @@ "async-done": "^1.2.2" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -460,10 +793,16 @@ "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", "dev": true }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, "base64id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", - "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true }, "better-assert": { @@ -475,53 +814,159 @@ "callsite": "1.0.0" } }, + "bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "dev": true, + "requires": { + "execa": "^0.7.0", + "executable": "^4.1.0" + } + }, + "bin-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz", + "integrity": "sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "find-versions": "^3.0.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "bin-version-check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-4.0.0.tgz", + "integrity": "sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==", + "dev": true, + "requires": { + "bin-version": "^3.0.0", + "semver": "^5.6.0", + "semver-truncate": "^1.1.2" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "bin-wrapper": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-4.1.0.tgz", + "integrity": "sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==", + "dev": true, + "requires": { + "bin-check": "^4.1.0", + "bin-version-check": "^4.0.0", + "download": "^7.1.0", + "import-lazy": "^3.1.0", + "os-filter-obj": "^2.0.0", + "pify": "^4.0.1" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } + }, "binary-extensions": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "blob": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", "dev": true }, - "bluebird": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", - "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==", - "dev": true - }, "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", "dev": true, "requires": { - "bytes": "3.0.0", + "bytes": "3.1.0", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" } }, + "boolean": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz", + "integrity": "sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -561,6 +1006,16 @@ } } }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", @@ -577,6 +1032,12 @@ "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", "dev": true }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", @@ -602,9 +1063,9 @@ "dev": true }, "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true }, "cache-base": { @@ -624,19 +1085,31 @@ "unset-value": "^1.0.0" } }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "cacheable-lookup": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz", + "integrity": "sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==", + "dev": true + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", "dev": true, "requires": { - "callsites": "^2.0.0" + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" }, "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", "dev": true } } @@ -653,12 +1126,45 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camel-case": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", + "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", + "dev": true, + "requires": { + "pascal-case": "^3.1.1", + "tslib": "^1.10.0" + } + }, "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", "dev": true }, + "capital-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.3.tgz", + "integrity": "sha512-OlUSJpUr7SY0uZFOxcwnDOU7/MpHlKTZx2mqnDYQFrDudXLFm0JJ9wr/l4csB+rh2Ug0OPuoSO53PqiZBqno9A==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0", + "upper-case-first": "^2.0.1" + } + }, + "caw": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", + "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", + "dev": true, + "requires": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + } + }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -670,6 +1176,26 @@ "supports-color": "^5.3.0" } }, + "change-case": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.1.tgz", + "integrity": "sha512-qRlUWn/hXnX1R1LBDF/RelJLiqNjKjUqlmuBVSEIyye8kq49CXqkZWKmi8XeUAdDXWFOcGLUMZ+aHn3Q5lzUXw==", + "dev": true, + "requires": { + "camel-case": "^4.1.1", + "capital-case": "^1.0.3", + "constant-case": "^3.0.3", + "dot-case": "^3.0.3", + "header-case": "^2.0.3", + "no-case": "^3.0.3", + "param-case": "^3.0.3", + "pascal-case": "^3.1.1", + "path-case": "^3.0.3", + "sentence-case": "^3.0.3", + "snake-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -698,11 +1224,35 @@ } }, "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "chrome-launcher": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.2.tgz", + "integrity": "sha512-zWD9RVVKd8Nx2xKGY4G08lb3nCD+2hmICxovvRE9QjBKQzHFvCYqGlsw15b4zUxLKq3wXEwVbR/yLtMbfk7JbQ==", "dev": true, - "optional": true + "requires": { + "@types/node": "*", + "escape-string-regexp": "^1.0.5", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^0.5.3", + "rimraf": "^3.0.2" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + } + } }, "ci-info": { "version": "2.0.0", @@ -734,18 +1284,18 @@ } }, "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "requires": { - "restore-cursor": "^2.0.0" + "restore-cursor": "^3.1.0" } }, "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "dev": true }, "cliui": { @@ -771,6 +1321,15 @@ "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", "dev": true }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "clone-stats": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", @@ -837,17 +1396,32 @@ "dev": true }, "colors": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", - "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", "dev": true }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", @@ -866,6 +1440,41 @@ "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", "dev": true }, + "compress-commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-3.0.0.tgz", + "integrity": "sha512-FyDqr8TKX5/X0qo+aVfaZ+PVmNJHJeckFBlq8jZGSJOgnynhfifoyl24qaqdUdDIBe0EVTHByN6NAkqYvE/2Xg==", + "dev": true, + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^3.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^2.3.7" + }, + "dependencies": { + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -884,18 +1493,48 @@ "typedarray": "^0.0.6" } }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, "requires": { "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "~1.3.2", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, + "constant-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.3.tgz", + "integrity": "sha512-FXtsSnnrFYpzDmvwDGQW+l8XK3GV1coLyBN0eBz16ZUzGaZcT2ANVCJmLeuw2GQgxKHQIe9e0w2dzkSfaRlUmA==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0", + "upper-case": "^2.0.1" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -934,9 +1573,9 @@ } }, "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==", + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", "dev": true }, "core-util-is": { @@ -946,26 +1585,66 @@ "dev": true }, "cosmiconfig": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.1.0.tgz", - "integrity": "sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", "dev": true, "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.9.0", - "lodash.get": "^4.4.2", - "parse-json": "^4.0.0" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" }, "dependencies": { "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", "dev": true, "requires": { + "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "requires": { + "buffer": "^5.1.0" + } + }, + "crc32-stream": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz", + "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==", + "dev": true, + "requires": { + "crc": "^3.4.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } } } @@ -983,6 +1662,12 @@ "which": "^1.2.9" } }, + "css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo=", + "dev": true + }, "custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", @@ -1000,9 +1685,9 @@ } }, "date-format": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.0.0.tgz", - "integrity": "sha512-M6UqVvZVgFYqZL1SfHsRGIQSz3ZL+qgbsV5Lp1Vj61LZVYuEwcMXYay7DRDtYs2HQQBK5hQtQ0fD9aEJ89V0LA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz", + "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==", "dev": true }, "debug": { @@ -1026,12 +1711,132 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "dev": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + } + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, "default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -1055,6 +1860,12 @@ "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", "dev": true }, + "defer-to-connect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==", + "dev": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1105,6 +1916,12 @@ } } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -1117,6 +1934,28 @@ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "dev": true }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "devtools": { + "version": "6.1.16", + "resolved": "https://registry.npmjs.org/devtools/-/devtools-6.1.16.tgz", + "integrity": "sha512-Px/K/xYY+fTW8D5yt7p6ZZJfkfHHulKVr2Y+BJSCQyKNSY/hiZFT6KAjoUFrAastLCqqs1gW2Dy/OGb0qWm+Hg==", + "dev": true, + "requires": { + "@wdio/config": "6.1.14", + "@wdio/logger": "6.0.16", + "@wdio/protocols": "6.1.14", + "@wdio/utils": "6.1.8", + "chrome-launcher": "^0.13.1", + "puppeteer-core": "^3.0.0", + "ua-parser-js": "^0.7.21", + "uuid": "^8.0.0" + } + }, "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -1144,11 +1983,74 @@ "void-elements": "^2.0.0" } }, + "dot-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz", + "integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, "dotenv": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", - "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==", - "dev": true + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true + }, + "download": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", + "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "dev": true, + "requires": { + "archive-type": "^4.0.0", + "caw": "^2.0.1", + "content-disposition": "^0.5.2", + "decompress": "^4.2.0", + "ext-name": "^5.0.0", + "file-type": "^8.1.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^8.3.1", + "make-dir": "^1.2.0", + "p-event": "^2.1.0", + "pify": "^3.0.0" + }, + "dependencies": { + "got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } }, "duplexer": { "version": "0.1.1", @@ -1156,6 +2058,12 @@ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -1191,9 +2099,9 @@ "dev": true }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "encodeurl": { @@ -1212,64 +2120,91 @@ } }, "engine.io": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", - "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", + "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", "dev": true, "requires": { "accepts": "~1.3.4", - "base64id": "1.0.0", + "base64id": "2.0.0", "cookie": "0.3.1", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "ws": "~3.3.1" + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "ws": "^7.1.2" }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, "engine.io-client": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", - "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz", + "integrity": "sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==", "dev": true, "requires": { - "component-emitter": "1.2.1", + "component-emitter": "~1.3.0", "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", "has-cors": "1.1.0", "indexof": "0.0.1", "parseqs": "0.0.5", "parseuri": "0.0.5", - "ws": "~3.3.1", + "ws": "~6.1.0", "xmlhttprequest-ssl": "~1.5.4", "yeast": "0.1.2" }, "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", "dev": true, "requires": { - "ms": "2.0.0" + "async-limiter": "~1.0.0" } } } }, "engine.io-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", - "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", + "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", "dev": true, "requires": { "after": "0.8.2", @@ -1305,30 +2240,21 @@ "next-tick": "^1.0.0" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", - "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==", - "dev": true - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "dev": true, "requires": { - "es6-promise": "^4.0.3" + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" } }, "es6-symbol": { @@ -1366,9 +2292,9 @@ "dev": true }, "eslint": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.0.1.tgz", - "integrity": "sha512-DyQRaMmORQ+JsWShYsSg4OPTjY56u1nCjAmICrE8vLWqyLKxhFXOthwMj1SA8xwfrv0CofLNVnqbfyhwCkaO0w==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -1377,42 +2303,43 @@ "cross-spawn": "^6.0.5", "debug": "^4.0.1", "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^6.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^3.1.0", - "globals": "^11.7.0", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", + "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", - "lodash": "^4.17.11", + "lodash": "^4.17.14", "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.2", + "optionator": "^0.8.3", "progress": "^2.0.0", "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", "table": "^5.2.3", - "text-table": "^0.2.0" + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "debug": { @@ -1424,14 +2351,24 @@ "ms": "^2.1.1" } }, - "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "is-glob": "^4.0.1" + }, + "dependencies": { + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + } } }, "ms": { @@ -1440,21 +2377,27 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } } } }, "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -1462,26 +2405,29 @@ } }, "eslint-utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", - "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", - "dev": true + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } }, "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", "dev": true }, "espree": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.0.0.tgz", - "integrity": "sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" } }, "esprima": { @@ -1491,12 +2437,20 @@ "dev": true }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } } }, "esrecurse": { @@ -1509,9 +2463,9 @@ } }, "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, "esutils": { @@ -1521,24 +2475,46 @@ "dev": true }, "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", + "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==", "dev": true }, "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "requires": { + "pify": "^2.2.0" } }, "expand-brackets": { @@ -1585,6 +2561,25 @@ "homedir-polyfill": "^1.0.1" } }, + "ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "requires": { + "mime-db": "^1.28.0" + } + }, + "ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "requires": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1688,6 +2683,54 @@ } } }, + "extract-zip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.0.tgz", + "integrity": "sha512-i42GQ498yibjdvIhivUsRslx608whtGoFIhF26Z7O4MYncBxp8CwalOs1lnHy21A9sIohWO2+uiE4SRtC9JXDg==", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "fancy-log": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", @@ -1700,15 +2743,15 @@ } }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "dev": true }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "fast-levenshtein": { @@ -1717,10 +2760,19 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -1735,6 +2787,36 @@ "flat-cache": "^2.0.1" } }, + "file-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", + "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", + "dev": true + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "dev": true + }, + "filenamify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", + "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -1759,26 +2841,18 @@ } }, "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.1", + "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.3.1", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", "unpipe": "~1.0.0" - }, - "dependencies": { - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", - "dev": true - } } }, "find-up": { @@ -1791,6 +2865,15 @@ "pinkie-promise": "^2.0.0" } }, + "find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "requires": { + "semver-regex": "^2.0.0" + } + }, "findup-sync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", @@ -1831,6 +2914,17 @@ "flatted": "^2.0.0", "rimraf": "2.6.3", "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "flatted": { @@ -1850,12 +2944,12 @@ } }, "follow-redirects": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", - "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz", + "integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==", "dev": true, "requires": { - "debug": "^3.2.6" + "debug": "^3.0.0" }, "dependencies": { "debug": { @@ -1868,9 +2962,9 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } @@ -1890,6 +2984,17 @@ "for-in": "^1.0.1" } }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -1899,34 +3004,39 @@ "map-cache": "^0.2.2" } }, - "fs-access": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", - "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", "dev": true, "requires": { - "null-check": "^1.0.0" + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", + "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + } } }, "fs-mkdirp-stream": { @@ -1946,41 +3056,38 @@ "dev": true }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", + "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", "dev": true, "optional": true, "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" + "bindings": "^1.5.0", + "nan": "^2.12.1", + "node-pre-gyp": "*" }, "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "bundled": true, "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "bundled": true, "dev": true, "optional": true }, "aproba": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "bundled": true, "dev": true, "optional": true }, "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "version": "1.1.5", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -1990,15 +3097,13 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "bundled": true, "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2006,76 +3111,81 @@ "concat-map": "0.0.1" } }, + "chownr": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true + }, "code-point-at": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "bundled": true, "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "bundled": true, "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "bundled": true, "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "bundled": true, "dev": true, "optional": true }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.2.6", + "bundled": true, "dev": true, "optional": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", + "version": "0.6.0", + "bundled": true, "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "bundled": true, "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "bundled": true, "dev": true, "optional": true }, + "fs-minipass": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.6.0" + } + }, "fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "bundled": true, "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2090,9 +3200,8 @@ } }, "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "version": "7.1.6", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2106,25 +3215,22 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "bundled": true, "dev": true, "optional": true }, "iconv-lite": { - "version": "0.4.21", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "version": "0.4.24", + "bundled": true, "dev": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "version": "3.0.3", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2133,8 +3239,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2143,23 +3248,20 @@ } }, "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "version": "2.0.4", + "bundled": true, "dev": true, "optional": true }, "ini": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "bundled": true, "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2168,15 +3270,13 @@ }, "isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "bundled": true, "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2185,15 +3285,32 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "bundled": true, "dev": true, "optional": true }, + "minipass": { + "version": "2.9.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.9.0" + } + }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2201,47 +3318,43 @@ } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "version": "2.1.2", + "bundled": true, "dev": true, "optional": true }, "needle": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", - "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", + "version": "2.4.0", + "bundled": true, "dev": true, "optional": true, "requires": { - "debug": "^2.1.2", + "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, "node-pre-gyp": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", - "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", + "version": "0.14.0", + "bundled": true, "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.0", + "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.1.7", + "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", - "tar": "^4" + "tar": "^4.4.2" } }, "nopt": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2250,16 +3363,23 @@ } }, "npm-bundled": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", - "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true, "dev": true, "optional": true }, "npm-packlist": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", - "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", + "version": "1.4.7", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2269,8 +3389,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2282,22 +3401,19 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "bundled": true, "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "bundled": true, "dev": true, "optional": true }, "once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2306,22 +3422,19 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "bundled": true, "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "bundled": true, "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2331,26 +3444,23 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "bundled": true, "dev": true, "optional": true }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "version": "2.0.1", + "bundled": true, "dev": true, "optional": true }, "rc": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", - "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", + "version": "1.2.8", + "bundled": true, "dev": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -2358,8 +3468,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "bundled": true, "dev": true, "optional": true } @@ -2367,8 +3476,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2382,61 +3490,53 @@ } }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "2.7.1", + "bundled": true, "dev": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "version": "5.1.2", + "bundled": true, "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "bundled": true, "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "bundled": true, "dev": true, "optional": true }, "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "version": "5.7.1", + "bundled": true, "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "bundled": true, "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "bundled": true, "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2447,8 +3547,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2457,8 +3556,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2467,32 +3565,49 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "bundled": true, "dev": true, "optional": true }, + "tar": { + "version": "4.4.13", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, "util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "bundled": true, "dev": true, "optional": true }, "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "version": "1.1.3", + "bundled": true, "dev": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.1.1", + "bundled": true, "dev": true, "optional": true } @@ -2516,33 +3631,21 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "get-proxy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", + "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", "dev": true, "requires": { - "pump": "^3.0.0" - }, - "dependencies": { - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } + "npm-conf": "^1.1.0" } }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -2616,6 +3719,29 @@ "object.defaults": "^1.1.0" } }, + "global-agent": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.1.12.tgz", + "integrity": "sha512-caAljRMS/qcDo69X9BfkgrihGUgGx44Fb4QQToNQjsiWh+YlQ66uqYVAdA8Olqit+5Ng0nkz09je3ZzANMZcjg==", + "dev": true, + "requires": { + "boolean": "^3.0.1", + "core-js": "^3.6.5", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, "global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", @@ -2641,10 +3767,22 @@ } }, "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "globalthis": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.1.tgz", + "integrity": "sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } }, "glogg": { "version": "1.0.2", @@ -2655,12 +3793,149 @@ "sparkles": "^1.0.0" } }, + "got": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/got/-/got-11.2.0.tgz", + "integrity": "sha512-68pBow9XXXSdVRV5wSx0kWMCZsag4xE3Ru0URVe0PWsSYmU4SJrUmEO6EVYFlFHc9rq/6Yqn6o1GxIb9torQxg==", + "dev": true, + "requires": { + "@sindresorhus/is": "^2.1.1", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "get-stream": "^5.1.0", + "http2-wrapper": "^1.0.0-beta.4.5", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz", + "integrity": "sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg==", + "dev": true + }, + "cacheable-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "keyv": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.1.tgz", + "integrity": "sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true + }, + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + } + } + }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "dev": true }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "gulp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", @@ -2856,12 +4131,27 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true + }, "has-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", "dev": true }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "requires": { + "has-symbol-support-x": "^1.4.1" + } + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -2894,6 +4184,26 @@ } } }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "header-case": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.3.tgz", + "integrity": "sha512-LChe/V32mnUQnTwTxd3aAlNMk8ia9tjCDb/LjYtoMrdAPApxLB+azejUk5ERZIZdIqvinwv6BAUuFXH/tQPdZA==", + "dev": true, + "requires": { + "capital-case": "^1.0.3", + "tslib": "^1.10.0" + } + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -2909,108 +4219,139 @@ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "dev": true, "requires": { "depd": "~1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" } }, "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "requires": { - "eventemitter3": "^3.0.0", + "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, + "http2-wrapper": { + "version": "1.0.0-beta.4.6", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.4.6.tgz", + "integrity": "sha512-9oB4BiGDTI1FmIBlOF9OJ5hwJvcBEmPCqk/hy314Uhy2uq5TjekUZM8w8SPLLlUEM+mxNhXdPAXfrJN2Zbb/GQ==", + "dev": true, + "requires": { + "quick-lru": "^5.0.0", + "resolve-alpn": "^1.0.0" + } + }, "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", "dev": true, "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" + "agent-base": "5", + "debug": "4" }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, "husky": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/husky/-/husky-1.3.1.tgz", - "integrity": "sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.7", - "execa": "^1.0.0", - "find-up": "^3.0.0", - "get-stdin": "^6.0.0", - "is-ci": "^2.0.0", - "pkg-dir": "^3.0.0", - "please-upgrade-node": "^3.1.1", - "read-pkg": "^4.0.1", - "run-node": "^1.0.0", - "slash": "^2.0.0" + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", + "integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "compare-versions": "^3.6.0", + "cosmiconfig": "^6.0.0", + "find-versions": "^3.2.0", + "opencollective-postinstall": "^2.0.2", + "pkg-dir": "^4.2.0", + "please-upgrade-node": "^3.2.0", + "slash": "^3.0.0", + "which-pm-runs": "^1.0.0" }, "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } }, - "parse-json": { + "chalk": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", "dev": true, "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "read-pkg": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", - "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, "requires": { - "normalize-package-data": "^2.3.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0" + "has-flag": "^4.0.0" } } } @@ -3024,45 +4365,34 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", - "dev": true - }, "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "dependencies": { - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" } }, + "import-lazy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", + "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==", + "dev": true + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3098,91 +4428,106 @@ "dev": true }, "inquirer": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz", - "integrity": "sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", "dev": true, "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } } } @@ -3193,6 +4538,16 @@ "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", "dev": true }, + "into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "dev": true, + "requires": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + } + }, "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -3259,15 +4614,6 @@ "builtin-modules": "^1.0.0" } }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -3307,10 +4653,10 @@ } } }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "is-docker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", + "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", "dev": true }, "is-extendable": { @@ -3343,6 +4689,12 @@ "is-extglob": "^2.1.1" } }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "dev": true + }, "is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", @@ -3369,6 +4721,18 @@ } } }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -3393,6 +4757,12 @@ "is-unc-path": "^1.0.0" } }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -3426,6 +4796,12 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "is-wsl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", + "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -3433,13 +4809,10 @@ "dev": true }, "isbinaryfile": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", - "dev": true, - "requires": { - "buffer-alloc": "^1.2.0" - } + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.6.tgz", + "integrity": "sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg==", + "dev": true }, "isexe": { "version": "2.0.0", @@ -3453,6 +4826,16 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "requires": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + } + }, "js-reporters": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/js-reporters/-/js-reporters-1.2.1.tgz", @@ -3475,6 +4858,12 @@ "esprima": "^4.0.0" } }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -3493,6 +4882,12 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -3502,18 +4897,6 @@ "graceful-fs": "^4.1.6" } }, - "jszip": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.0.tgz", - "integrity": "sha512-4WjbsaEtBK/DHeDZOPiPw5nzSGLDEDDreFRDEgnoMwmknPjTqa+23XuYFk6NiGbeiAeZCctiQ/X/z0lQBmDVOQ==", - "dev": true, - "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" - } - }, "just-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", @@ -3521,61 +4904,200 @@ "dev": true }, "karma": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/karma/-/karma-4.1.0.tgz", - "integrity": "sha512-xckiDqyNi512U4dXGOOSyLKPwek6X/vUizSy2f3geYevbLj+UIdvNwbn7IwfUIL2g1GXEPWt/87qFD1fBbl/Uw==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/karma/-/karma-5.0.9.tgz", + "integrity": "sha512-dUA5z7Lo7G4FRSe1ZAXqOINEEWxmCjDBbfRBmU/wYlSMwxUQJP/tEEP90yJt3Uqo03s9rCgVnxtlfq+uDhxSPg==", "dev": true, "requires": { - "bluebird": "^3.3.0", - "body-parser": "^1.16.1", - "braces": "^2.3.2", - "chokidar": "^2.0.3", - "colors": "^1.1.0", - "connect": "^3.6.0", - "core-js": "^2.2.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.0.0", + "colors": "^1.4.0", + "connect": "^3.7.0", "di": "^0.0.1", - "dom-serialize": "^2.2.0", - "flatted": "^2.0.0", - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "http-proxy": "^1.13.0", - "isbinaryfile": "^3.0.0", - "lodash": "^4.17.11", - "log4js": "^4.0.0", - "mime": "^2.3.1", - "minimatch": "^3.0.2", - "optimist": "^0.6.1", - "qjobs": "^1.1.4", - "range-parser": "^1.2.0", - "rimraf": "^2.6.0", - "safe-buffer": "^5.0.1", - "socket.io": "2.1.1", + "dom-serialize": "^2.2.1", + "flatted": "^2.0.2", + "glob": "^7.1.6", + "graceful-fs": "^4.2.4", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.6", + "lodash": "^4.17.15", + "log4js": "^6.2.1", + "mime": "^2.4.5", + "minimatch": "^3.0.4", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^2.3.0", "source-map": "^0.6.1", - "tmp": "0.0.33", - "useragent": "2.3.0" + "tmp": "0.2.1", + "ua-parser-js": "0.7.21", + "yargs": "^15.3.1" }, "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", + "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } } } }, "karma-chrome-launcher": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz", - "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", + "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", "dev": true, "requires": { - "fs-access": "^1.0.0", "which": "^1.2.1" } }, @@ -3589,15 +5111,18 @@ } }, "karma-firefox-launcher": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz", - "integrity": "sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.3.0.tgz", + "integrity": "sha512-Fi7xPhwrRgr+94BnHX0F5dCl1miIW4RHnzjIGxF8GaIEp7rNqX7LSi7ok63VXs3PS/5MQaQMhGxw+bvD+pibBQ==", + "dev": true, + "requires": { + "is-wsl": "^2.1.0" + } }, "karma-qunit": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/karma-qunit/-/karma-qunit-3.1.2.tgz", - "integrity": "sha512-3m3zP7NpYkOCxoJ5SQyHQowLSiqz9sNEa1/jJmtgt+U4akxm58lgsXDpdquNgQ8dzs65kEkQIt5ihEoVe2SHEQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/karma-qunit/-/karma-qunit-4.1.1.tgz", + "integrity": "sha512-sl4rSTGK1fXWyXdNu8ySQvsf77OuJ1gQg9gbHIj0V05h4FvcNogpirO7R4ijU4L7ioXk8WupvSJZHE7QI9B7uQ==", "dev": true }, "karma-safari-launcher": { @@ -3607,14 +5132,14 @@ "dev": true }, "karma-sauce-launcher": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/karma-sauce-launcher/-/karma-sauce-launcher-2.0.2.tgz", - "integrity": "sha512-jLUFaJhHMcKpxFWUesyWYihzM5FvQiJsDwGcCtKeOy2lsWhkVw0V0Byqb1d+wU6myU1mribBtsIcub23HS4kWA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/karma-sauce-launcher/-/karma-sauce-launcher-4.1.5.tgz", + "integrity": "sha512-X6/1IaUTOhbSuw+Y8ea8/1fAHsIbitDosJVnxqC0ZxIlDQMM4/U0WbETtCIbwUDoa89jwM3alfDs5RTRUW5mJw==", "dev": true, "requires": { - "sauce-connect-launcher": "^1.2.4", - "saucelabs": "^1.5.0", - "selenium-webdriver": "^4.0.0-alpha.1" + "global-agent": "^2.1.8", + "saucelabs": "^4.3.0", + "webdriverio": "^6.1.9" } }, "karma-story-reporter": { @@ -3634,10 +5159,19 @@ } } }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, "last-run": { @@ -3687,15 +5221,6 @@ "type-check": "~0.3.2" } }, - "lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "requires": { - "immediate": "~3.0.5" - } - }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -3712,6 +5237,22 @@ "resolve": "^1.1.7" } }, + "lighthouse-logger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz", + "integrity": "sha512-wzUvdIeJZhRsG6gpZfmSCfysaxNEr43i+QT+Hie94wvHDKFLi4n7C2GqZ4sTC+PH5b5iktmXJvU87rWvhP3lHw==", + "dev": true, + "requires": { + "debug": "^2.6.8", + "marky": "^1.2.0" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -3726,27 +5267,24 @@ } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } + "p-locate": "^4.1.0" } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, "lodash.debounce": { @@ -3755,23 +5293,65 @@ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", + "dev": true + }, + "lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=", "dev": true }, "log4js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.1.0.tgz", - "integrity": "sha512-eDa+zZPeVEeK6QGJAePyXM6pg4P3n3TO5rX9iZMVY48JshsTyLJZLIL5HipI1kQ2qLsSyOpUqNND/C5H4WhhiA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz", + "integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==", "dev": true, "requires": { - "date-format": "^2.0.0", + "date-format": "^3.0.0", "debug": "^4.1.1", - "flatted": "^2.0.0", - "rfdc": "^1.1.2", - "streamroller": "^1.0.4" + "flatted": "^2.0.1", + "rfdc": "^1.1.4", + "streamroller": "^2.2.4" }, "dependencies": { "debug": { @@ -3783,14 +5363,47 @@ "ms": "^2.1.1" } }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, + "loglevel": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz", + "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==", + "dev": true + }, + "loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true + }, + "lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -3801,6 +5414,23 @@ "yallist": "^2.1.2" } }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -3810,15 +5440,6 @@ "kind-of": "^6.0.2" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -3834,6 +5455,12 @@ "object-visit": "^1.0.0" } }, + "marky": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.1.tgz", + "integrity": "sha512-md9k+Gxa3qLH6sUKpeC2CNkJK/Ld+bEz5X96nYwloqphQE0CKCVEKco/6jxEZixinqNdz5RFi/KaCyfbMDMAXQ==", + "dev": true + }, "matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", @@ -3869,31 +5496,29 @@ } } }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "dev": true, "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" + "escape-string-regexp": "^4.0.0" }, "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true } } }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, "mezr": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/mezr/-/mezr-0.6.2.tgz", @@ -3922,30 +5547,42 @@ } }, "mime": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.2.tgz", - "integrity": "sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", "dev": true }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", "dev": true }, "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", "dev": true, "requires": { - "mime-db": "1.40.0" + "mime-db": "1.44.0" } }, "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "dev": true }, "minimatch": { @@ -3957,46 +5594,10 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true, - "optional": true - } - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -4015,14 +5616,28 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4036,15 +5651,15 @@ "dev": true }, "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, "nan": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", - "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", "dev": true, "optional": true }, @@ -4074,9 +5689,9 @@ "dev": true }, "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", "dev": true }, "next-tick": { @@ -4091,10 +5706,20 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dev": true, + "requires": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + }, "node-watch": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.6.0.tgz", - "integrity": "sha512-XAgTL05z75ptd7JSVejH1a2Dm1zmXYhuDr9l230Qk6Z7/7GPcnAs/UyJJ4ggsXSvWil8iOzwQLW0zuGUvHpG8g==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.6.1.tgz", + "integrity": "sha512-gwQiR7weFRV8mAtT0x0kXkZ18dfRLB45xH7q0hCOVQMLfLb2f1ZaSvR57q4/b/Vj6B0RwMNJYbvb69e1yM7qEA==", "dev": true }, "normalize-package-data": { @@ -4118,6 +5743,28 @@ "remove-trailing-separator": "^1.0.1" } }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "dependencies": { + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } + } + }, "now-and-later": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", @@ -4127,6 +5774,24 @@ "once": "^1.3.2" } }, + "npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "requires": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -4136,18 +5801,18 @@ "path-key": "^2.0.0" } }, - "null-check": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", - "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", - "dev": true - }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, "object-component": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", @@ -4272,44 +5937,32 @@ } }, "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "mimic-fn": "^2.1.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } + "opencollective-postinstall": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", + "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==", + "dev": true }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" + "word-wrap": "~1.2.3" } }, "ordered-read-streams": { @@ -4321,6 +5974,15 @@ "readable-stream": "^2.0.1" } }, + "os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "requires": { + "arch": "^2.1.0" + } + }, "os-locale": { "version": "1.4.0", "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -4336,12 +5998,21 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", "dev": true }, + "p-event": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", + "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", + "dev": true, + "requires": { + "p-timeout": "^2.0.1" + } + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -4349,40 +6020,53 @@ "dev": true }, "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", "dev": true }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" } }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-finally": "^1.0.0" } }, "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true + "param-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz", + "integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==", + "dev": true, + "requires": { + "dot-case": "^3.0.3", + "tslib": "^1.10.0" + } }, "parent-module": { "version": "1.0.1", @@ -4443,12 +6127,32 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true }, + "pascal-case": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz", + "integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true }, + "path-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.3.tgz", + "integrity": "sha512-UMFU6UETFpCNWbIWNczshPrnK/7JAXBP2NYw80ojElbQ2+JYxdqWDBkvvqM93u4u6oLmuJ/tPOf2tM8KtXv4eg==", + "dev": true, + "requires": { + "dot-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, "path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", @@ -4508,6 +6212,18 @@ "pinkie-promise": "^2.0.0" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -4530,29 +6246,36 @@ } }, "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "requires": { - "find-up": "^3.0.0" + "find-up": "^4.0.0" }, "dependencies": { "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true } } }, "please-upgrade-node": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz", - "integrity": "sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", "dev": true, "requires": { "semver-compare": "^1.0.0" @@ -4582,10 +6305,16 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", "dev": true }, "pretty-bytes": { @@ -4618,6 +6347,18 @@ "integrity": "sha1-qhPUqZ1aOcXRaMviB7zOV26EpzI=", "dev": true }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -4651,6 +6392,41 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "puppeteer-core": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-3.3.0.tgz", + "integrity": "sha512-hynQ3r0J/lkGrKeBCqu160jrj0WhthYLIzDQPkBxLzxPokjw4elk1sn6mXAian/kfD2NRzpdh9FSykxZyL56uA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^4.0.0", + "mime": "^2.0.3", + "progress": "^2.0.1", + "proxy-from-env": "^1.0.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", @@ -4658,21 +6434,38 @@ "dev": true }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true }, "qunit": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.9.2.tgz", - "integrity": "sha512-wTOYHnioWHcx5wa85Wl15IE7D6zTZe2CQlsodS14yj7s2FZ3MviRnQluspBZsueIDEO7doiuzKlv05yfky1R7w==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.10.0.tgz", + "integrity": "sha512-EP9Q9Kf45z4l/X02ZJtyTQU9DBc82pEWAncSNx7Weo/73BDpX71xqbsdDAQrtEeeilK70cib7CY/lniJV6Cwwg==", "dev": true, "requires": { "commander": "2.12.2", "js-reporters": "1.2.1", "minimatch": "3.0.4", - "node-watch": "0.6.0", + "node-watch": "0.6.1", "resolve": "1.9.0" }, "dependencies": { @@ -4688,32 +6481,21 @@ } }, "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true }, "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", "dev": true, "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "unpipe": "1.0.0" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } } }, "read-pkg": { @@ -4781,6 +6563,12 @@ "resolve": "^1.1.6" } }, + "regenerator-runtime": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "dev": true + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -4880,6 +6668,12 @@ "path-parse": "^1.0.6" } }, + "resolve-alpn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", + "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==", + "dev": true + }, "resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", @@ -4911,13 +6705,39 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "resq": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.7.1.tgz", + "integrity": "sha512-09u9Q5SAuJfAW5UoVAmvRtLvCOMaKP+djiixTXsZvPaojGKhuvc0Nfvp84U1rIfopJWEOXi5ywpCFwCk7mj8Xw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1" + }, + "dependencies": { + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + } + } + }, "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "requires": { - "onetime": "^2.0.0", + "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, @@ -4928,50 +6748,79 @@ "dev": true }, "rfdc": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz", - "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", + "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==", + "dev": true + }, + "rgb2hex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.0.tgz", + "integrity": "sha512-cHdNTwmTMPu/TpP1bJfdApd6MbD+Kzi4GNnM6h35mdFChhQPSi9cAI8J7DMn5kQDKX8NuBaQXAyo360Oa7tOEA==", "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" } }, + "roarr": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.3.tgz", + "integrity": "sha512-AEjYvmAhlyxOeB9OqPUzQCo3kuAkNfuDk/HqWbZdFsqDFpapkTjiw+p4svNEoRLvuqNTxqfL+s+gtD4eDgZ+CA==", + "dev": true, + "requires": { + "boolean": "^3.0.0", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, "rollup": { - "version": "1.16.7", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.16.7.tgz", - "integrity": "sha512-P3GVcbVSLLjHWFLKGerYRe3Q/yggRXmTZFx/4WZf4wzGwO6hAg5jyMAFMQKc0dts8rFID4BQngfoz6yQbI7iMQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.15.0.tgz", + "integrity": "sha512-HAk4kyXiV5sdNDnbKWk5zBPnkX/DAgx09Kbp8rRIRDVsTUVN3vnSowR7ZHkV6/lAiE6c2TQ8HtYb72aCPGW4Jw==", "dev": true, "requires": { - "@types/estree": "0.0.39", - "@types/node": "^12.0.10", - "acorn": "^6.1.1" + "fsevents": "~2.1.2" + }, + "dependencies": { + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + } } }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", "dev": true, "requires": { "is-promise": "^2.1.0" } }, - "run-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", - "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", - "dev": true - }, "rxjs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", - "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -4998,53 +6847,37 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "sauce-connect-launcher": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sauce-connect-launcher/-/sauce-connect-launcher-1.2.4.tgz", - "integrity": "sha512-X2vfwulR6brUGiicXKxPm1GJ7dBEeP1II450Uv4bHGrcGOapZNgzJvn9aioea5IC5BPp/7qjKdE3xbbTBIVXMA==", - "dev": true, - "requires": { - "adm-zip": "~0.4.3", - "async": "^2.1.2", - "https-proxy-agent": "^2.2.1", - "lodash": "^4.16.6", - "rimraf": "^2.5.4" - } - }, "saucelabs": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", - "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-4.4.0.tgz", + "integrity": "sha512-qSIIi1uSt++aaqhehLNmVbmSzjEsZj+pNlua6YpK0UChKLlyXSXNCMsmSSA9hfzD+PrAWQMNYGrikOQvGUrtrw==", "dev": true, "requires": { - "https-proxy-agent": "^2.2.1" + "bin-wrapper": "^4.1.0", + "change-case": "^4.1.1", + "form-data": "^3.0.0", + "got": "^11.1.4", + "hash.js": "^1.1.7", + "tunnel": "0.0.6", + "yargs": "^15.3.1" } }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "selenium-webdriver": { - "version": "4.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.1.tgz", - "integrity": "sha512-z88rdjHAv3jmTZ7KSGUkTvo4rGzcDGMq0oXWHNIDK96Gs31JKVdu9+FMtT4KBrVoibg8dUicJDok6GnqqttO5Q==", + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", "dev": true, "requires": { - "jszip": "^3.1.3", - "rimraf": "^2.5.4", - "tmp": "0.0.30", - "xml2js": "^0.4.17" + "commander": "~2.8.1" }, "dependencies": { - "tmp": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", - "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "dev": true, "requires": { - "os-tmpdir": "~1.0.1" + "graceful-readlink": ">= 1.0.0" } } } @@ -5070,6 +6903,49 @@ "sver-compat": "^1.5.0" } }, + "semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true + }, + "semver-truncate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz", + "integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, + "sentence-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.3.tgz", + "integrity": "sha512-ZPr4dgTcNkEfcGOMFQyDdJrTU9uQO1nb1cjf+nuzb6FxgMDgKddZOM29qEsB7jvsZSMruLRcL2KfM4ypKpa0LA==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0", + "upper-case-first": "^2.0.1" + } + }, + "serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "requires": { + "type-fest": "^0.13.1" + }, + "dependencies": { + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true + } + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -5083,9 +6959,9 @@ "dev": true }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -5106,9 +6982,9 @@ } }, "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", "dev": true }, "shebang-command": { @@ -5127,15 +7003,15 @@ "dev": true }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, "slice-ansi": { @@ -5157,6 +7033,16 @@ } } }, + "snake-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.3.tgz", + "integrity": "sha512-WM1sIXEO+rsAHBKjGf/6R1HBBcgbncKS08d2Aqec/mrDSpU80SiOU41hO7ny6DToHSyrlwTYzQBIK1FPSx4Y3Q==", + "dev": true, + "requires": { + "dot-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -5265,87 +7151,133 @@ } }, "socket.io": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", - "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", + "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", "dev": true, "requires": { - "debug": "~3.1.0", - "engine.io": "~3.2.0", + "debug": "~4.1.0", + "engine.io": "~3.4.0", "has-binary2": "~1.0.2", "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.1.1", - "socket.io-parser": "~3.2.0" + "socket.io-client": "2.3.0", + "socket.io-parser": "~3.4.0" }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==", "dev": true }, "socket.io-client": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", - "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", "dev": true, "requires": { "backo2": "1.0.2", "base64-arraybuffer": "0.1.5", "component-bind": "1.0.0", "component-emitter": "1.2.1", - "debug": "~3.1.0", - "engine.io-client": "~3.2.0", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", "has-binary2": "~1.0.2", "has-cors": "1.1.0", "indexof": "0.0.1", "object-component": "0.0.3", "parseqs": "0.0.5", "parseuri": "0.0.5", - "socket.io-parser": "~3.2.0", + "socket.io-parser": "~3.3.0", "to-array": "0.1.4" }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", "dev": true, "requires": { - "ms": "2.0.0" + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } }, "socket.io-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", - "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", + "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", "dev": true, "requires": { "component-emitter": "1.2.1", - "debug": "~3.1.0", + "debug": "~4.1.0", "isarray": "2.0.1" }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "isarray": { @@ -5353,9 +7285,33 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "dev": true, + "requires": { + "sort-keys": "^1.0.0" + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5376,9 +7332,9 @@ } }, "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -5504,35 +7460,45 @@ "dev": true }, "streamroller": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.4.tgz", - "integrity": "sha512-Wc2Gm5ygjSX8ZpW9J7Y9FwiSzTlKSvcl0FTTMd3rn7RoxDXpBW+xD9TY5sWL2n0UR61COB0LG1BQvN6nTUQbLQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz", + "integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==", "dev": true, "requires": { - "async": "^2.6.1", - "date-format": "^2.0.0", - "debug": "^3.1.0", - "fs-extra": "^7.0.0", - "lodash": "^4.17.10" + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" }, "dependencies": { + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "dev": true + }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -5571,6 +7537,15 @@ "is-utf8": "^0.2.0" } }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -5578,11 +7553,20 @@ "dev": true }, "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", "dev": true }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -5603,13 +7587,13 @@ } }, "table": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.1.tgz", - "integrity": "sha512-E6CK1/pZe2N75rGZQotFOdmzWQ1AILtgYbMAbAjvms0S1l5IDB47zG3nCnFGB/w+7nB3vKofbLXCH7HPBo864w==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", "dev": true, "requires": { - "ajv": "^6.9.1", - "lodash": "^4.17.11", + "ajv": "^6.10.2", + "lodash": "^4.17.14", "slice-ansi": "^2.1.0", "string-width": "^3.0.0" }, @@ -5620,6 +7604,12 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -5648,35 +7638,92 @@ } } }, - "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "tar-fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", + "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", "dev": true, - "optional": true, "requires": { "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" }, "dependencies": { - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", "dev": true, - "optional": true + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "tar-stream": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", + "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", + "dev": true, + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } } } }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, "terser": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.1.2.tgz", - "integrity": "sha512-jvNoEQSPXJdssFwqPSgWjsOrb+ELoE+ILpHPKXC83tIxOlh2U75F1KuB2luLD/3a6/7K3Vw5pDn+hvu0C4AzSw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz", + "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==", "dev": true, "requires": { "commander": "^2.20.0", @@ -5685,9 +7732,9 @@ }, "dependencies": { "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, "source-map": { @@ -5736,6 +7783,12 @@ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "dev": true }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -5761,6 +7814,12 @@ "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", "dev": true }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -5812,12 +7871,42 @@ "through2": "^2.0.3" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "dev": true + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "type": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", @@ -5833,14 +7922,20 @@ "prelude-ls": "~1.1.2" } }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "requires": { "media-typer": "0.3.0", - "mime-types": "~2.1.18" + "mime-types": "~2.1.24" } }, "typedarray": { @@ -5849,12 +7944,22 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "ua-parser-js": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", + "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==", "dev": true }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -5885,38 +7990,15 @@ "dev": true }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "unique-stream": { @@ -5987,6 +8069,24 @@ "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", "dev": true }, + "upper-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.1.tgz", + "integrity": "sha512-laAsbea9SY5osxrv7S99vH9xAaJKrw5Qpdh4ENRLcaxipjKsiaBwiAsxfa8X5mObKNTQPsupSq0J/VIxsSJe3A==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "upper-case-first": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.1.tgz", + "integrity": "sha512-105J8XqQ+9RxW3l9gHZtgve5oaiR9TIwvmZAMAIZWRHe00T21cdvewKORTlOJf/zXW6VukuTshM+HXZNWz7N5w==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -6002,22 +8102,27 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "dev": true + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, - "useragent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", - "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", - "dev": true, - "requires": { - "lru-cache": "4.1.x", - "tmp": "0.0.x" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6030,6 +8135,18 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", "dev": true }, + "uuid": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", + "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, "v8flags": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", @@ -6121,6 +8238,44 @@ "integrity": "sha512-TOMFWtQdxzjWp8qx4DAraTWTsdhxVSiWa6NkPFSaPtZ1diKUxTn4yTix73A1euG1WbSOMMPcY51cnjTIHrGtDA==", "dev": true }, + "webdriver": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-6.1.14.tgz", + "integrity": "sha512-6fXoGDnxWfJn9zqfYbc6+VEV5N1obd3K7iuRmJ7w/hH9d9XDxcrcx147T5egiy1qziko61hqYE/x9VtSQbjPcA==", + "dev": true, + "requires": { + "@wdio/config": "6.1.14", + "@wdio/logger": "6.0.16", + "@wdio/protocols": "6.1.14", + "@wdio/utils": "6.1.8", + "got": "^11.0.2", + "lodash.merge": "^4.6.1" + } + }, + "webdriverio": { + "version": "6.1.16", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-6.1.16.tgz", + "integrity": "sha512-vk6/XeErNnMooebCtxwIR//LqrastXO9gXL+eVUGNRAuG5omp8XCdT+MK2fmlw53xhcPULQ/y3h8ysYlPnPeyA==", + "dev": true, + "requires": { + "@wdio/config": "6.1.14", + "@wdio/logger": "6.0.16", + "@wdio/repl": "6.1.8", + "@wdio/utils": "6.1.8", + "archiver": "^4.0.1", + "css-value": "^0.0.1", + "devtools": "6.1.16", + "grapheme-splitter": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "lodash.isobject": "^3.0.2", + "lodash.isplainobject": "^4.0.6", + "lodash.zip": "^4.2.0", + "resq": "^1.6.0", + "rgb2hex": "^0.2.0", + "serialize-error": "^7.0.0", + "webdriver": "6.1.14" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -6136,10 +8291,16 @@ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, - "wordwrap": { + "which-pm-runs": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, "wrap-ansi": { @@ -6168,30 +8329,9 @@ } }, "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "dev": true, - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", + "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==", "dev": true }, "xmlhttprequest-ssl": { @@ -6218,31 +8358,50 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, + "yaml": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.8.3.tgz", + "integrity": "sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.7" + } + }, "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^3.0.0", + "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" + "yargs-parser": "^18.1.1" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -6250,23 +8409,39 @@ "dev": true }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, "get-caller-file": { @@ -6275,37 +8450,17 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true }, "require-main-filename": { "version": "2.0.0", @@ -6314,23 +8469,23 @@ "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } }, "which-module": { @@ -6340,14 +8495,14 @@ "dev": true }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "y18n": { @@ -6357,9 +8512,9 @@ "dev": true }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -6377,11 +8532,45 @@ "camelcase": "^3.0.0" } }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", "dev": true + }, + "zip-stream": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-3.0.1.tgz", + "integrity": "sha512-r+JdDipt93ttDjsOVPU5zaq5bAyY+3H19bDrThkvuVxC0xMQzU1PJcS6D+KrP3u96gH9XLomcHPb+2skoDjulQ==", + "dev": true, + "requires": { + "archiver-utils": "^2.1.0", + "compress-commons": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } } } } diff --git a/package.json b/package.json index f1218606..6eb06b3b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "muuri", - "version": "0.8.0", - "description": "Responsive, sortable, filterable and draggable grid layouts.", + "version": "0.9.0", + "description": "Responsive, sortable, filterable and draggable layouts", "keywords": [ "grid", "layout", @@ -10,7 +10,7 @@ "sort", "drag" ], - "homepage": "https://github.com/haltu/muuri", + "homepage": "https://muuri.dev/", "license": "MIT", "author": { "name": "Niklas Rämö", @@ -22,6 +22,17 @@ "url": "git@github.com:haltu/muuri.git" }, "main": "dist/muuri.js", + "module": "dist/muuri.module.js", + "types": "src/index.d.ts", + "files": [ + "package.json", + "src", + "dist", + "README.md", + "LICENSE.md", + "AUTHORS.txt", + "CONTRIBUTING.md" + ], "scripts": { "test": "./node_modules/.bin/gulp test", "test-sauce": "./node_modules/.bin/gulp test-sauce", @@ -33,7 +44,8 @@ "build": "./node_modules/.bin/gulp build", "lint": "./node_modules/.bin/gulp lint", "format": "./node_modules/.bin/prettier --write ./src/**/*.js", - "format-test": "./node_modules/.bin/prettier --list-different ./src/**/*.js", + "format-tests": "./node_modules/.bin/prettier --write ./tests/**/*.js", + "validate-formatting": "./node_modules/.bin/prettier --list-different ./src/**/*.js", "size": "./node_modules/.bin/gulp size", "pre-commit-hook": "./node_modules/.bin/gulp pre-commit" }, @@ -43,27 +55,26 @@ } }, "devDependencies": { - "dotenv": "^8.0.0", + "dotenv": "^8.2.0", "gulp": "^4.0.2", "gulp-eslint": "^6.0.0", "gulp-size": "^3.0.0", - "husky": "^1.3.1", - "karma": "^4.1.0", - "karma-chrome-launcher": "^2.2.0", + "husky": "^4.2.5", + "karma": "^5.0.9", + "karma-chrome-launcher": "^3.1.0", "karma-edge-launcher": "^0.4.2", - "karma-firefox-launcher": "^1.1.0", - "karma-qunit": "^3.1.2", + "karma-firefox-launcher": "^1.3.0", + "karma-qunit": "^4.1.1", "karma-safari-launcher": "^1.0.0", - "karma-sauce-launcher": "^2.0.2", + "karma-sauce-launcher": "^4.1.5", "karma-story-reporter": "^0.3.1", "mezr": "^0.6.2", - "prettier": "^1.18.2", + "prettier": "^2.0.5", "prosthetic-hand": "^1.3.1", - "qunit": "^2.9.2", - "rimraf": "^2.6.3", - "rollup": "^1.16.7", - "terser": "^4.1.2", - "web-animations-js": "^2.3.2", - "yargs": "^13.2.4" + "qunit": "^2.10.0", + "rimraf": "^3.0.2", + "rollup": "^2.15.0", + "terser": "^4.7.0", + "web-animations-js": "^2.3.2" } } diff --git a/rollup.banner.js b/rollup.banner.js index 8d38f8fd..882c8413 100644 --- a/rollup.banner.js +++ b/rollup.banner.js @@ -12,8 +12,12 @@ module.exports = `/** * Copyright (c) 2016-present, Niklas Rämö * @license MIT * -* Muuri Ticker / Muuri Emitter / Muuri Queue +* Muuri Ticker / Muuri Emitter / Muuri Dragger * Copyright (c) 2018-present, Niklas Rämö * @license MIT +* +* Muuri AutoScroller +* Copyright (c) 2019-present, Niklas Rämö +* @license MIT */ `; diff --git a/rollup.config.js b/rollup.config.js index 02e3a6f7..ac148429 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -12,11 +12,19 @@ const stripBanner = { module.exports = { input: 'src/index.js', - output: { - name: 'Muuri', - file: pkg.main, - format: 'umd', - banner: banner - }, + output: [ + { + name: 'Muuri', + file: pkg.main, + format: 'umd', + banner: banner + }, + { + name: 'Muuri', + file: pkg.module, + format: 'es', + banner: banner + } + ], plugins: [stripBanner] }; diff --git a/src/Item/ItemAnimate.js b/src/Animator/Animator.js similarity index 51% rename from src/Item/ItemAnimate.js rename to src/Animator/Animator.js index 2e757a11..414be55c 100644 --- a/src/Item/ItemAnimate.js +++ b/src/Animator/Animator.js @@ -4,11 +4,14 @@ * https://github.com/haltu/muuri/blob/master/LICENSE.md */ -import getStyle from '../utils/getStyle'; -import getStyleName from '../utils/getStyleName'; +import getCurrentStyles from '../utils/getCurrentStyles'; +import getUnprefixedPropName from '../utils/getUnprefixedPropName'; import isFunction from '../utils/isFunction'; +import isNative from '../utils/isNative'; import setStyles from '../utils/setStyles'; -import { transformProp } from '../utils/supportedTransform'; + +var HAS_WEB_ANIMATIONS = !!(Element && isFunction(Element.prototype.animate)); +var HAS_NATIVE_WEB_ANIMATIONS = !!(Element && isNative(Element.prototype.animate)); /** * Item animation handler powered by Web Animations API. @@ -16,14 +19,14 @@ import { transformProp } from '../utils/supportedTransform'; * @class * @param {HTMLElement} element */ -function ItemAnimate(element) { +function Animator(element) { this._element = element; this._animation = null; + this._duration = 0; + this._easing = ''; this._callback = null; this._props = []; this._values = []; - this._keyframes = []; - this._options = {}; this._isDestroyed = false; this._onFinish = this._onFinish.bind(this); } @@ -38,7 +41,6 @@ function ItemAnimate(element) { * running. * * @public - * @memberof ItemAnimate.prototype * @param {Object} propsFrom * @param {Object} propsTo * @param {Object} [options] @@ -46,38 +48,57 @@ function ItemAnimate(element) { * @param {String} [options.easing='ease'] * @param {Function} [options.onFinish] */ -ItemAnimate.prototype.start = function(propsFrom, propsTo, options) { +Animator.prototype.start = function (propsFrom, propsTo, options) { if (this._isDestroyed) return; + var element = this._element; + var opts = options || {}; + + // If we don't have web animations available let's not animate. + if (!HAS_WEB_ANIMATIONS) { + setStyles(element, propsTo); + this._callback = isFunction(opts.onFinish) ? opts.onFinish : null; + this._onFinish(); + return; + } + var animation = this._animation; var currentProps = this._props; var currentValues = this._values; - var opts = options || 0; + var duration = opts.duration || 300; + var easing = opts.easing || 'ease'; var cancelAnimation = false; + var propName, propCount, propIndex; // If we have an existing animation running, let's check if it needs to be // cancelled or if it can continue running. if (animation) { - var propCount = 0; - var propIndex; + propCount = 0; + + // Cancel animation if duration or easing has changed. + if (duration !== this._duration || easing !== this._easing) { + cancelAnimation = true; + } // Check if the requested animation target props and values match with the // current props and values. - for (var propName in propsTo) { - ++propCount; - propIndex = currentProps.indexOf(propName); - if (propIndex === -1 || propsTo[propName] !== currentValues[propIndex]) { - cancelAnimation = true; - break; + if (!cancelAnimation) { + for (propName in propsTo) { + ++propCount; + propIndex = currentProps.indexOf(propName); + if (propIndex === -1 || propsTo[propName] !== currentValues[propIndex]) { + cancelAnimation = true; + break; + } } - } - // Check if the target props count matches current props count. This is - // needed for the edge case scenario where target props contain the same - // styles as current props, but the current props have some additional - // props. - if (!cancelAnimation && propCount !== currentProps.length) { - cancelAnimation = true; + // Check if the target props count matches current props count. This is + // needed for the edge case scenario where target props contain the same + // styles as current props, but the current props have some additional + // props. + if (propCount !== currentProps.length) { + cancelAnimation = true; + } } } @@ -98,21 +119,22 @@ ItemAnimate.prototype.start = function(propsFrom, propsTo, options) { currentValues.push(propsTo[propName]); } - // Set up keyframes. - var animKeyframes = this._keyframes; - animKeyframes[0] = propsFrom; - animKeyframes[1] = propsTo; - - // Set up options. - var animOptions = this._options; - animOptions.duration = opts.duration || 300; - animOptions.easing = opts.easing || 'ease'; - - // Start the animation - var element = this._element; - animation = element.animate(animKeyframes, animOptions); - animation.onfinish = this._onFinish; - this._animation = animation; + // Start the animation. We need to provide unprefixed property names to the + // Web Animations polyfill if it is being used. If we have native Web + // Animations available we need to provide prefixed properties instead. + this._duration = duration; + this._easing = easing; + this._animation = element.animate( + [ + createFrame(propsFrom, HAS_NATIVE_WEB_ANIMATIONS), + createFrame(propsTo, HAS_NATIVE_WEB_ANIMATIONS), + ], + { + duration: duration, + easing: easing, + } + ); + this._animation.onfinish = this._onFinish; // Set the end styles. This makes sure that the element stays at the end // values after animation is finished. @@ -123,46 +145,31 @@ ItemAnimate.prototype.start = function(propsFrom, propsTo, options) { * Stop instance's current animation if running. * * @public - * @memberof ItemAnimate.prototype - * @param {Object} [styles] */ -ItemAnimate.prototype.stop = function(styles) { +Animator.prototype.stop = function () { if (this._isDestroyed || !this._animation) return; - - var element = this._element; - var currentProps = this._props; - var currentValues = this._values; - var propName; - var propValue; - var i; - - // Calculate (if not provided) and set styles. - if (!styles) { - for (i = 0; i < currentProps.length; i++) { - propName = currentProps[i]; - propValue = getStyle(element, getStyleName(propName)); - element.style[propName === 'transform' ? transformProp : propName] = propValue; - } - } else { - setStyles(element, styles); - } - - // Cancel animation. this._animation.cancel(); this._animation = this._callback = null; + this._props.length = this._values.length = 0; +}; - // Reset current props and values. - currentProps.length = currentValues.length = 0; +/** + * Read the current values of the element's animated styles from the DOM. + * + * @public + * @return {Object} + */ +Animator.prototype.getCurrentStyles = function () { + return getCurrentStyles(element, currentProps); }; /** * Check if the item is being animated currently. * * @public - * @memberof ItemAnimate.prototype * @return {Boolean} */ -ItemAnimate.prototype.isAnimating = function() { +Animator.prototype.isAnimating = function () { return !!this._animation; }; @@ -170,12 +177,11 @@ ItemAnimate.prototype.isAnimating = function() { * Destroy the instance and stop current animation if it is running. * * @public - * @memberof ItemAnimate.prototype */ -ItemAnimate.prototype.destroy = function() { +Animator.prototype.destroy = function () { if (this._isDestroyed) return; this.stop(); - this._element = this._options = this._keyframes = null; + this._element = null; this._isDestroyed = true; }; @@ -188,13 +194,25 @@ ItemAnimate.prototype.destroy = function() { * Animation end handler. * * @private - * @memberof ItemAnimate.prototype */ -ItemAnimate.prototype._onFinish = function() { +Animator.prototype._onFinish = function () { var callback = this._callback; this._animation = this._callback = null; this._props.length = this._values.length = 0; callback && callback(); }; -export default ItemAnimate; +/** + * Private helpers + * *************** + */ + +function createFrame(props, prefix) { + var frame = {}; + for (var prop in props) { + frame[prefix ? prop : getUnprefixedPropName(prop)] = props[prop]; + } + return frame; +} + +export default Animator; diff --git a/src/AutoScroller/AutoScroller.js b/src/AutoScroller/AutoScroller.js new file mode 100644 index 00000000..73e5070e --- /dev/null +++ b/src/AutoScroller/AutoScroller.js @@ -0,0 +1,736 @@ +/** + * Muuri AutoScroller + * Copyright (c) 2019-present, Niklas Rämö + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md + */ + +import { addAutoScrollTick, cancelAutoScrollTick } from '../ticker'; +import { LEFT, RIGHT, UP, DOWN, AXIS_X, AXIS_Y, FORWARD, BACKWARD } from './constants'; +import ScrollRequest from './ScrollRequest'; +import ScrollAction from './ScrollAction'; +import Pool from './Pool'; +import getIntersectionScore from '../utils/getIntersectionScore'; +import isFunction from '../utils/isFunction'; +import { + getScrollElement, + getScrollLeft, + getScrollTop, + getScrollLeftMax, + getScrollTopMax, + getContentRect, + getItemAutoScrollSettings, + prepareItemScrollSync, + applyItemScrollSync, + computeThreshold, +} from './utils'; + +var RECT_1 = { + width: 0, + height: 0, + left: 0, + right: 0, + top: 0, + bottom: 0, +}; + +var RECT_2 = { + width: 0, + height: 0, + left: 0, + right: 0, + top: 0, + bottom: 0, +}; + +export default function AutoScroller() { + this._isDestroyed = false; + this._isTicking = false; + this._tickTime = 0; + this._tickDeltaTime = 0; + this._items = []; + this._actions = []; + this._requests = {}; + this._requests[AXIS_X] = {}; + this._requests[AXIS_Y] = {}; + this._requestOverlapCheck = {}; + this._dragPositions = {}; + this._dragDirections = {}; + this._overlapCheckInterval = 150; + + this._requestPool = new Pool( + function () { + return new ScrollRequest(); + }, + function (request) { + request.reset(); + } + ); + + this._actionPool = new Pool( + function () { + return new ScrollAction(); + }, + function (action) { + action.reset(); + } + ); + + this._readTick = this._readTick.bind(this); + this._writeTick = this._writeTick.bind(this); +} + +AutoScroller.AXIS_X = AXIS_X; +AutoScroller.AXIS_Y = AXIS_Y; +AutoScroller.FORWARD = FORWARD; +AutoScroller.BACKWARD = BACKWARD; +AutoScroller.LEFT = LEFT; +AutoScroller.RIGHT = RIGHT; +AutoScroller.UP = UP; +AutoScroller.DOWN = DOWN; + +AutoScroller.smoothSpeed = function (maxSpeed, acceleration, deceleration) { + return function (item, element, data) { + var targetSpeed = 0; + if (!data.isEnding) { + if (data.threshold > 0) { + var factor = data.threshold - Math.max(0, data.distance); + targetSpeed = (maxSpeed / data.threshold) * factor; + } else { + targetSpeed = maxSpeed; + } + } + + var currentSpeed = data.speed; + var nextSpeed = targetSpeed; + + if (currentSpeed === targetSpeed) { + return nextSpeed; + } + + if (currentSpeed < targetSpeed) { + nextSpeed = currentSpeed + acceleration * (data.deltaTime / 1000); + return Math.min(targetSpeed, nextSpeed); + } else { + nextSpeed = currentSpeed - deceleration * (data.deltaTime / 1000); + return Math.max(targetSpeed, nextSpeed); + } + }; +}; + +AutoScroller.pointerHandle = function (pointerSize) { + var rect = { left: 0, top: 0, width: 0, height: 0 }; + var size = pointerSize || 1; + return function (item, x, y, w, h, pX, pY) { + rect.left = pX - size * 0.5; + rect.top = pY - size * 0.5; + rect.width = size; + rect.height = size; + return rect; + }; +}; + +AutoScroller.prototype._readTick = function (time) { + if (this._isDestroyed) return; + if (time && this._tickTime) { + this._tickDeltaTime = time - this._tickTime; + this._tickTime = time; + this._updateRequests(); + this._updateActions(); + } else { + this._tickTime = time; + this._tickDeltaTime = 0; + } +}; + +AutoScroller.prototype._writeTick = function () { + if (this._isDestroyed) return; + this._applyActions(); + addAutoScrollTick(this._readTick, this._writeTick); +}; + +AutoScroller.prototype._startTicking = function () { + this._isTicking = true; + addAutoScrollTick(this._readTick, this._writeTick); +}; + +AutoScroller.prototype._stopTicking = function () { + this._isTicking = false; + this._tickTime = 0; + this._tickDeltaTime = 0; + cancelAutoScrollTick(); +}; + +AutoScroller.prototype._getItemHandleRect = function (item, handle, rect) { + var itemDrag = item._drag; + + if (handle) { + var ev = itemDrag._dragMoveEvent || itemDrag._dragStartEvent; + var data = handle( + item, + itemDrag._clientX, + itemDrag._clientY, + item._width, + item._height, + ev.clientX, + ev.clientY + ); + rect.left = data.left; + rect.top = data.top; + rect.width = data.width; + rect.height = data.height; + } else { + rect.left = itemDrag._clientX; + rect.top = itemDrag._clientY; + rect.width = item._width; + rect.height = item._height; + } + + rect.right = rect.left + rect.width; + rect.bottom = rect.top + rect.height; + + return rect; +}; + +AutoScroller.prototype._requestItemScroll = function ( + item, + axis, + element, + direction, + threshold, + distance, + maxValue +) { + var reqMap = this._requests[axis]; + var request = reqMap[item._id]; + + if (request) { + if (request.element !== element || request.direction !== direction) { + request.reset(); + } + } else { + request = this._requestPool.pick(); + } + + request.item = item; + request.element = element; + request.direction = direction; + request.threshold = threshold; + request.distance = distance; + request.maxValue = maxValue; + reqMap[item._id] = request; +}; + +AutoScroller.prototype._cancelItemScroll = function (item, axis) { + var reqMap = this._requests[axis]; + var request = reqMap[item._id]; + if (!request) return; + if (request.action) request.action.removeRequest(request); + this._requestPool.release(request); + delete reqMap[item._id]; +}; + +AutoScroller.prototype._checkItemOverlap = function (item, checkX, checkY) { + var settings = getItemAutoScrollSettings(item); + var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets; + var threshold = settings.threshold; + var safeZone = settings.safeZone; + + if (!targets || !targets.length) { + checkX && this._cancelItemScroll(item, AXIS_X); + checkY && this._cancelItemScroll(item, AXIS_Y); + return; + } + + var dragDirections = this._dragDirections[item._id]; + var dragDirectionX = dragDirections[0]; + var dragDirectionY = dragDirections[1]; + + if (!dragDirectionX && !dragDirectionY) { + checkX && this._cancelItemScroll(item, AXIS_X); + checkY && this._cancelItemScroll(item, AXIS_Y); + return; + } + + var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1); + var testRect = RECT_2; + + var target = null; + var testElement = null; + var testAxisX = true; + var testAxisY = true; + var testScore = 0; + var testPriority = 0; + var testThreshold = null; + var testDirection = null; + var testDistance = 0; + var testMaxScrollX = 0; + var testMaxScrollY = 0; + + var xElement = null; + var xPriority = -Infinity; + var xThreshold = 0; + var xScore = 0; + var xDirection = null; + var xDistance = 0; + var xMaxScroll = 0; + + var yElement = null; + var yPriority = -Infinity; + var yThreshold = 0; + var yScore = 0; + var yDirection = null; + var yDistance = 0; + var yMaxScroll = 0; + + for (var i = 0; i < targets.length; i++) { + target = targets[i]; + testAxisX = checkX && dragDirectionX && target.axis !== AXIS_Y; + testAxisY = checkY && dragDirectionY && target.axis !== AXIS_X; + testPriority = target.priority || 0; + + // Ignore this item if it's x-axis and y-axis priority is lower than + // the currently matching item's. + if ((!testAxisX || testPriority < xPriority) && (!testAxisY || testPriority < yPriority)) { + continue; + } + + testElement = getScrollElement(target.element || target); + testMaxScrollX = testAxisX ? getScrollLeftMax(testElement) : -1; + testMaxScrollY = testAxisY ? getScrollTopMax(testElement) : -1; + + // Ignore this item if there is no possibility to scroll. + if (!testMaxScrollX && !testMaxScrollY) continue; + + testRect = getContentRect(testElement, testRect); + testScore = getIntersectionScore(itemRect, testRect); + + // Ignore this item if it's not overlapping at all with the dragged item. + if (testScore <= 0) continue; + + // Test x-axis. + if ( + testAxisX && + testPriority >= xPriority && + testMaxScrollX > 0 && + (testPriority > xPriority || testScore > xScore) + ) { + testDirection = null; + testThreshold = computeThreshold( + typeof target.threshold === 'number' ? target.threshold : threshold, + safeZone, + itemRect.width, + testRect.width + ); + if (dragDirectionX === RIGHT) { + testDistance = testRect.right + testThreshold.offset - itemRect.right; + if (testDistance <= testThreshold.value && getScrollLeft(testElement) < testMaxScrollX) { + testDirection = RIGHT; + } + } else if (dragDirectionX === LEFT) { + testDistance = itemRect.left - (testRect.left - testThreshold.offset); + if (testDistance <= testThreshold.value && getScrollLeft(testElement) > 0) { + testDirection = LEFT; + } + } + + if (testDirection !== null) { + xElement = testElement; + xPriority = testPriority; + xThreshold = testThreshold.value; + xScore = testScore; + xDirection = testDirection; + xDistance = testDistance; + xMaxScroll = testMaxScrollX; + } + } + + // Test y-axis. + if ( + testAxisY && + testPriority >= yPriority && + testMaxScrollY > 0 && + (testPriority > yPriority || testScore > yScore) + ) { + testDirection = null; + testThreshold = computeThreshold( + typeof target.threshold === 'number' ? target.threshold : threshold, + safeZone, + itemRect.height, + testRect.height + ); + if (dragDirectionY === DOWN) { + testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom; + if (testDistance <= testThreshold.value && getScrollTop(testElement) < testMaxScrollY) { + testDirection = DOWN; + } + } else if (dragDirectionY === UP) { + testDistance = itemRect.top - (testRect.top - testThreshold.offset); + if (testDistance <= testThreshold.value && getScrollTop(testElement) > 0) { + testDirection = UP; + } + } + + if (testDirection !== null) { + yElement = testElement; + yPriority = testPriority; + yThreshold = testThreshold.value; + yScore = testScore; + yDirection = testDirection; + yDistance = testDistance; + yMaxScroll = testMaxScrollY; + } + } + } + + // Request or cancel x-axis scroll. + if (checkX) { + if (xElement) { + this._requestItemScroll( + item, + AXIS_X, + xElement, + xDirection, + xThreshold, + xDistance, + xMaxScroll + ); + } else { + this._cancelItemScroll(item, AXIS_X); + } + } + + // Request or cancel y-axis scroll. + if (checkY) { + if (yElement) { + this._requestItemScroll( + item, + AXIS_Y, + yElement, + yDirection, + yThreshold, + yDistance, + yMaxScroll + ); + } else { + this._cancelItemScroll(item, AXIS_Y); + } + } +}; + +AutoScroller.prototype._updateScrollRequest = function (scrollRequest) { + var item = scrollRequest.item; + var settings = getItemAutoScrollSettings(item); + var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets; + var targetCount = (targets && targets.length) || 0; + var threshold = settings.threshold; + var safeZone = settings.safeZone; + var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1); + var testRect = RECT_2; + var target = null; + var testElement = null; + var testIsAxisX = false; + var testScore = null; + var testThreshold = null; + var testDistance = null; + var testScroll = null; + var testMaxScroll = null; + var hasReachedEnd = null; + + for (var i = 0; i < targetCount; i++) { + target = targets[i]; + + // Make sure we have a matching element. + testElement = getScrollElement(target.element || target); + if (testElement !== scrollRequest.element) continue; + + // Make sure we have a matching axis. + testIsAxisX = !!(AXIS_X & scrollRequest.direction); + if (testIsAxisX) { + if (target.axis === AXIS_Y) continue; + } else { + if (target.axis === AXIS_X) continue; + } + + // Stop scrolling if there is no room to scroll anymore. + testMaxScroll = testIsAxisX ? getScrollLeftMax(testElement) : getScrollTopMax(testElement); + if (testMaxScroll <= 0) { + break; + } + + testRect = getContentRect(testElement, testRect); + testScore = getIntersectionScore(itemRect, testRect); + + // Stop scrolling if dragged item is not overlapping with the scroll + // element anymore. + if (testScore <= 0) { + break; + } + + // Compute threshold and edge offset. + testThreshold = computeThreshold( + typeof target.threshold === 'number' ? target.threshold : threshold, + safeZone, + testIsAxisX ? itemRect.width : itemRect.height, + testIsAxisX ? testRect.width : testRect.height + ); + + // Compute distance (based on current direction). + if (scrollRequest.direction === LEFT) { + testDistance = itemRect.left - (testRect.left - testThreshold.offset); + } else if (scrollRequest.direction === RIGHT) { + testDistance = testRect.right + testThreshold.offset - itemRect.right; + } else if (scrollRequest.direction === UP) { + testDistance = itemRect.top - (testRect.top - testThreshold.offset); + } else { + testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom; + } + + // Stop scrolling if threshold is not exceeded. + if (testDistance > testThreshold.value) { + break; + } + + // Stop scrolling if we have reached the end of the scroll value. + testScroll = testIsAxisX ? getScrollLeft(testElement) : getScrollTop(testElement); + hasReachedEnd = + FORWARD & scrollRequest.direction ? testScroll >= testMaxScroll : testScroll <= 0; + if (hasReachedEnd) { + break; + } + + // Scrolling can continue, let's update the values. + scrollRequest.maxValue = testMaxScroll; + scrollRequest.threshold = testThreshold.value; + scrollRequest.distance = testDistance; + scrollRequest.isEnding = false; + return true; + } + + // Before we end the request, let's see if we need to stop the scrolling + // smoothly or immediately. + if (settings.smoothStop === true && scrollRequest.speed > 0) { + if (hasReachedEnd === null) hasReachedEnd = scrollRequest.hasReachedEnd(); + scrollRequest.isEnding = hasReachedEnd ? false : true; + } else { + scrollRequest.isEnding = false; + } + + return scrollRequest.isEnding; +}; + +AutoScroller.prototype._updateRequests = function () { + var items = this._items; + var requestsX = this._requests[AXIS_X]; + var requestsY = this._requests[AXIS_Y]; + var item, reqX, reqY, checkTime, needsCheck, checkX, checkY; + + for (var i = 0; i < items.length; i++) { + item = items[i]; + checkTime = this._requestOverlapCheck[item._id]; + needsCheck = checkTime > 0 && this._tickTime - checkTime > this._overlapCheckInterval; + + checkX = true; + reqX = requestsX[item._id]; + if (reqX && reqX.isActive) { + checkX = !this._updateScrollRequest(reqX); + if (checkX) { + needsCheck = true; + this._cancelItemScroll(item, AXIS_X); + } + } + + checkY = true; + reqY = requestsY[item._id]; + if (reqY && reqY.isActive) { + checkY = !this._updateScrollRequest(reqY); + if (checkY) { + needsCheck = true; + this._cancelItemScroll(item, AXIS_Y); + } + } + + if (needsCheck) { + this._requestOverlapCheck[item._id] = 0; + this._checkItemOverlap(item, checkX, checkY); + } + } +}; + +AutoScroller.prototype._requestAction = function (request, axis) { + var actions = this._actions; + var isAxisX = axis === AXIS_X; + var action = null; + + for (var i = 0; i < actions.length; i++) { + action = actions[i]; + + // If the action's request does not match the request's -> skip. + if (request.element !== action.element) { + action = null; + continue; + } + + // If the request and action share the same element, but the request slot + // for the requested axis is already reserved let's ignore and cancel this + // request. + if (isAxisX ? action.requestX : action.requestY) { + this._cancelItemScroll(request.item, axis); + return; + } + + // Seems like we have found our action, let's break the loop. + break; + } + + if (!action) action = this._actionPool.pick(); + action.element = request.element; + action.addRequest(request); + + request.tick(this._tickDeltaTime); + actions.push(action); +}; + +AutoScroller.prototype._updateActions = function () { + var items = this._items; + var requests = this._requests; + var actions = this._actions; + var itemId; + var reqX; + var reqY; + var i; + + // Generate actions. + for (i = 0; i < items.length; i++) { + itemId = items[i]._id; + reqX = requests[AXIS_X][itemId]; + reqY = requests[AXIS_Y][itemId]; + if (reqX) this._requestAction(reqX, AXIS_X); + if (reqY) this._requestAction(reqY, AXIS_Y); + } + + // Compute actions' scroll values. + for (i = 0; i < actions.length; i++) { + actions[i].computeScrollValues(); + } +}; + +AutoScroller.prototype._applyActions = function () { + var actions = this._actions; + var items = this._items; + var i; + + // No actions -> no scrolling. + if (!actions.length) return; + + // Scroll all the required elements. + for (i = 0; i < actions.length; i++) { + actions[i].scroll(); + this._actionPool.release(actions[i]); + } + + // Reset actions. + actions.length = 0; + + // Sync the item position immediately after all the auto-scrolling business is + // finished. Without this procedure the items will jitter during auto-scroll + // (in some cases at least) since the drag scroll handler is async (bound to + // raf tick). Note that this procedure should not emit any dragScroll events, + // because otherwise they would be emitted twice for the same event. + for (i = 0; i < items.length; i++) prepareItemScrollSync(items[i]); + for (i = 0; i < items.length; i++) applyItemScrollSync(items[i]); +}; + +AutoScroller.prototype._updateDragDirection = function (item) { + var dragPositions = this._dragPositions[item._id]; + var dragDirections = this._dragDirections[item._id]; + var x1 = item._drag._left; + var y1 = item._drag._top; + if (dragPositions.length) { + var x2 = dragPositions[0]; + var y2 = dragPositions[1]; + dragDirections[0] = x1 > x2 ? RIGHT : x1 < x2 ? LEFT : dragDirections[0] || 0; + dragDirections[1] = y1 > y2 ? DOWN : y1 < y2 ? UP : dragDirections[1] || 0; + } + dragPositions[0] = x1; + dragPositions[1] = y1; +}; + +AutoScroller.prototype.addItem = function (item) { + if (this._isDestroyed) return; + var index = this._items.indexOf(item); + if (index === -1) { + this._items.push(item); + this._requestOverlapCheck[item._id] = this._tickTime; + this._dragDirections[item._id] = [0, 0]; + this._dragPositions[item._id] = []; + if (!this._isTicking) this._startTicking(); + } +}; + +AutoScroller.prototype.updateItem = function (item) { + if (this._isDestroyed) return; + this._updateDragDirection(item); + if (!this._requestOverlapCheck[item._id]) { + this._requestOverlapCheck[item._id] = this._tickTime; + } +}; + +AutoScroller.prototype.removeItem = function (item) { + if (this._isDestroyed) return; + + var index = this._items.indexOf(item); + if (index === -1) return; + + var itemId = item._id; + + var reqX = this._requests[AXIS_X][itemId]; + if (reqX) { + this._cancelItemScroll(item, AXIS_X); + delete this._requests[AXIS_X][itemId]; + } + + var reqY = this._requests[AXIS_Y][itemId]; + if (reqY) { + this._cancelItemScroll(item, AXIS_Y); + delete this._requests[AXIS_Y][itemId]; + } + + delete this._requestOverlapCheck[itemId]; + delete this._dragPositions[itemId]; + delete this._dragDirections[itemId]; + this._items.splice(index, 1); + + if (this._isTicking && !this._items.length) { + this._stopTicking(); + } +}; + +AutoScroller.prototype.isItemScrollingX = function (item) { + var reqX = this._requests[AXIS_X][item._id]; + return !!(reqX && reqX.isActive); +}; + +AutoScroller.prototype.isItemScrollingY = function (item) { + var reqY = this._requests[AXIS_Y][item._id]; + return !!(reqY && reqY.isActive); +}; + +AutoScroller.prototype.isItemScrolling = function (item) { + return this.isItemScrollingX(item) || this.isItemScrollingY(item); +}; + +AutoScroller.prototype.destroy = function () { + if (this._isDestroyed) return; + + var items = this._items.slice(0); + for (var i = 0; i < items.length; i++) { + this.removeItem(items[i]); + } + + this._actions.length = 0; + this._requestPool.reset(); + this._actionPool.reset(); + + this._isDestroyed = true; +}; diff --git a/src/Queue/LICENSE.md b/src/AutoScroller/LICENSE.md similarity index 96% rename from src/Queue/LICENSE.md rename to src/AutoScroller/LICENSE.md index a8c4f6b1..0eea03e3 100644 --- a/src/Queue/LICENSE.md +++ b/src/AutoScroller/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2018, Niklas Rämö +Copyright (c) 2019, Niklas Rämö Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/src/AutoScroller/Pool.js b/src/AutoScroller/Pool.js new file mode 100644 index 00000000..d0a91182 --- /dev/null +++ b/src/AutoScroller/Pool.js @@ -0,0 +1,26 @@ +/** + * Muuri AutoScroller + * Copyright (c) 2019-present, Niklas Rämö + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md + */ + +export default function Pool(createItem, releaseItem) { + this.pool = []; + this.createItem = createItem; + this.releaseItem = releaseItem; +} + +Pool.prototype.pick = function () { + return this.pool.pop() || this.createItem(); +}; + +Pool.prototype.release = function (item) { + this.releaseItem(item); + if (this.pool.indexOf(item) !== -1) return; + this.pool.push(item); +}; + +Pool.prototype.reset = function () { + this.pool.length = 0; +}; diff --git a/src/AutoScroller/ScrollAction.js b/src/AutoScroller/ScrollAction.js new file mode 100644 index 00000000..6f4bd0cd --- /dev/null +++ b/src/AutoScroller/ScrollAction.js @@ -0,0 +1,66 @@ +/** + * Muuri AutoScroller + * Copyright (c) 2019-present, Niklas Rämö + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md + */ + +import { getScrollLeft, getScrollTop } from './utils'; +import { AXIS_X } from './constants'; + +export default function ScrollAction() { + this.element = null; + this.requestX = null; + this.requestY = null; + this.scrollLeft = 0; + this.scrollTop = 0; +} + +ScrollAction.prototype.reset = function () { + if (this.requestX) this.requestX.action = null; + if (this.requestY) this.requestY.action = null; + this.element = null; + this.requestX = null; + this.requestY = null; + this.scrollLeft = 0; + this.scrollTop = 0; +}; + +ScrollAction.prototype.addRequest = function (request) { + if (AXIS_X & request.direction) { + this.removeRequest(this.requestX); + this.requestX = request; + } else { + this.removeRequest(this.requestY); + this.requestY = request; + } + request.action = this; +}; + +ScrollAction.prototype.removeRequest = function (request) { + if (!request) return; + if (this.requestX === request) { + this.requestX = null; + request.action = null; + } else if (this.requestY === request) { + this.requestY = null; + request.action = null; + } +}; + +ScrollAction.prototype.computeScrollValues = function () { + this.scrollLeft = this.requestX ? this.requestX.value : getScrollLeft(this.element); + this.scrollTop = this.requestY ? this.requestY.value : getScrollTop(this.element); +}; + +ScrollAction.prototype.scroll = function () { + var element = this.element; + if (!element) return; + + if (element.scrollTo) { + element.scrollTo(this.scrollLeft, this.scrollTop); + } else { + element.scrollLeft = this.scrollLeft; + element.scrollTop = this.scrollTop; + } +}; diff --git a/src/AutoScroller/ScrollRequest.js b/src/AutoScroller/ScrollRequest.js new file mode 100644 index 00000000..b0177c09 --- /dev/null +++ b/src/AutoScroller/ScrollRequest.js @@ -0,0 +1,108 @@ +/** + * Muuri AutoScroller + * Copyright (c) 2019-present, Niklas Rämö + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md + */ + +import isFunction from '../utils/isFunction'; +import { AXIS_X, FORWARD } from './constants'; +import { getScrollLeft, getScrollTop, getItemAutoScrollSettings } from './utils'; + +export default function ScrollRequest() { + this.reset(); +} + +ScrollRequest.prototype.reset = function () { + if (this.isActive) this.onStop(); + this.item = null; + this.element = null; + this.isActive = false; + this.isEnding = false; + this.direction = null; + this.value = null; + this.maxValue = 0; + this.threshold = 0; + this.distance = 0; + this.speed = 0; + this.duration = 0; + this.action = null; +}; + +ScrollRequest.prototype.hasReachedEnd = function () { + return FORWARD & this.direction ? this.value >= this.maxValue : this.value <= 0; +}; + +ScrollRequest.prototype.computeCurrentScrollValue = function () { + if (this.value === null) { + return AXIS_X & this.direction ? getScrollLeft(this.element) : getScrollTop(this.element); + } + return Math.max(0, Math.min(this.value, this.maxValue)); +}; + +ScrollRequest.prototype.computeNextScrollValue = function (deltaTime) { + var delta = this.speed * (deltaTime / 1000); + var nextValue = FORWARD & this.direction ? this.value + delta : this.value - delta; + return Math.max(0, Math.min(nextValue, this.maxValue)); +}; + +ScrollRequest.prototype.computeSpeed = (function () { + var data = { + direction: null, + threshold: 0, + distance: 0, + value: 0, + maxValue: 0, + deltaTime: 0, + duration: 0, + isEnding: false, + }; + + return function (deltaTime) { + var item = this.item; + var speed = getItemAutoScrollSettings(item).speed; + + if (isFunction(speed)) { + data.direction = this.direction; + data.threshold = this.threshold; + data.distance = this.distance; + data.value = this.value; + data.maxValue = this.maxValue; + data.duration = this.duration; + data.speed = this.speed; + data.deltaTime = deltaTime; + data.isEnding = this.isEnding; + return speed(item, this.element, data); + } else { + return speed; + } + }; +})(); + +ScrollRequest.prototype.tick = function (deltaTime) { + if (!this.isActive) { + this.isActive = true; + this.onStart(); + } + this.value = this.computeCurrentScrollValue(); + this.speed = this.computeSpeed(deltaTime); + this.value = this.computeNextScrollValue(deltaTime); + this.duration += deltaTime; + return this.value; +}; + +ScrollRequest.prototype.onStart = function () { + var item = this.item; + var onStart = getItemAutoScrollSettings(item).onStart; + if (isFunction(onStart)) onStart(item, this.element, this.direction); +}; + +ScrollRequest.prototype.onStop = function () { + var item = this.item; + var onStop = getItemAutoScrollSettings(item).onStop; + if (isFunction(onStop)) onStop(item, this.element, this.direction); + // Manually nudge sort to happen. There's a good chance that the item is still + // after the scroll stops which means that the next sort will be triggered + // only after the item is moved or it's parent scrolled. + if (item._drag) item._drag.sort(); +}; diff --git a/src/AutoScroller/constants.js b/src/AutoScroller/constants.js new file mode 100644 index 00000000..37f938e9 --- /dev/null +++ b/src/AutoScroller/constants.js @@ -0,0 +1,15 @@ +/** + * Muuri AutoScroller + * Copyright (c) 2019-present, Niklas Rämö + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md + */ + +export var AXIS_X = 1; +export var AXIS_Y = 2; +export var FORWARD = 4; +export var BACKWARD = 8; +export var LEFT = AXIS_X | BACKWARD; +export var RIGHT = AXIS_X | FORWARD; +export var UP = AXIS_Y | BACKWARD; +export var DOWN = AXIS_Y | FORWARD; diff --git a/src/AutoScroller/utils.js b/src/AutoScroller/utils.js new file mode 100644 index 00000000..d62c1bf8 --- /dev/null +++ b/src/AutoScroller/utils.js @@ -0,0 +1,139 @@ +/** + * Muuri AutoScroller + * Copyright (c) 2019-present, Niklas Rämö + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/src/AutoScroller/LICENSE.md + */ + +import getStyleAsFloat from '../utils/getStyleAsFloat'; + +var DOC_ELEM = document.documentElement; +var BODY = document.body; +var THRESHOLD_DATA = { value: 0, offset: 0 }; + +/** + * @param {HTMLElement|Window} element + * @returns {HTMLElement|Window} + */ +export function getScrollElement(element) { + if (element === window || element === DOC_ELEM || element === BODY) { + return window; + } else { + return element; + } +} + +/** + * @param {HTMLElement|Window} element + * @returns {Number} + */ +export function getScrollLeft(element) { + return element === window ? element.pageXOffset : element.scrollLeft; +} + +/** + * @param {HTMLElement|Window} element + * @returns {Number} + */ +export function getScrollTop(element) { + return element === window ? element.pageYOffset : element.scrollTop; +} + +/** + * @param {HTMLElement|Window} element + * @returns {Number} + */ +export function getScrollLeftMax(element) { + if (element === window) { + return DOC_ELEM.scrollWidth - DOC_ELEM.clientWidth; + } else { + return element.scrollWidth - element.clientWidth; + } +} + +/** + * @param {HTMLElement|Window} element + * @returns {Number} + */ +export function getScrollTopMax(element) { + if (element === window) { + return DOC_ELEM.scrollHeight - DOC_ELEM.clientHeight; + } else { + return element.scrollHeight - element.clientHeight; + } +} + +/** + * Get window's or element's client rectangle data relative to the element's + * content dimensions (includes inner size + padding, excludes scrollbars, + * borders and margins). + * + * @param {HTMLElement|Window} element + * @returns {Rectangle} + */ +export function getContentRect(element, result) { + result = result || {}; + + if (element === window) { + result.width = DOC_ELEM.clientWidth; + result.height = DOC_ELEM.clientHeight; + result.left = 0; + result.right = result.width; + result.top = 0; + result.bottom = result.height; + } else { + var bcr = element.getBoundingClientRect(); + var borderLeft = element.clientLeft || getStyleAsFloat(element, 'border-left-width'); + var borderTop = element.clientTop || getStyleAsFloat(element, 'border-top-width'); + result.width = element.clientWidth; + result.height = element.clientHeight; + result.left = bcr.left + borderLeft; + result.right = result.left + result.width; + result.top = bcr.top + borderTop; + result.bottom = result.top + result.height; + } + + return result; +} + +/** + * @param {Item} item + * @returns {Object} + */ +export function getItemAutoScrollSettings(item) { + return item._drag._getGrid()._settings.dragAutoScroll; +} + +/** + * @param {Item} item + */ +export function prepareItemScrollSync(item) { + if (!item._drag) return; + item._drag._prepareScroll(); +} + +/** + * @param {Item} item + */ +export function applyItemScrollSync(item) { + if (!item._drag || !item._isActive) return; + var drag = item._drag; + drag._scrollDiffX = drag._scrollDiffY = 0; + item._setTranslate(drag._left, drag._top); +} + +/** + * Compute threshold value and edge offset. + * + * @param {Number} threshold + * @param {Number} safeZone + * @param {Number} itemSize + * @param {Number} targetSize + * @returns {Object} + */ +export function computeThreshold(threshold, safeZone, itemSize, targetSize) { + THRESHOLD_DATA.value = Math.min(targetSize / 2, threshold); + THRESHOLD_DATA.offset = + Math.max(0, itemSize + THRESHOLD_DATA.value * 2 + targetSize * safeZone - targetSize) / 2; + return THRESHOLD_DATA; +} diff --git a/src/Dragger/Dragger.js b/src/Dragger/Dragger.js index 4ecf5791..88015a31 100644 --- a/src/Dragger/Dragger.js +++ b/src/Dragger/Dragger.js @@ -5,40 +5,24 @@ * https://github.com/haltu/muuri/blob/master/src/Dragger/LICENSE.md */ +import { HAS_TOUCH_EVENTS, HAS_POINTER_EVENTS, HAS_MS_POINTER_EVENTS } from '../constants'; + import Emitter from '../Emitter/Emitter'; +import EdgeHack from './EdgeHack'; import getPrefixedPropName from '../utils/getPrefixedPropName'; -import raf from '../utils/raf'; - -// Detect support for passive events: -// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection -var isPassiveEventsSupported = false; -try { - var passiveOpts = Object.defineProperty({}, 'passive', { - get: function() { - isPassiveEventsSupported = true; - } - }); - window.addEventListener('testPassive', null, passiveOpts); - window.removeEventListener('testPassive', null, passiveOpts); -} catch (e) {} +import hasPassiveEvents from '../utils/hasPassiveEvents'; -// Dragger events. -export var events = { - start: 'start', - move: 'move', - end: 'end', - cancel: 'cancel' -}; +var ua = window.navigator.userAgent.toLowerCase(); +var isEdge = ua.indexOf('edge') > -1; +var isIE = ua.indexOf('trident') > -1; +var isFirefox = ua.indexOf('firefox') > -1; +var isAndroid = ua.indexOf('android') > -1; -var hasTouchEvents = !!('ontouchstart' in window || window.TouchEvent); -var hasPointerEvents = !!window.PointerEvent; -var hasMsPointerEvents = !!window.navigator.msPointerEnabled; -var isAndroid = /(android)/i.test(window.navigator.userAgent); -var listenerOptions = isPassiveEventsSupported ? { passive: true } : false; +var listenerOptions = hasPassiveEvents() ? { passive: true } : false; var taProp = 'touchAction'; -var taPropPrefixed = getPrefixedPropName(window.document.documentElement.style, taProp); +var taPropPrefixed = getPrefixedPropName(document.documentElement.style, taProp); var taDefaultValue = 'auto'; /** @@ -55,7 +39,7 @@ function Dragger(element, cssProps) { this._isDestroyed = false; this._cssProps = {}; this._touchAction = ''; - this._startEvent = null; + this._isActive = false; this._pointerId = null; this._startTime = 0; @@ -64,34 +48,31 @@ function Dragger(element, cssProps) { this._currentX = 0; this._currentY = 0; - this._preStartCheck = this._preStartCheck.bind(this); - this._abortNonCancelable = this._abortNonCancelable.bind(this); this._onStart = this._onStart.bind(this); this._onMove = this._onMove.bind(this); this._onCancel = this._onCancel.bind(this); this._onEnd = this._onEnd.bind(this); - // Apply initial css props. + // Can't believe had to build a freaking class for a hack! + this._edgeHack = null; + if ((isEdge || isIE) && (HAS_POINTER_EVENTS || HAS_MS_POINTER_EVENTS)) { + this._edgeHack = new EdgeHack(this); + } + + // Apply initial CSS props. this.setCssProps(cssProps); - // If touch action was not provided with initial css props let's assume it's + // If touch action was not provided with initial CSS props let's assume it's // auto. if (!this._touchAction) { this.setTouchAction(taDefaultValue); } - // Prevent native link/image dragging for the item and it's ancestors. + // Prevent native link/image dragging for the item and it's children. element.addEventListener('dragstart', Dragger._preventDefault, false); // Listen to start event. - element.addEventListener(Dragger._events.start, this._preStartCheck, listenerOptions); - - // If we have touch events, but no pointer events we need to also listen for - // mouse events in addition to touch events for devices which support both - // mouse and touch interaction. - if (hasTouchEvents && !hasPointerEvents && !hasMsPointerEvents) { - element.addEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions); - } + element.addEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions); } /** @@ -103,39 +84,46 @@ Dragger._pointerEvents = { start: 'pointerdown', move: 'pointermove', cancel: 'pointercancel', - end: 'pointerup' + end: 'pointerup', }; Dragger._msPointerEvents = { start: 'MSPointerDown', move: 'MSPointerMove', cancel: 'MSPointerCancel', - end: 'MSPointerUp' + end: 'MSPointerUp', }; Dragger._touchEvents = { start: 'touchstart', move: 'touchmove', cancel: 'touchcancel', - end: 'touchend' + end: 'touchend', }; Dragger._mouseEvents = { start: 'mousedown', move: 'mousemove', cancel: '', - end: 'mouseup' + end: 'mouseup', }; -Dragger._events = (function() { - if (hasPointerEvents) return Dragger._pointerEvents; - if (hasMsPointerEvents) return Dragger._msPointerEvents; - if (hasTouchEvents) return Dragger._touchEvents; +Dragger._inputEvents = (function () { + if (HAS_TOUCH_EVENTS) return Dragger._touchEvents; + if (HAS_POINTER_EVENTS) return Dragger._pointerEvents; + if (HAS_MS_POINTER_EVENTS) return Dragger._msPointerEvents; return Dragger._mouseEvents; })(); Dragger._emitter = new Emitter(); +Dragger._emitterEvents = { + start: 'start', + move: 'move', + end: 'end', + cancel: 'cancel', +}; + Dragger._activeInstances = []; /** @@ -143,53 +131,55 @@ Dragger._activeInstances = []; * ************************ */ -Dragger._preventDefault = function(e) { +Dragger._preventDefault = function (e) { if (e.preventDefault && e.cancelable !== false) e.preventDefault(); }; -Dragger._activateInstance = function(instance) { +Dragger._activateInstance = function (instance) { var index = Dragger._activeInstances.indexOf(instance); if (index > -1) return; Dragger._activeInstances.push(instance); - Dragger._emitter.on(events.move, instance._onMove); - Dragger._emitter.on(events.cancel, instance._onCancel); - Dragger._emitter.on(events.end, instance._onEnd); + Dragger._emitter.on(Dragger._emitterEvents.move, instance._onMove); + Dragger._emitter.on(Dragger._emitterEvents.cancel, instance._onCancel); + Dragger._emitter.on(Dragger._emitterEvents.end, instance._onEnd); if (Dragger._activeInstances.length === 1) { Dragger._bindListeners(); } }; -Dragger._deactivateInstance = function(instance) { +Dragger._deactivateInstance = function (instance) { var index = Dragger._activeInstances.indexOf(instance); if (index === -1) return; Dragger._activeInstances.splice(index, 1); - Dragger._emitter.off(events.move, instance._onMove); - Dragger._emitter.off(events.cancel, instance._onCancel); - Dragger._emitter.off(events.end, instance._onEnd); + Dragger._emitter.off(Dragger._emitterEvents.move, instance._onMove); + Dragger._emitter.off(Dragger._emitterEvents.cancel, instance._onCancel); + Dragger._emitter.off(Dragger._emitterEvents.end, instance._onEnd); if (!Dragger._activeInstances.length) { Dragger._unbindListeners(); } }; -Dragger._bindListeners = function() { - var events = Dragger._events; - window.addEventListener(events.move, Dragger._onMove, listenerOptions); - window.addEventListener(events.end, Dragger._onEnd, listenerOptions); - events.cancel && window.addEventListener(events.cancel, Dragger._onCancel, listenerOptions); +Dragger._bindListeners = function () { + window.addEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions); + window.addEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions); + if (Dragger._inputEvents.cancel) { + window.addEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions); + } }; -Dragger._unbindListeners = function() { - var events = Dragger._events; - window.removeEventListener(events.move, Dragger._onMove, listenerOptions); - window.removeEventListener(events.end, Dragger._onEnd, listenerOptions); - events.cancel && window.removeEventListener(events.cancel, Dragger._onCancel, listenerOptions); +Dragger._unbindListeners = function () { + window.removeEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions); + window.removeEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions); + if (Dragger._inputEvents.cancel) { + window.removeEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions); + } }; -Dragger._getEventPointerId = function(event) { +Dragger._getEventPointerId = function (event) { // If we have pointer id available let's use it. if (typeof event.pointerId === 'number') { return event.pointerId; @@ -204,7 +194,7 @@ Dragger._getEventPointerId = function(event) { return 1; }; -Dragger._getTouchById = function(event, id) { +Dragger._getTouchById = function (event, id) { // If we have a pointer event return the whole event if there's a match, and // null otherwise. if (typeof event.pointerId === 'number') { @@ -227,16 +217,16 @@ Dragger._getTouchById = function(event, id) { return event; }; -Dragger._onMove = function(e) { - Dragger._emitter.emit(events.move, e); +Dragger._onMove = function (e) { + Dragger._emitter.emit(Dragger._emitterEvents.move, e); }; -Dragger._onCancel = function(e) { - Dragger._emitter.emit(events.cancel, e); +Dragger._onCancel = function (e) { + Dragger._emitter.emit(Dragger._emitterEvents.cancel, e); }; -Dragger._onEnd = function(e) { - Dragger._emitter.emit(events.end, e); +Dragger._onEnd = function (e) { + Dragger._emitter.emit(Dragger._emitterEvents.end, e); }; /** @@ -248,25 +238,15 @@ Dragger._onEnd = function(e) { * Reset current drag operation (if any). * * @private - * @memberof Dragger.prototype */ -Dragger.prototype._reset = function() { - if (this._isDestroyed) return; - +Dragger.prototype._reset = function () { this._pointerId = null; this._startTime = 0; this._startX = 0; this._startY = 0; this._currentX = 0; this._currentY = 0; - this._startEvent = null; - - this._element.removeEventListener( - Dragger._touchEvents.start, - this._abortNonCancelable, - listenerOptions - ); - + this._isActive = false; Dragger._deactivateInstance(this); }; @@ -274,12 +254,11 @@ Dragger.prototype._reset = function() { * Create a custom dragger event from a raw event. * * @private - * @memberof Dragger.prototype * @param {String} type * @param {(PointerEvent|TouchEvent|MouseEvent)} e - * @returns {DraggerEvent} + * @returns {Object} */ -Dragger.prototype._createEvent = function(type, e) { +Dragger.prototype._createEvent = function (type, e) { var touch = this._getTrackedTouch(e); return { // Hammer.js compatibility interface. @@ -288,9 +267,10 @@ Dragger.prototype._createEvent = function(type, e) { distance: this.getDistance(), deltaX: this.getDeltaX(), deltaY: this.getDeltaY(), - deltaTime: type === events.start ? 0 : this.getDeltaTime(), - isFirst: type === events.start, - isFinal: type === events.end || type === events.cancel, + deltaTime: type === Dragger._emitterEvents.start ? 0 : this.getDeltaTime(), + isFirst: type === Dragger._emitterEvents.start, + isFinal: type === Dragger._emitterEvents.end || type === Dragger._emitterEvents.cancel, + pointerType: e.pointerType || (e.touches ? 'touch' : 'mouse'), // Partial Touch API interface. identifier: this._pointerId, screenX: touch.screenX, @@ -299,7 +279,7 @@ Dragger.prototype._createEvent = function(type, e) { clientY: touch.clientY, pageX: touch.pageX, pageY: touch.pageY, - target: touch.target + target: touch.target, }; }; @@ -307,11 +287,10 @@ Dragger.prototype._createEvent = function(type, e) { * Emit a raw event as dragger event internally. * * @private - * @memberof Dragger.prototype * @param {String} type * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ -Dragger.prototype._emit = function(type, e) { +Dragger.prototype._emit = function (type, e) { this._emitter.emit(type, this._createEvent(type, e)); }; @@ -324,135 +303,68 @@ Dragger.prototype._emit = function(type, e) { * it will be returned immediately. * * @private - * @memberof Dragger.prototype - * @param {(PointerEvent|TouchEvent|MouseEvent)} + * @param {(PointerEvent|TouchEvent|MouseEvent)} e * @returns {?(Touch|PointerEvent|MouseEvent)} */ -Dragger.prototype._getTrackedTouch = function(e) { - if (this._pointerId === null) { - return null; - } else { - return Dragger._getTouchById(e, this._pointerId); - } +Dragger.prototype._getTrackedTouch = function (e) { + if (this._pointerId === null) return null; + return Dragger._getTouchById(e, this._pointerId); }; /** - * A pre-handler for start event that checks if we can start dragging. + * Handler for start event. * * @private - * @memberof Dragger.prototype * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ -Dragger.prototype._preStartCheck = function(e) { +Dragger.prototype._onStart = function (e) { if (this._isDestroyed) return; - // Make sure the element is not being dragged currently. - if (this.isDragging()) return; - - // Special cancelable check for Android to prevent drag procedure from - // starting if native scrolling is in progress. Part 1. - if (isAndroid && e.cancelable === false) return; - - // Make sure left button is pressed on mouse. - if (e.button) return; + // If pointer id is already assigned let's return early. + if (this._pointerId !== null) return; // Get (and set) pointer id. this._pointerId = Dragger._getEventPointerId(e); if (this._pointerId === null) return; - // Store the start event and trigger start (async or sync). Pointer events - // are emitted before touch events if the browser supports both of them. And - // if you try to move an element before `touchstart` is emitted the pointer - // events for that element will be canceled. The fix is to delay the emitted - // pointer events in such a scenario by one frame so that `touchstart` has - // time to be emitted before the element is (potentially) moved. - this._startEvent = e; - if (hasTouchEvents && (hasPointerEvents || hasMsPointerEvents)) { - // Special cancelable check for Android to prevent drag procedure from - // starting if native scrolling is in progress. Part 2. - if (isAndroid) { - this._element.addEventListener( - Dragger._touchEvents.start, - this._abortNonCancelable, - listenerOptions - ); - } - raf(this._onStart); - } else { - this._onStart(); - } -}; - -/** - * Abort start event if it turns out to be non-cancelable. - * - * @private - * @memberof Dragger.prototype - * @param {(PointerEvent|TouchEvent|MouseEvent)} e - */ -Dragger.prototype._abortNonCancelable = function(e) { - this._element.removeEventListener( - Dragger._touchEvents.start, - this._abortNonCancelable, - listenerOptions - ); - - if (this._startEvent && e.cancelable === false) { - this._pointerId = null; - this._startEvent = null; - } -}; - -/** - * Start the drag procedure if possible. - * - * @private - * @memberof Dragger.prototype - */ -Dragger.prototype._onStart = function() { - var e = this._startEvent; - if (!e) return; - - this._startEvent = null; - + // Setup initial data and emit start event. var touch = this._getTrackedTouch(e); - if (!touch) return; - - // Set up init data and emit start event. this._startX = this._currentX = touch.clientX; this._startY = this._currentY = touch.clientY; this._startTime = Date.now(); - this._emit(events.start, e); - Dragger._activateInstance(this); + this._isActive = true; + this._emit(Dragger._emitterEvents.start, e); + + // If the drag procedure was not reset within the start procedure let's + // activate the instance (start listening to move/cancel/end events). + if (this._isActive) { + Dragger._activateInstance(this); + } }; /** * Handler for move event. * * @private - * @memberof Dragger.prototype * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ -Dragger.prototype._onMove = function(e) { +Dragger.prototype._onMove = function (e) { var touch = this._getTrackedTouch(e); if (!touch) return; - this._currentX = touch.clientX; this._currentY = touch.clientY; - this._emit(events.move, e); + this._emit(Dragger._emitterEvents.move, e); }; /** - * Handler for move cancel event. + * Handler for cancel event. * * @private - * @memberof Dragger.prototype * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ -Dragger.prototype._onCancel = function(e) { +Dragger.prototype._onCancel = function (e) { if (!this._getTrackedTouch(e)) return; - - this._emit(events.cancel, e); + this._emit(Dragger._emitterEvents.cancel, e); this._reset(); }; @@ -460,13 +372,11 @@ Dragger.prototype._onCancel = function(e) { * Handler for end event. * * @private - * @memberof Dragger.prototype * @param {(PointerEvent|TouchEvent|MouseEvent)} e */ -Dragger.prototype._onEnd = function(e) { +Dragger.prototype._onEnd = function (e) { if (!this._getTrackedTouch(e)) return; - - this._emit(events.end, e); + this._emit(Dragger._emitterEvents.end, e); this._reset(); }; @@ -479,21 +389,19 @@ Dragger.prototype._onEnd = function(e) { * Check if the element is being dragged at the moment. * * @public - * @memberof Dragger.prototype * @returns {Boolean} */ -Dragger.prototype.isDragging = function() { - return this._pointerId !== null; +Dragger.prototype.isActive = function () { + return this._isActive; }; /** * Set element's touch-action CSS property. * * @public - * @memberof Dragger.prototype * @param {String} value */ -Dragger.prototype.setTouchAction = function(value) { +Dragger.prototype.setTouchAction = function (value) { // Store unmodified touch action value (we trust user input here). this._touchAction = value; @@ -507,11 +415,13 @@ Dragger.prototype.setTouchAction = function(value) { // that prevents default action on touch start event. A dirty hack, but best // we can do for now. The other options would be to somehow polyfill the // unsupported touch action behavior with custom heuristics which sounds like - // a can of worms. - if (hasTouchEvents) { - this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false); - if (this._element.style[taPropPrefixed] !== value) { - this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false); + // a can of worms. We do a special exception here for Firefox Android which's + // touch-action does not work properly if the dragged element is moved in the + // the DOM tree on touchstart. + if (HAS_TOUCH_EVENTS) { + this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); + if (this._element.style[taPropPrefixed] !== value || (isFirefox && isAndroid)) { + this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); } } }; @@ -521,10 +431,9 @@ Dragger.prototype.setTouchAction = function(value) { * props with value pairs as it's first argument. * * @public - * @memberof Dragger.prototype * @param {Object} [newProps] */ -Dragger.prototype.setCssProps = function(newProps) { +Dragger.prototype.setCssProps = function (newProps) { if (!newProps) return; var currentProps = this._cssProps; @@ -564,10 +473,9 @@ Dragger.prototype.setCssProps = function(newProps) { * Positive value indicates movement from left to right. * * @public - * @memberof Dragger.prototype * @returns {Number} */ -Dragger.prototype.getDeltaX = function() { +Dragger.prototype.getDeltaX = function () { return this._currentX - this._startX; }; @@ -576,10 +484,9 @@ Dragger.prototype.getDeltaX = function() { * Positive value indicates movement from top to bottom. * * @public - * @memberof Dragger.prototype * @returns {Number} */ -Dragger.prototype.getDeltaY = function() { +Dragger.prototype.getDeltaY = function () { return this._currentY - this._startY; }; @@ -587,10 +494,9 @@ Dragger.prototype.getDeltaY = function() { * How far (in pixels) has pointer moved from start position. * * @public - * @memberof Dragger.prototype * @returns {Number} */ -Dragger.prototype.getDistance = function() { +Dragger.prototype.getDistance = function () { var x = this.getDeltaX(); var y = this.getDeltaY(); return Math.sqrt(x * x + y * y); @@ -600,10 +506,9 @@ Dragger.prototype.getDistance = function() { * How long has pointer been dragged. * * @public - * @memberof Dragger.prototype * @returns {Number} */ -Dragger.prototype.getDeltaTime = function() { +Dragger.prototype.getDeltaTime = function () { return this._startTime ? Date.now() - this._startTime : 0; }; @@ -611,12 +516,11 @@ Dragger.prototype.getDeltaTime = function() { * Bind drag event listeners. * * @public - * @memberof Dragger.prototype * @param {String} eventName * - 'start', 'move', 'cancel' or 'end'. * @param {Function} listener */ -Dragger.prototype.on = function(eventName, listener) { +Dragger.prototype.on = function (eventName, listener) { this._emitter.on(eventName, listener); }; @@ -624,12 +528,11 @@ Dragger.prototype.on = function(eventName, listener) { * Unbind drag event listeners. * * @public - * @memberof Dragger.prototype * @param {String} eventName * - 'start', 'move', 'cancel' or 'end'. * @param {Function} listener */ -Dragger.prototype.off = function(events, listener) { +Dragger.prototype.off = function (eventName, listener) { this._emitter.off(eventName, listener); }; @@ -637,13 +540,13 @@ Dragger.prototype.off = function(events, listener) { * Destroy the instance and unbind all drag event listeners. * * @public - * @memberof Dragger.prototype */ -Dragger.prototype.destroy = function() { +Dragger.prototype.destroy = function () { if (this._isDestroyed) return; var element = this._element; - var events = Dragger._events; + + if (this._edgeHack) this._edgeHack.destroy(); // Reset data and deactivate the instance. this._reset(); @@ -652,10 +555,9 @@ Dragger.prototype.destroy = function() { this._emitter.destroy(); // Unbind event handlers. - element.removeEventListener(events.start, this._preStartCheck, listenerOptions); - element.removeEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions); + element.removeEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions); element.removeEventListener('dragstart', Dragger._preventDefault, false); - element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false); + element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); // Reset styles. for (var prop in this._cssProps) { diff --git a/src/Dragger/EdgeHack.js b/src/Dragger/EdgeHack.js new file mode 100644 index 00000000..48991c70 --- /dev/null +++ b/src/Dragger/EdgeHack.js @@ -0,0 +1,119 @@ +/** + * Muuri Dragger + * Copyright (c) 2018-present, Niklas Rämö + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/src/Dragger/LICENSE.md + */ + +import { HAS_POINTER_EVENTS, HAS_MS_POINTER_EVENTS } from '../constants'; + +var pointerout = HAS_POINTER_EVENTS ? 'pointerout' : HAS_MS_POINTER_EVENTS ? 'MSPointerOut' : ''; +var waitDuration = 100; + +/** + * If you happen to use Edge or IE on a touch capable device there is a + * a specific case where pointercancel and pointerend events are never emitted, + * even though one them should always be emitted when you release your finger + * from the screen. The bug appears specifically when Muuri shifts the dragged + * element's position in the DOM after pointerdown event, IE and Edge don't like + * that behaviour and quite often forget to emit the pointerend/pointercancel + * event. But, they do emit pointerout event so we utilize that here. + * Specifically, if there has been no pointermove event within 100 milliseconds + * since the last pointerout event we force cancel the drag operation. This hack + * works surprisingly well 99% of the time. There is that 1% chance there still + * that dragged items get stuck but it is what it is. + * + * @class + * @param {Dragger} dragger + */ +function EdgeHack(dragger) { + if (!pointerout) return; + + this._dragger = dragger; + this._timeout = null; + this._outEvent = null; + this._isActive = false; + + this._addBehaviour = this._addBehaviour.bind(this); + this._removeBehaviour = this._removeBehaviour.bind(this); + this._onTimeout = this._onTimeout.bind(this); + this._resetData = this._resetData.bind(this); + this._onStart = this._onStart.bind(this); + this._onOut = this._onOut.bind(this); + + this._dragger.on('start', this._onStart); +} + +/** + * @private + */ +EdgeHack.prototype._addBehaviour = function () { + if (this._isActive) return; + this._isActive = true; + this._dragger.on('move', this._resetData); + this._dragger.on('cancel', this._removeBehaviour); + this._dragger.on('end', this._removeBehaviour); + window.addEventListener(pointerout, this._onOut); +}; + +/** + * @private + */ +EdgeHack.prototype._removeBehaviour = function () { + if (!this._isActive) return; + this._dragger.off('move', this._resetData); + this._dragger.off('cancel', this._removeBehaviour); + this._dragger.off('end', this._removeBehaviour); + window.removeEventListener(pointerout, this._onOut); + this._resetData(); + this._isActive = false; +}; + +/** + * @private + */ +EdgeHack.prototype._resetData = function () { + window.clearTimeout(this._timeout); + this._timeout = null; + this._outEvent = null; +}; + +/** + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + */ +EdgeHack.prototype._onStart = function (e) { + if (e.pointerType === 'mouse') return; + this._addBehaviour(); +}; + +/** + * @private + * @param {(PointerEvent|TouchEvent|MouseEvent)} e + */ +EdgeHack.prototype._onOut = function (e) { + if (!this._dragger._getTrackedTouch(e)) return; + this._resetData(); + this._outEvent = e; + this._timeout = window.setTimeout(this._onTimeout, waitDuration); +}; + +/** + * @private + */ +EdgeHack.prototype._onTimeout = function () { + var e = this._outEvent; + this._resetData(); + if (this._dragger.isActive()) this._dragger._onCancel(e); +}; + +/** + * @public + */ +EdgeHack.prototype.destroy = function () { + if (!pointerout) return; + this._dragger.off('start', this._onStart); + this._removeBehaviour(); +}; + +export default EdgeHack; diff --git a/src/Emitter/Emitter.js b/src/Emitter/Emitter.js index 4da09190..68fe29bb 100644 --- a/src/Emitter/Emitter.js +++ b/src/Emitter/Emitter.js @@ -14,7 +14,7 @@ function Emitter() { this._events = {}; this._queue = []; this._counter = 0; - this._isDestroyed = false; + this._clearOnEmit = false; } /** @@ -26,13 +26,12 @@ function Emitter() { * Bind an event listener. * * @public - * @memberof Emitter.prototype * @param {String} event * @param {Function} listener * @returns {Emitter} */ -Emitter.prototype.on = function(event, listener) { - if (this._isDestroyed) return this; +Emitter.prototype.on = function (event, listener) { + if (!this._events || !event || !listener) return this; // Get listeners queue and create it if it does not exist. var listeners = this._events[event]; @@ -48,28 +47,40 @@ Emitter.prototype.on = function(event, listener) { * Unbind all event listeners that match the provided listener function. * * @public - * @memberof Emitter.prototype * @param {String} event - * @param {Function} [listener] + * @param {Function} listener * @returns {Emitter} */ -Emitter.prototype.off = function(event, listener) { - if (this._isDestroyed) return this; +Emitter.prototype.off = function (event, listener) { + if (!this._events || !event || !listener) return this; // Get listeners and return immediately if none is found. var listeners = this._events[event]; if (!listeners || !listeners.length) return this; - // If no specific listener is provided remove all listeners. - if (!listener) { - listeners.length = 0; - return this; + // Remove all matching listeners. + var index; + while ((index = listeners.indexOf(listener)) !== -1) { + listeners.splice(index, 1); } - // Remove all matching listeners. - var i = listeners.length; - while (i--) { - if (listener === listeners[i]) listeners.splice(i, 1); + return this; +}; + +/** + * Unbind all listeners of the provided event. + * + * @public + * @param {String} event + * @returns {Emitter} + */ +Emitter.prototype.clear = function (event) { + if (!this._events || !event) return this; + + var listeners = this._events[event]; + if (listeners) { + listeners.length = 0; + delete this._events[event]; } return this; @@ -79,31 +90,46 @@ Emitter.prototype.off = function(event, listener) { * Emit all listeners in a specified event with the provided arguments. * * @public - * @memberof Emitter.prototype * @param {String} event - * @param {*} [arg1] - * @param {*} [arg2] - * @param {*} [arg3] + * @param {...*} [args] * @returns {Emitter} */ -Emitter.prototype.emit = function(event, arg1, arg2, arg3) { - if (this._isDestroyed) return this; +Emitter.prototype.emit = function (event) { + if (!this._events || !event) { + this._clearOnEmit = false; + return this; + } // Get event listeners and quit early if there's no listeners. var listeners = this._events[event]; - if (!listeners || !listeners.length) return this; + if (!listeners || !listeners.length) { + this._clearOnEmit = false; + return this; + } var queue = this._queue; - var qLength = queue.length; - var aLength = arguments.length - 1; - var i; + var startIndex = queue.length; + var argsLength = arguments.length - 1; + var args; + + // If we have more than 3 arguments let's put the arguments in an array and + // apply it to the listeners. + if (argsLength > 3) { + args = []; + args.push.apply(args, arguments); + args.shift(); + } // Add the current listeners to the callback queue before we process them. // This is necessary to guarantee that all of the listeners are called in // correct order even if new event listeners are removed/added during // processing and/or events are emitted during processing. - for (i = 0; i < listeners.length; i++) { - queue.push(listeners[i]); + queue.push.apply(queue, listeners); + + // Reset the event's listeners if need be. + if (this._clearOnEmit) { + listeners.length = 0; + this._clearOnEmit = false; } // Increment queue counter. This is needed for the scenarios where emit is @@ -113,15 +139,18 @@ Emitter.prototype.emit = function(event, arg1, arg2, arg3) { ++this._counter; // Process the queue (the specific part of it for this emit). - for (i = qLength, qLength = queue.length; i < qLength; i++) { + var i = startIndex; + var endIndex = queue.length; + for (; i < endIndex; i++) { // prettier-ignore - aLength === 0 ? queue[i]() : - aLength === 1 ? queue[i](arg1) : - aLength === 2 ? queue[i](arg1, arg2) : - queue[i](arg1, arg2, arg3); + argsLength === 0 ? queue[i]() : + argsLength === 1 ? queue[i](arguments[1]) : + argsLength === 2 ? queue[i](arguments[1], arguments[2]) : + argsLength === 3 ? queue[i](arguments[1], arguments[2], arguments[3]) : + queue[i].apply(null, args); // Stop processing if the emitter is destroyed. - if (this._isDestroyed) return this; + if (!this._events) return this; } // Decrement queue process counter. @@ -134,32 +163,45 @@ Emitter.prototype.emit = function(event, arg1, arg2, arg3) { }; /** - * Destroy emitter instance. Basically just removes all bound listeners. + * Emit all listeners in a specified event with the provided arguments and + * remove the event's listeners just before calling the them. This method allows + * the emitter to serve as a queue where all listeners are called only once. * * @public - * @memberof Emitter.prototype + * @param {String} event + * @param {...*} [args] * @returns {Emitter} */ -Emitter.prototype.destroy = function() { - if (this._isDestroyed) return this; - - var events = this._events; - var event; +Emitter.prototype.burst = function () { + if (!this._events) return this; + this._clearOnEmit = true; + this.emit.apply(this, arguments); + return this; +}; - // Flag as destroyed. - this._isDestroyed = true; +/** + * Check how many listeners there are for a specific event. + * + * @public + * @param {String} event + * @returns {Boolean} + */ +Emitter.prototype.countListeners = function (event) { + if (!this._events) return 0; + var listeners = this._events[event]; + return listeners ? listeners.length : 0; +}; - // Reset queue (if queue is currently processing this will also stop that). +/** + * Destroy emitter instance. Basically just removes all bound listeners. + * + * @public + * @returns {Emitter} + */ +Emitter.prototype.destroy = function () { + if (!this._events) return this; this._queue.length = this._counter = 0; - - // Remove all listeners. - for (event in events) { - if (events[event]) { - events[event].length = 0; - events[event] = undefined; - } - } - + this._events = null; return this; }; diff --git a/src/Grid/Grid.js b/src/Grid/Grid.js index c03b7dd0..5bce4dab 100644 --- a/src/Grid/Grid.js +++ b/src/Grid/Grid.js @@ -5,58 +5,62 @@ */ import { - actionMove, - actionSwap, - eventSynchronize, - eventLayoutStart, - eventLayoutEnd, - eventAdd, - eventRemove, - eventShowStart, - eventShowEnd, - eventHideStart, - eventHideEnd, - eventFilter, - eventSort, - eventMove, - eventDestroy, - gridInstances, - namespace -} from '../shared'; + ACTION_MOVE, + ACTION_SWAP, + EVENT_SYNCHRONIZE, + EVENT_LAYOUT_START, + EVENT_LAYOUT_ABORT, + EVENT_LAYOUT_END, + EVENT_ADD, + EVENT_REMOVE, + EVENT_SHOW_START, + EVENT_SHOW_END, + EVENT_HIDE_START, + EVENT_HIDE_END, + EVENT_FILTER, + EVENT_SORT, + EVENT_MOVE, + EVENT_DESTROY, + GRID_INSTANCES, + ITEM_ELEMENT_MAP, + MAX_SAFE_FLOAT32_INTEGER, +} from '../constants'; -import Emitter from '../Emitter/Emitter'; import Item from '../Item/Item'; -import ItemAnimate from '../Item/ItemAnimate'; import ItemDrag from '../Item/ItemDrag'; import ItemDragPlaceholder from '../Item/ItemDragPlaceholder'; import ItemLayout from '../Item/ItemLayout'; import ItemMigrate from '../Item/ItemMigrate'; -import ItemRelease from '../Item/ItemRelease'; +import ItemDragRelease from '../Item/ItemDragRelease'; import ItemVisibility from '../Item/ItemVisibility'; +import Emitter from '../Emitter/Emitter'; +import Animator from '../Animator/Animator'; import Packer from '../Packer/Packer'; import Dragger from '../Dragger/Dragger'; +import AutoScroller from '../AutoScroller/AutoScroller'; import addClass from '../utils/addClass'; +import arrayInsert from '../utils/arrayInsert'; import arrayMove from '../utils/arrayMove'; import arraySwap from '../utils/arraySwap'; import createUid from '../utils/createUid'; import debounce from '../utils/debounce'; import elementMatches from '../utils/elementMatches'; +import getPrefixedPropName from '../utils/getPrefixedPropName'; import getStyle from '../utils/getStyle'; import getStyleAsFloat from '../utils/getStyleAsFloat'; -import arrayInsert from '../utils/arrayInsert'; import isFunction from '../utils/isFunction'; import isNodeList from '../utils/isNodeList'; import isPlainObject from '../utils/isPlainObject'; +import noop from '../utils/noop'; import removeClass from '../utils/removeClass'; +import setStyles from '../utils/setStyles'; import toArray from '../utils/toArray'; -var packer = new Packer(); -var noop = function() {}; - -var numberType = 'number'; -var stringType = 'string'; -var instantLayout = 'instant'; +var NUMBER_TYPE = 'number'; +var STRING_TYPE = 'string'; +var INSTANT_LAYOUT = 'instant'; +var layoutId = 0; /** * Creates a new Grid instance. @@ -64,31 +68,31 @@ var instantLayout = 'instant'; * @class * @param {(HTMLElement|String)} element * @param {Object} [options] - * @param {?(HTMLElement[]|NodeList|String)} [options.items] + * @param {(String|HTMLElement[]|NodeList|HTMLCollection)} [options.items="*"] * @param {Number} [options.showDuration=300] * @param {String} [options.showEasing="ease"] - * @param {Object} [options.visibleStyles] + * @param {Object} [options.visibleStyles={opacity: "1", transform: "scale(1)"}] * @param {Number} [options.hideDuration=300] * @param {String} [options.hideEasing="ease"] - * @param {Object} [options.hiddenStyles] + * @param {Object} [options.hiddenStyles={opacity: "0", transform: "scale(0.5)"}] * @param {(Function|Object)} [options.layout] * @param {Boolean} [options.layout.fillGaps=false] * @param {Boolean} [options.layout.horizontal=false] * @param {Boolean} [options.layout.alignRight=false] * @param {Boolean} [options.layout.alignBottom=false] - * @param {Boolean} [options.layout.rounding=true] - * @param {(Boolean|Number)} [options.layoutOnResize=100] + * @param {Boolean} [options.layout.rounding=false] + * @param {(Boolean|Number)} [options.layoutOnResize=150] * @param {Boolean} [options.layoutOnInit=true] * @param {Number} [options.layoutDuration=300] * @param {String} [options.layoutEasing="ease"] * @param {?Object} [options.sortData=null] * @param {Boolean} [options.dragEnabled=false] + * @param {?String} [options.dragHandle=null] * @param {?HtmlElement} [options.dragContainer=null] * @param {?Function} [options.dragStartPredicate] * @param {Number} [options.dragStartPredicate.distance=0] * @param {Number} [options.dragStartPredicate.delay=0] - * @param {(Boolean|String)} [options.dragStartPredicate.handle=false] - * @param {?String} [options.dragAxis] + * @param {String} [options.dragAxis="xy"] * @param {(Boolean|Function)} [options.dragSort=true] * @param {Object} [options.dragSortHeuristics] * @param {Number} [options.dragSortHeuristics.sortInterval=100] @@ -97,16 +101,27 @@ var instantLayout = 'instant'; * @param {(Function|Object)} [options.dragSortPredicate] * @param {Number} [options.dragSortPredicate.threshold=50] * @param {String} [options.dragSortPredicate.action="move"] - * @param {Number} [options.dragReleaseDuration=300] - * @param {String} [options.dragReleaseEasing="ease"] + * @param {String} [options.dragSortPredicate.migrateAction="move"] + * @param {Object} [options.dragRelease] + * @param {Number} [options.dragRelease.duration=300] + * @param {String} [options.dragRelease.easing="ease"] + * @param {Boolean} [options.dragRelease.useDragContainer=true] * @param {Object} [options.dragCssProps] * @param {Object} [options.dragPlaceholder] * @param {Boolean} [options.dragPlaceholder.enabled=false] - * @param {Number} [options.dragPlaceholder.duration=300] - * @param {String} [options.dragPlaceholder.easing="ease"] * @param {?Function} [options.dragPlaceholder.createElement=null] * @param {?Function} [options.dragPlaceholder.onCreate=null] * @param {?Function} [options.dragPlaceholder.onRemove=null] + * @param {Object} [options.dragAutoScroll] + * @param {(Function|Array)} [options.dragAutoScroll.targets=[]] + * @param {?Function} [options.dragAutoScroll.handle=null] + * @param {Number} [options.dragAutoScroll.threshold=50] + * @param {Number} [options.dragAutoScroll.safeZone=0.2] + * @param {(Function|Number)} [options.dragAutoScroll.speed] + * @param {Boolean} [options.dragAutoScroll.sortDuringScroll=true] + * @param {Boolean} [options.dragAutoScroll.smoothStop=false] + * @param {?Function} [options.dragAutoScroll.onStart=null] + * @param {?Function} [options.dragAutoScroll.onStop=null] * @param {String} [options.containerClass="muuri"] * @param {String} [options.itemClass="muuri-item"] * @param {String} [options.itemVisibleClass="muuri-item-visible"] @@ -116,87 +131,56 @@ var instantLayout = 'instant'; * @param {String} [options.itemReleasingClass="muuri-item-releasing"] * @param {String} [options.itemPlaceholderClass="muuri-item-placeholder"] */ - function Grid(element, options) { - var inst = this; - var settings; - var items; - var layoutOnResize; - - // Allow passing element as selector string. Store element for instance. - element = this._element = - typeof element === stringType ? window.document.querySelector(element) : element; + // Allow passing element as selector string + if (typeof element === STRING_TYPE) { + element = document.querySelector(element); + } // Throw an error if the container element is not body element or does not // exist within the body element. var isElementInDom = element.getRootNode ? element.getRootNode({ composed: true }) === document - : window.document.body.contains(element); - if (!isElementInDom || element === window.document.documentElement) { - throw new Error('Container element must be an existing DOM element'); + : document.body.contains(element); + if (!isElementInDom || element === document.documentElement) { + throw new Error('Container element must be an existing DOM element.'); } // Create instance settings by merging the options with default options. - settings = this._settings = mergeSettings(Grid.defaultOptions, options); - - // Sanitize dragSort setting. + var settings = mergeSettings(Grid.defaultOptions, options); + settings.visibleStyles = normalizeStyles(settings.visibleStyles); + settings.hiddenStyles = normalizeStyles(settings.hiddenStyles); if (!isFunction(settings.dragSort)) { settings.dragSort = !!settings.dragSort; } - // Create instance id and store it to the grid instances collection. this._id = createUid(); - gridInstances[this._id] = inst; - - // Destroyed flag. + this._element = element; + this._settings = settings; this._isDestroyed = false; - - // The layout object (mutated on every layout). + this._items = []; this._layout = { id: 0, items: [], slots: [], - setWidth: false, - setHeight: false, - width: 0, - height: 0 }; - - // Create private Emitter instance. + this._isLayoutFinished = true; + this._nextLayoutData = null; this._emitter = new Emitter(); + this._onLayoutDataReceived = this._onLayoutDataReceived.bind(this); + + // Store grid instance to the grid instances collection. + GRID_INSTANCES[this._id] = this; // Add container element's class name. addClass(element, settings.containerClass); - // Create initial items. - this._items = []; - items = settings.items; - if (typeof items === stringType) { - toArray(element.children).forEach(function(itemElement) { - if (items === '*' || elementMatches(itemElement, items)) { - inst._items.push(new Item(inst, itemElement)); - } - }); - } else if (Array.isArray(items) || isNodeList(items)) { - this._items = toArray(items).map(function(itemElement) { - return new Item(inst, itemElement); - }); - } - // If layoutOnResize option is a valid number sanitize it and bind the resize // handler. - layoutOnResize = settings.layoutOnResize; - if (typeof layoutOnResize !== numberType) { - layoutOnResize = layoutOnResize === true ? 0 : -1; - } - if (layoutOnResize >= 0) { - window.addEventListener( - 'resize', - (inst._resizeHandler = debounce(function() { - inst.refreshItems().layout(); - }, layoutOnResize)) - ); - } + bindLayoutOnResize(this, settings.layoutOnResize); + + // Add initial items. + this.add(getInitialGridElements(element, settings.items), { layout: false }); // Layout on init if necessary. if (settings.layoutOnInit) { @@ -210,68 +194,107 @@ function Grid(element, options) { */ /** + * @public + * @static * @see Item */ Grid.Item = Item; /** + * @public + * @static * @see ItemLayout */ Grid.ItemLayout = ItemLayout; /** + * @public + * @static * @see ItemVisibility */ Grid.ItemVisibility = ItemVisibility; /** + * @public + * @static * @see ItemMigrate */ Grid.ItemMigrate = ItemMigrate; /** - * @see ItemAnimate - */ -Grid.ItemAnimate = ItemAnimate; - -/** + * @public + * @static * @see ItemDrag */ Grid.ItemDrag = ItemDrag; /** - * @see ItemRelease + * @public + * @static + * @see ItemDragRelease */ -Grid.ItemRelease = ItemRelease; +Grid.ItemDragRelease = ItemDragRelease; /** + * @public + * @static * @see ItemDragPlaceholder */ Grid.ItemDragPlaceholder = ItemDragPlaceholder; /** + * @public + * @static * @see Emitter */ Grid.Emitter = Emitter; /** + * @public + * @static + * @see Animator + */ +Grid.Animator = Animator; + +/** + * @public + * @static * @see Dragger */ Grid.Dragger = Dragger; /** + * @public + * @static * @see Packer */ Grid.Packer = Packer; +/** + * @public + * @static + * @see AutoScroller + */ +Grid.AutoScroller = AutoScroller; + +/** + * The default Packer instance used by default for all layouts. + * + * @public + * @static + * @type {Packer} + */ +Grid.defaultPacker = new Packer(2); + /** * Default options for Grid instance. * * @public - * @memberof Grid + * @static + * @type {Object} */ Grid.defaultOptions = { - // Item elements + // Initial item elements items: '*', // Default show animation @@ -285,11 +308,11 @@ Grid.defaultOptions = { // Item's visible/hidden state styles visibleStyles: { opacity: '1', - transform: 'scale(1)' + transform: 'scale(1)', }, hiddenStyles: { opacity: '0', - transform: 'scale(0.5)' + transform: 'scale(0.5)', }, // Layout @@ -298,9 +321,9 @@ Grid.defaultOptions = { horizontal: false, alignRight: false, alignBottom: false, - rounding: true + rounding: false, }, - layoutOnResize: 100, + layoutOnResize: 150, layoutOnInit: true, layoutDuration: 300, layoutEasing: 'ease', @@ -311,39 +334,52 @@ Grid.defaultOptions = { // Drag & Drop dragEnabled: false, dragContainer: null, + dragHandle: null, dragStartPredicate: { distance: 0, delay: 0, - handle: false }, - dragAxis: null, + dragAxis: 'xy', dragSort: true, dragSortHeuristics: { sortInterval: 100, minDragDistance: 10, - minBounceBackAngle: 1 + minBounceBackAngle: 1, }, dragSortPredicate: { threshold: 50, - action: actionMove + action: ACTION_MOVE, + migrateAction: ACTION_MOVE, + }, + dragRelease: { + duration: 300, + easing: 'ease', + useDragContainer: true, }, - dragReleaseDuration: 300, - dragReleaseEasing: 'ease', dragCssProps: { touchAction: 'none', userSelect: 'none', userDrag: 'none', tapHighlightColor: 'rgba(0, 0, 0, 0)', touchCallout: 'none', - contentZooming: 'none' + contentZooming: 'none', }, dragPlaceholder: { enabled: false, - duration: 300, - easing: 'ease', createElement: null, onCreate: null, - onRemove: null + onRemove: null, + }, + dragAutoScroll: { + targets: [], + handle: null, + threshold: 50, + safeZone: 0.2, + speed: AutoScroller.smoothSpeed(1000, 2000, 2500), + sortDuringScroll: true, + smoothStop: false, + onStart: null, + onStop: null, }, // Classnames @@ -354,7 +390,7 @@ Grid.defaultOptions = { itemPositioningClass: 'muuri-item-positioning', itemDraggingClass: 'muuri-item-dragging', itemReleasingClass: 'muuri-item-releasing', - itemPlaceholderClass: 'muuri-item-placeholder' + itemPlaceholderClass: 'muuri-item-placeholder', }; /** @@ -366,12 +402,11 @@ Grid.defaultOptions = { * Bind an event listener. * * @public - * @memberof Grid.prototype * @param {String} event * @param {Function} listener * @returns {Grid} */ -Grid.prototype.on = function(event, listener) { +Grid.prototype.on = function (event, listener) { this._emitter.on(event, listener); return this; }; @@ -380,12 +415,11 @@ Grid.prototype.on = function(event, listener) { * Unbind an event listener. * * @public - * @memberof Grid.prototype * @param {String} event * @param {Function} listener * @returns {Grid} */ -Grid.prototype.off = function(event, listener) { +Grid.prototype.off = function (event, listener) { this._emitter.off(event, listener); return this; }; @@ -394,82 +428,155 @@ Grid.prototype.off = function(event, listener) { * Get the container element. * * @public - * @memberof Grid.prototype * @returns {HTMLElement} */ -Grid.prototype.getElement = function() { +Grid.prototype.getElement = function () { return this._element; }; /** - * Get all items. Optionally you can provide specific targets (elements and - * indices). Note that the returned array is not the same object used by the - * instance so modifying it will not affect instance's items. All items that - * are not found are omitted from the returned array. + * Get instance's item by element or by index. Target can also be an Item + * instance in which case the function returns the item if it exists within + * related Grid instance. If nothing is found with the provided target, null + * is returned. + * + * @private + * @param {(HtmlElement|Number|Item)} [target] + * @returns {?Item} + */ +Grid.prototype.getItem = function (target) { + // If no target is specified or the instance is destroyed, return null. + if (this._isDestroyed || (!target && target !== 0)) { + return null; + } + + // If target is number return the item in that index. If the number is lower + // than zero look for the item starting from the end of the items array. For + // example -1 for the last item, -2 for the second last item, etc. + if (typeof target === NUMBER_TYPE) { + return this._items[target > -1 ? target : this._items.length + target] || null; + } + + // If the target is an instance of Item return it if it is attached to this + // Grid instance, otherwise return null. + if (target instanceof Item) { + return target._gridId === this._id ? target : null; + } + + // In other cases let's assume that the target is an element, so let's try + // to find an item that matches the element and return it. If item is not + // found return null. + if (ITEM_ELEMENT_MAP) { + var item = ITEM_ELEMENT_MAP.get(target); + return item && item._gridId === this._id ? item : null; + } else { + for (var i = 0; i < this._items.length; i++) { + if (this._items[i]._element === target) { + return this._items[i]; + } + } + } + + return null; +}; + +/** + * Get all items. Optionally you can provide specific targets (elements, + * indices and item instances). All items that are not found are omitted from + * the returned array. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} [targets] + * @param {(HtmlElement|Number|Item|Array)} [targets] * @returns {Item[]} */ -Grid.prototype.getItems = function(targets) { +Grid.prototype.getItems = function (targets) { // Return all items immediately if no targets were provided or if the // instance is destroyed. - if (this._isDestroyed || (!targets && targets !== 0)) { + if (this._isDestroyed || targets === undefined) { return this._items.slice(0); } - var ret = []; - var targetItems = toArray(targets); - var item; - var i; + var items = []; + var i, item; - // If target items are defined return filtered results. - for (i = 0; i < targetItems.length; i++) { - item = this._getItem(targetItems[i]); - item && ret.push(item); + if (Array.isArray(targets) || isNodeList(targets)) { + for (i = 0; i < targets.length; i++) { + item = this.getItem(targets[i]); + if (item) items.push(item); + } + } else { + item = this.getItem(targets); + if (item) items.push(item); } - return ret; + return items; }; /** - * Update the cached dimensions of the instance's items. + * Update the cached dimensions of the instance's items. By default all the + * items are refreshed, but you can also provide an array of target items as the + * first argument if you want to refresh specific items. Note that all hidden + * items are not refreshed by default since their "display" property is "none" + * and their dimensions are therefore not readable from the DOM. However, if you + * do want to force update hidden item dimensions too you can provide `true` + * as the second argument, which makes the elements temporarily visible while + * their dimensions are being read. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} [items] + * @param {Item[]} [items] + * @param {Boolean} [force=false] * @returns {Grid} */ -Grid.prototype.refreshItems = function(items) { +Grid.prototype.refreshItems = function (items, force) { if (this._isDestroyed) return this; - var targets = this.getItems(items); - var i; + var targets = items || this._items; + var i, item, style, hiddenItemStyles; + + if (force === true) { + hiddenItemStyles = []; + for (i = 0; i < targets.length; i++) { + item = targets[i]; + if (!item.isVisible() && !item.isHiding()) { + style = item.getElement().style; + style.visibility = 'hidden'; + style.display = ''; + hiddenItemStyles.push(style); + } + } + } for (i = 0; i < targets.length; i++) { - targets[i]._refreshDimensions(); + targets[i]._refreshDimensions(force); + } + + if (force === true) { + for (i = 0; i < hiddenItemStyles.length; i++) { + style = hiddenItemStyles[i]; + style.visibility = ''; + style.display = 'none'; + } + hiddenItemStyles.length = 0; } return this; }; /** - * Update the sort data of the instance's items. + * Update the sort data of the instance's items. By default all the items are + * refreshed, but you can also provide an array of target items if you want to + * refresh specific items. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} [items] + * @param {Item[]} [items] * @returns {Grid} */ -Grid.prototype.refreshSortData = function(items) { +Grid.prototype.refreshSortData = function (items) { if (this._isDestroyed) return this; - var targetItems = this.getItems(items); - var i; - - for (i = 0; i < targetItems.length; i++) { - targetItems[i]._refreshSortData(); + var targets = items || this._items; + for (var i = 0; i < targets.length; i++) { + targets[i]._refreshSortData(); } return this; @@ -483,33 +590,29 @@ Grid.prototype.refreshSortData = function(items) { * left untouched. * * @public - * @memberof Grid.prototype * @returns {Grid} */ -Grid.prototype.synchronize = function() { +Grid.prototype.synchronize = function () { if (this._isDestroyed) return this; - var container = this._element; var items = this._items; + if (!items.length) return this; + var fragment; var element; - var i; - // Append all elements in order to the container element. - if (items.length) { - for (i = 0; i < items.length; i++) { - element = items[i]._element; - if (element.parentNode === container) { - fragment = fragment || window.document.createDocumentFragment(); - fragment.appendChild(element); - } + for (var i = 0; i < items.length; i++) { + element = items[i]._element; + if (element.parentNode === this._element) { + fragment = fragment || document.createDocumentFragment(); + fragment.appendChild(element); } - - if (fragment) container.appendChild(fragment); } - // Emit synchronize event. - this._emit(eventSynchronize); + if (!fragment) return this; + + this._element.appendChild(fragment); + this._emit(EVENT_SYNCHRONIZE); return this; }; @@ -518,97 +621,72 @@ Grid.prototype.synchronize = function() { * Calculate and apply item positions. * * @public - * @memberof Grid.prototype * @param {Boolean} [instant=false] - * @param {LayoutCallback} [onFinish] + * @param {Function} [onFinish] * @returns {Grid} */ -Grid.prototype.layout = function(instant, onFinish) { +Grid.prototype.layout = function (instant, onFinish) { if (this._isDestroyed) return this; - var inst = this; - var element = this._element; - var layout = this._updateLayout(); - var layoutId = layout.id; - var itemsLength = layout.items.length; - var counter = itemsLength; - var isBorderBox; - var item; - var i; + // Cancel unfinished layout algorithm if possible. + var unfinishedLayout = this._nextLayoutData; + if (unfinishedLayout && isFunction(unfinishedLayout.cancel)) { + unfinishedLayout.cancel(); + } - // The finish function, which will be used for checking if all the items - // have laid out yet. After all items have finished their animations call - // callback and emit layoutEnd event. Only emit layoutEnd event if there - // hasn't been a new layout call during this layout. - function tryFinish() { - if (--counter > 0) return; + // Compute layout id (let's stay in Float32 range). + layoutId = (layoutId % MAX_SAFE_FLOAT32_INTEGER) + 1; + var nextLayoutId = layoutId; - var hasLayoutChanged = inst._layout.id !== layoutId; - var callback = isFunction(instant) ? instant : onFinish; + // Store data for next layout. + this._nextLayoutData = { + id: nextLayoutId, + instant: instant, + onFinish: onFinish, + cancel: null, + }; - if (isFunction(callback)) { - callback(hasLayoutChanged, layout.items.slice(0)); - } + // Collect layout items (all active grid items). + var items = this._items; + var layoutItems = []; + for (var i = 0; i < items.length; i++) { + if (items[i]._isActive) layoutItems.push(items[i]); + } - if (!hasLayoutChanged && inst._hasListeners(eventLayoutEnd)) { - inst._emit(eventLayoutEnd, layout.items.slice(0)); - } + // Compute new layout. + this._refreshDimensions(); + var gridWidth = this._width - this._borderLeft - this._borderRight; + var gridHeight = this._height - this._borderTop - this._borderBottom; + var layoutSettings = this._settings.layout; + var cancelLayout; + if (isFunction(layoutSettings)) { + cancelLayout = layoutSettings( + this, + nextLayoutId, + layoutItems, + gridWidth, + gridHeight, + this._onLayoutDataReceived + ); + } else { + Grid.defaultPacker.setOptions(layoutSettings); + cancelLayout = Grid.defaultPacker.createLayout( + this, + nextLayoutId, + layoutItems, + gridWidth, + gridHeight, + this._onLayoutDataReceived + ); } - // If grid's width or height was modified, we need to update it's cached - // dimensions. Also keep in mind that grid's cached width/height should - // always equal to what elem.getBoundingClientRect() would return, so - // therefore we need to add the grid element's borders to the dimensions if - // it's box-sizing is border-box. Note that we support providing the - // dimensions as a string here too so that one can define the unit of the - // dimensions, in which case we don't do the border-box check. + // Store layout cancel method if available. if ( - (layout.setHeight && typeof layout.height === numberType) || - (layout.setWidth && typeof layout.width === numberType) + isFunction(cancelLayout) && + this._nextLayoutData && + this._nextLayoutData.id === nextLayoutId ) { - isBorderBox = getStyle(element, 'box-sizing') === 'border-box'; - } - if (layout.setHeight) { - if (typeof layout.height === numberType) { - element.style.height = - (isBorderBox ? layout.height + this._borderTop + this._borderBottom : layout.height) + 'px'; - } else { - element.style.height = layout.height; - } - } - if (layout.setWidth) { - if (typeof layout.width === numberType) { - element.style.width = - (isBorderBox ? layout.width + this._borderLeft + this._borderRight : layout.width) + 'px'; - } else { - element.style.width = layout.width; - } - } - - // Emit layoutStart event. Note that this is intentionally emitted after the - // container element's dimensions are set, because otherwise there would be - // no hook for reacting to container dimension changes. - if (this._hasListeners(eventLayoutStart)) { - this._emit(eventLayoutStart, layout.items.slice(0)); - } - - // If there are no items let's finish quickly. - if (!itemsLength) { - tryFinish(); - return this; - } - - // If there are items let's position them. - for (i = 0; i < itemsLength; i++) { - item = layout.items[i]; - if (!item) continue; - - // Update item's position. - item._left = layout.slots[i * 2]; - item._top = layout.slots[i * 2 + 1]; - - // Layout item if it is not dragged. - item.isDragging() ? tryFinish() : item._layout.start(instant === true, tryFinish); + this._nextLayoutData.cancel = cancelLayout; } return this; @@ -628,31 +706,49 @@ Grid.prototype.layout = function(instant, onFinish) { * are positioned without animation during their first layout. * * @public - * @memberof Grid.prototype * @param {(HTMLElement|HTMLElement[])} elements * @param {Object} [options] * @param {Number} [options.index=-1] - * @param {Boolean} [options.isActive] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {Boolean} [options.active] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Item[]} */ -Grid.prototype.add = function(elements, options) { +Grid.prototype.add = function (elements, options) { if (this._isDestroyed || !elements) return []; var newItems = toArray(elements); if (!newItems.length) return newItems; - var opts = options || 0; + var opts = options || {}; var layout = opts.layout ? opts.layout : opts.layout === undefined; var items = this._items; var needsLayout = false; + var fragment; + var element; var item; var i; + // Collect all the elements that are not child of the grid element into a + // document fragment. + for (i = 0; i < newItems.length; i++) { + element = newItems[i]; + if (element.parentNode !== this._element) { + fragment = fragment || document.createDocumentFragment(); + fragment.appendChild(element); + } + } + + // If we have a fragment, let's append it to the grid element. We could just + // not do this and the `new Item()` instantiation would handle this for us, + // but this way we can add the elements into the DOM a bit faster. + if (fragment) { + this._element.appendChild(fragment); + } + // Map provided elements into new grid items. for (i = 0; i < newItems.length; i++) { - item = new Item(this, newItems[i], opts.isActive); - newItems[i] = item; + element = newItems[i]; + item = newItems[i] = new Item(this, element, opts.active); // If the item to be added is active, we need to do a layout. Also, we // need to mark the item with the skipNextAnimation flag to make it @@ -665,17 +761,25 @@ Grid.prototype.add = function(elements, options) { } } + // Set up the items' initial dimensions and sort data. This needs to be done + // in a separate loop to avoid layout thrashing. + for (i = 0; i < newItems.length; i++) { + item = newItems[i]; + item._refreshDimensions(); + item._refreshSortData(); + } + // Add the new items to the items collection to correct index. arrayInsert(items, newItems, opts.index); // Emit add event. - if (this._hasListeners(eventAdd)) { - this._emit(eventAdd, newItems.slice(0)); + if (this._hasListeners(EVENT_ADD)) { + this._emit(EVENT_ADD, newItems.slice(0)); } // If layout is needed. if (needsLayout && layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } return newItems; @@ -685,79 +789,89 @@ Grid.prototype.add = function(elements, options) { * Remove items from the instance. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} items + * @param {Item[]} items * @param {Object} [options] * @param {Boolean} [options.removeElements=false] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Item[]} */ -Grid.prototype.remove = function(items, options) { - if (this._isDestroyed) return this; +Grid.prototype.remove = function (items, options) { + if (this._isDestroyed || !items.length) return []; - var opts = options || 0; + var opts = options || {}; var layout = opts.layout ? opts.layout : opts.layout === undefined; var needsLayout = false; var allItems = this.getItems(); - var targetItems = this.getItems(items); + var targetItems = []; var indices = []; + var index; var item; var i; // Remove the individual items. - for (i = 0; i < targetItems.length; i++) { - item = targetItems[i]; - indices.push(allItems.indexOf(item)); + for (i = 0; i < items.length; i++) { + item = items[i]; + if (item._isDestroyed) continue; + + index = this._items.indexOf(item); + if (index === -1) continue; + if (item._isActive) needsLayout = true; + + targetItems.push(item); + indices.push(allItems.indexOf(item)); item._destroy(opts.removeElements); + this._items.splice(index, 1); } // Emit remove event. - if (this._hasListeners(eventRemove)) { - this._emit(eventRemove, targetItems.slice(0), indices); + if (this._hasListeners(EVENT_REMOVE)) { + this._emit(EVENT_REMOVE, targetItems.slice(0), indices); } // If layout is needed. if (needsLayout && layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } return targetItems; }; /** - * Show instance items. + * Show specific instance items. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} items + * @param {Item[]} items * @param {Object} [options] * @param {Boolean} [options.instant=false] - * @param {ShowCallback} [options.onFinish] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {Boolean} [options.syncWithLayout=true] + * @param {Function} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ -Grid.prototype.show = function(items, options) { - if (this._isDestroyed) return this; - this._setItemsVisibility(items, true, options); +Grid.prototype.show = function (items, options) { + if (!this._isDestroyed && items.length) { + this._setItemsVisibility(items, true, options); + } return this; }; /** - * Hide instance items. + * Hide specific instance items. * * @public - * @memberof Grid.prototype - * @param {GridMultiItemQuery} items + * @param {Item[]} items * @param {Object} [options] * @param {Boolean} [options.instant=false] - * @param {HideCallback} [options.onFinish] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {Boolean} [options.syncWithLayout=true] + * @param {Function} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ -Grid.prototype.hide = function(items, options) { - if (this._isDestroyed) return this; - this._setItemsVisibility(items, false, options); +Grid.prototype.hide = function (items, options) { + if (!this._isDestroyed && items.length) { + this._setItemsVisibility(items, false, options); + } return this; }; @@ -772,23 +886,24 @@ Grid.prototype.hide = function(items, options) { * matching items will be shown and others hidden. * * @public - * @memberof Grid.prototype * @param {(Function|String)} predicate * @param {Object} [options] * @param {Boolean} [options.instant=false] + * @param {Boolean} [options.syncWithLayout=true] * @param {FilterCallback} [options.onFinish] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ -Grid.prototype.filter = function(predicate, options) { +Grid.prototype.filter = function (predicate, options) { if (this._isDestroyed || !this._items.length) return this; var itemsToShow = []; var itemsToHide = []; - var isPredicateString = typeof predicate === stringType; + var isPredicateString = typeof predicate === STRING_TYPE; var isPredicateFn = isFunction(predicate); - var opts = options || 0; + var opts = options || {}; var isInstant = opts.instant === true; + var syncWithLayout = opts.syncWithLayout; var layout = opts.layout ? opts.layout : opts.layout === undefined; var onFinish = isFunction(opts.onFinish) ? opts.onFinish : null; var tryFinishCounter = -1; @@ -798,7 +913,7 @@ Grid.prototype.filter = function(predicate, options) { // If we have onFinish callback, let's create proper tryFinish callback. if (onFinish) { - tryFinish = function() { + tryFinish = function () { ++tryFinishCounter && onFinish(itemsToShow.slice(0), itemsToHide.slice(0)); }; } @@ -819,8 +934,9 @@ Grid.prototype.filter = function(predicate, options) { if (itemsToShow.length) { this.show(itemsToShow, { instant: isInstant, + syncWithLayout: syncWithLayout, onFinish: tryFinish, - layout: false + layout: false, }); } else { tryFinish(); @@ -830,8 +946,9 @@ Grid.prototype.filter = function(predicate, options) { if (itemsToHide.length) { this.hide(itemsToHide, { instant: isInstant, + syncWithLayout: syncWithLayout, onFinish: tryFinish, - layout: false + layout: false, }); } else { tryFinish(); @@ -840,13 +957,13 @@ Grid.prototype.filter = function(predicate, options) { // If there are any items to filter. if (itemsToShow.length || itemsToHide.length) { // Emit filter event. - if (this._hasListeners(eventFilter)) { - this._emit(eventFilter, itemsToShow.slice(0), itemsToHide.slice(0)); + if (this._hasListeners(EVENT_FILTER)) { + this._emit(EVENT_FILTER, itemsToShow.slice(0), itemsToHide.slice(0)); } // If layout is needed. if (layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } } @@ -864,42 +981,18 @@ Grid.prototype.filter = function(predicate, options) { * same order. * * @public - * @memberof Grid.prototype - * @param {(Function|Item[]|String|String[])} comparer + * @param {(Function|String|Item[])} comparer * @param {Object} [options] * @param {Boolean} [options.descending=false] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ -Grid.prototype.sort = (function() { +Grid.prototype.sort = (function () { var sortComparer; var isDescending; var origItems; var indexMap; - function parseCriteria(data) { - return data - .trim() - .split(' ') - .map(function(val) { - return val.split(':'); - }); - } - - function getIndexMap(items) { - var ret = {}; - for (var i = 0; i < items.length; i++) { - ret[items[i]._id] = i; - } - return ret; - } - - function compareIndices(itemA, itemB) { - var indexA = indexMap[itemA._id]; - var indexB = indexMap[itemB._id]; - return isDescending ? indexB - indexA : indexA - indexB; - } - function defaultComparer(a, b) { var result = 0; var criteriaName; @@ -931,80 +1024,83 @@ Grid.prototype.sort = (function() { } // If values are equal let's compare the item indices to make sure we - // have a stable sort. + // have a stable sort. Note that this is not necessary in evergreen browsers + // because Array.sort() is nowadays stable. However, in order to guarantee + // same results in older browsers we need this. if (!result) { - if (!indexMap) indexMap = getIndexMap(origItems); - result = compareIndices(a, b); + if (!indexMap) indexMap = createIndexMap(origItems); + result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b); } return result; } function customComparer(a, b) { - var result = sortComparer(a, b); - // If descending let's invert the result value. - if (isDescending && result) result = -result; - // If we have a valid result (not zero) let's return it right away. - if (result) return result; - // If result is zero let's compare the item indices to make sure we have a - // stable sort. - if (!indexMap) indexMap = getIndexMap(origItems); - return compareIndices(a, b); + var result = isDescending ? -sortComparer(a, b) : sortComparer(a, b); + if (!result) { + if (!indexMap) indexMap = createIndexMap(origItems); + result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b); + } + return result; } - return function(comparer, options) { + return function (comparer, options) { if (this._isDestroyed || this._items.length < 2) return this; var items = this._items; - var opts = options || 0; + var opts = options || {}; var layout = opts.layout ? opts.layout : opts.layout === undefined; - var i; // Setup parent scope data. - sortComparer = comparer; isDescending = !!opts.descending; origItems = items.slice(0); indexMap = null; // If function is provided do a native array sort. - if (isFunction(sortComparer)) { + if (isFunction(comparer)) { + sortComparer = comparer; items.sort(customComparer); } // Otherwise if we got a string, let's sort by the sort data as provided in // the instance's options. - else if (typeof sortComparer === stringType) { - sortComparer = parseCriteria(comparer); + else if (typeof comparer === STRING_TYPE) { + sortComparer = comparer + .trim() + .split(' ') + .filter(function (val) { + return val; + }) + .map(function (val) { + return val.split(':'); + }); items.sort(defaultComparer); } // Otherwise if we got an array, let's assume it's a presorted array of the - // items and order the items based on it. - else if (Array.isArray(sortComparer)) { - if (sortComparer.length !== items.length) { - throw new Error('[' + namespace + '] sort reference items do not match with grid items.'); - } - for (i = 0; i < items.length; i++) { - if (sortComparer.indexOf(items[i]) < 0) { - throw new Error('[' + namespace + '] sort reference items do not match with grid items.'); - } - items[i] = sortComparer[i]; - } - if (isDescending) items.reverse(); + // items and order the items based on it. Here we blindly trust that the + // presorted array consists of the same item instances as the current + // `gird._items` array. + else if (Array.isArray(comparer)) { + items.length = 0; + items.push.apply(items, comparer); } - // Otherwise let's just skip it, nothing we can do here. + // Otherwise let's throw an error. else { - /** @todo Maybe throw an error here? */ - return this; + sortComparer = isDescending = origItems = indexMap = null; + throw new Error('Invalid comparer argument provided.'); } // Emit sort event. - if (this._hasListeners(eventSort)) { - this._emit(eventSort, items.slice(0), origItems); + if (this._hasListeners(EVENT_SORT)) { + this._emit(EVENT_SORT, items.slice(0), origItems); } // If layout is needed. if (layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } + // Reset data (to avoid mem leaks). + sortComparer = isDescending = origItems = indexMap = null; + return this; }; })(); @@ -1013,27 +1109,26 @@ Grid.prototype.sort = (function() { * Move item to another index or in place of another item. * * @public - * @memberof Grid.prototype - * @param {GridSingleItemQuery} item - * @param {GridSingleItemQuery} position + * @param {(HtmlElement|Number|Item)} item + * @param {(HtmlElement|Number|Item)} position * @param {Object} [options] * @param {String} [options.action="move"] * - Accepts either "move" or "swap". * - "move" moves the item in place of the other item. * - "swap" swaps the position of the items. - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {(Boolean|Function|String)} [options.layout=true] * @returns {Grid} */ -Grid.prototype.move = function(item, position, options) { +Grid.prototype.move = function (item, position, options) { if (this._isDestroyed || this._items.length < 2) return this; var items = this._items; - var opts = options || 0; + var opts = options || {}; var layout = opts.layout ? opts.layout : opts.layout === undefined; - var isSwap = opts.action === actionSwap; - var action = isSwap ? actionSwap : actionMove; - var fromItem = this._getItem(item); - var toItem = this._getItem(position); + var isSwap = opts.action === ACTION_SWAP; + var action = isSwap ? ACTION_SWAP : ACTION_MOVE; + var fromItem = this.getItem(item); + var toItem = this.getItem(position); var fromIndex; var toIndex; @@ -1051,18 +1146,18 @@ Grid.prototype.move = function(item, position, options) { } // Emit move event. - if (this._hasListeners(eventMove)) { - this._emit(eventMove, { + if (this._hasListeners(EVENT_MOVE)) { + this._emit(EVENT_MOVE, { item: fromItem, fromIndex: fromIndex, toIndex: toIndex, - action: action + action: action, }); } // If layout is needed. if (layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } } @@ -1073,45 +1168,44 @@ Grid.prototype.move = function(item, position, options) { * Send item to another Grid instance. * * @public - * @memberof Grid.prototype - * @param {GridSingleItemQuery} item - * @param {Grid} grid - * @param {GridSingleItemQuery} position + * @param {(HtmlElement|Number|Item)} item + * @param {Grid} targetGrid + * @param {(HtmlElement|Number|Item)} position * @param {Object} [options] * @param {HTMLElement} [options.appendTo=document.body] - * @param {(Boolean|LayoutCallback|String)} [options.layoutSender=true] - * @param {(Boolean|LayoutCallback|String)} [options.layoutReceiver=true] + * @param {(Boolean|Function|String)} [options.layoutSender=true] + * @param {(Boolean|Function|String)} [options.layoutReceiver=true] * @returns {Grid} */ -Grid.prototype.send = function(item, grid, position, options) { - if (this._isDestroyed || grid._isDestroyed || this === grid) return this; +Grid.prototype.send = function (item, targetGrid, position, options) { + if (this._isDestroyed || targetGrid._isDestroyed || this === targetGrid) return this; // Make sure we have a valid target item. - item = this._getItem(item); + item = this.getItem(item); if (!item) return this; - var opts = options || 0; - var container = opts.appendTo || window.document.body; + var opts = options || {}; + var container = opts.appendTo || document.body; var layoutSender = opts.layoutSender ? opts.layoutSender : opts.layoutSender === undefined; var layoutReceiver = opts.layoutReceiver ? opts.layoutReceiver : opts.layoutReceiver === undefined; // Start the migration process. - item._migrate.start(grid, position, container); + item._migrate.start(targetGrid, position, container); // If migration was started successfully and the item is active, let's layout // the grids. if (item._migrate._isActive && item._isActive) { if (layoutSender) { this.layout( - layoutSender === instantLayout, + layoutSender === INSTANT_LAYOUT, isFunction(layoutSender) ? layoutSender : undefined ); } if (layoutReceiver) { - grid.layout( - layoutReceiver === instantLayout, + targetGrid.layout( + layoutReceiver === INSTANT_LAYOUT, isFunction(layoutReceiver) ? layoutReceiver : undefined ); } @@ -1124,38 +1218,34 @@ Grid.prototype.send = function(item, grid, position, options) { * Destroy the instance. * * @public - * @memberof Grid.prototype * @param {Boolean} [removeElements=false] * @returns {Grid} */ -Grid.prototype.destroy = function(removeElements) { +Grid.prototype.destroy = function (removeElements) { if (this._isDestroyed) return this; var container = this._element; var items = this._items.slice(0); - var i; + var layoutStyles = (this._layout && this._layout.styles) || {}; + var i, prop; // Unbind window resize event listener. - if (this._resizeHandler) { - window.removeEventListener('resize', this._resizeHandler); - } + unbindLayoutOnResize(this); // Destroy items. - for (i = 0; i < items.length; i++) { - items[i]._destroy(removeElements); - } + for (i = 0; i < items.length; i++) items[i]._destroy(removeElements); + this._items.length = 0; // Restore container. removeClass(container, this._settings.containerClass); - container.style.height = ''; - container.style.width = ''; + for (prop in layoutStyles) container.style[prop] = ''; // Emit destroy event and unbind all events. - this._emit(eventDestroy); + this._emit(EVENT_DESTROY); this._emitter.destroy(); // Remove reference from the grid instances collection. - gridInstances[this._id] = undefined; + delete GRID_INSTANCES[this._id]; // Flag instance as destroyed. this._isDestroyed = true; @@ -1168,106 +1258,14 @@ Grid.prototype.destroy = function(removeElements) { * ************************* */ -/** - * Get instance's item by element or by index. Target can also be an Item - * instance in which case the function returns the item if it exists within - * related Grid instance. If nothing is found with the provided target, null - * is returned. - * - * @private - * @memberof Grid.prototype - * @param {GridSingleItemQuery} [target] - * @returns {?Item} - */ -Grid.prototype._getItem = function(target) { - // If no target is specified or the instance is destroyed, return null. - if (this._isDestroyed || (!target && target !== 0)) { - return null; - } - - // If target is number return the item in that index. If the number is lower - // than zero look for the item starting from the end of the items array. For - // example -1 for the last item, -2 for the second last item, etc. - if (typeof target === numberType) { - return this._items[target > -1 ? target : this._items.length + target] || null; - } - - // If the target is an instance of Item return it if it is attached to this - // Grid instance, otherwise return null. - if (target instanceof Item) { - return target._gridId === this._id ? target : null; - } - - // In other cases let's assume that the target is an element, so let's try - // to find an item that matches the element and return it. If item is not - // found return null. - /** @todo This could be made a lot faster by using Map/WeakMap of elements. */ - for (var i = 0; i < this._items.length; i++) { - if (this._items[i]._element === target) { - return this._items[i]; - } - } - - return null; -}; - -/** - * Recalculates and updates instance's layout data. - * - * @private - * @memberof Grid.prototype - * @returns {LayoutData} - */ -Grid.prototype._updateLayout = function() { - var layout = this._layout; - var settings = this._settings.layout; - var width; - var height; - var newLayout; - var i; - - // Let's increment layout id. - ++layout.id; - - // Let's update layout items - layout.items.length = 0; - for (i = 0; i < this._items.length; i++) { - if (this._items[i]._isActive) layout.items.push(this._items[i]); - } - - // Let's make sure we have the correct container dimensions. - this._refreshDimensions(); - - // Calculate container width and height (without borders). - width = this._width - this._borderLeft - this._borderRight; - height = this._height - this._borderTop - this._borderBottom; - - // Calculate new layout. - if (isFunction(settings)) { - newLayout = settings(layout.items, width, height); - } else { - newLayout = packer.getLayout(layout.items, width, height, layout.slots, settings); - } - - // Let's update the grid's layout. - layout.slots = newLayout.slots; - layout.setWidth = Boolean(newLayout.setWidth); - layout.setHeight = Boolean(newLayout.setHeight); - layout.width = newLayout.width; - layout.height = newLayout.height; - - return layout; -}; - /** * Emit a grid event. * * @private - * @memberof Grid.prototype * @param {String} event * @param {...*} [arg] */ -Grid.prototype._emit = function() { +Grid.prototype._emit = function () { if (this._isDestroyed) return; this._emitter.emit.apply(this._emitter, arguments); }; @@ -1276,41 +1274,40 @@ Grid.prototype._emit = function() { * Check if there are any events listeners for an event. * * @private - * @memberof Grid.prototype * @param {String} event * @returns {Boolean} */ -Grid.prototype._hasListeners = function(event) { - var listeners = this._emitter._events[event]; - return !!(listeners && listeners.length); +Grid.prototype._hasListeners = function (event) { + if (this._isDestroyed) return false; + return this._emitter.countListeners(event) > 0; }; /** * Update container's width, height and offsets. * * @private - * @memberof Grid.prototype */ -Grid.prototype._updateBoundingRect = function() { +Grid.prototype._updateBoundingRect = function () { var element = this._element; var rect = element.getBoundingClientRect(); this._width = rect.width; this._height = rect.height; this._left = rect.left; this._top = rect.top; + this._right = rect.right; + this._bottom = rect.bottom; }; /** * Update container's border sizes. * * @private - * @memberof Grid.prototype * @param {Boolean} left * @param {Boolean} right * @param {Boolean} top * @param {Boolean} bottom */ -Grid.prototype._updateBorders = function(left, right, top, bottom) { +Grid.prototype._updateBorders = function (left, right, top, bottom) { var element = this._element; if (left) this._borderLeft = getStyleAsFloat(element, 'border-left-width'); if (right) this._borderRight = getStyleAsFloat(element, 'border-right-width'); @@ -1322,35 +1319,151 @@ Grid.prototype._updateBorders = function(left, right, top, bottom) { * Refresh all of container's internal dimensions and offsets. * * @private - * @memberof Grid.prototype */ -Grid.prototype._refreshDimensions = function() { +Grid.prototype._refreshDimensions = function () { this._updateBoundingRect(); this._updateBorders(1, 1, 1, 1); + this._boxSizing = getStyle(this._element, 'box-sizing'); }; +/** + * Calculate and apply item positions. + * + * @private + * @param {Object} layout + */ +Grid.prototype._onLayoutDataReceived = (function () { + var itemsToLayout = []; + return function (layout) { + if (this._isDestroyed || !this._nextLayoutData || this._nextLayoutData.id !== layout.id) return; + + var grid = this; + var instant = this._nextLayoutData.instant; + var onFinish = this._nextLayoutData.onFinish; + var numItems = layout.items.length; + var counter = numItems; + var item; + var left; + var top; + var i; + + // Reset next layout data. + this._nextLayoutData = null; + + if (!this._isLayoutFinished && this._hasListeners(EVENT_LAYOUT_ABORT)) { + this._emit(EVENT_LAYOUT_ABORT, this._layout.items.slice(0)); + } + + // Update the layout reference. + this._layout = layout; + + // Update the item positions and collect all items that need to be laid + // out. It is critical that we update the item position _before_ the + // layoutStart event as the new data might be needed in the callback. + itemsToLayout.length = 0; + for (i = 0; i < numItems; i++) { + item = layout.items[i]; + + // Make sure we have a matching item. + if (!item) { + --counter; + continue; + } + + // Get the item's new left and top values. + left = layout.slots[i * 2]; + top = layout.slots[i * 2 + 1]; + + // Let's skip the layout process if we can. Possibly avoids a lot of DOM + // operations which saves us some CPU cycles. + if (item._canSkipLayout(left, top)) { + --counter; + continue; + } + + // Update the item's position. + item._left = left; + item._top = top; + + // Only active non-dragged items need to be moved. + if (item.isActive() && !item.isDragging()) { + itemsToLayout.push(item); + } + } + + // Set layout styles to the grid element. + if (layout.styles) { + setStyles(this._element, layout.styles); + } + + // layoutStart event is intentionally emitted after the container element's + // dimensions are set, because otherwise there would be no hook for reacting + // to container dimension changes. + if (this._hasListeners(EVENT_LAYOUT_START)) { + this._emit(EVENT_LAYOUT_START, layout.items.slice(0), instant === true); + } + + function tryFinish() { + if (--counter > 0) return; + + var hasLayoutChanged = grid._layout.id !== layout.id; + var callback = isFunction(instant) ? instant : onFinish; + + if (!hasLayoutChanged) { + grid._isLayoutFinished = true; + } + + if (isFunction(callback)) { + callback(layout.items.slice(0), hasLayoutChanged); + } + + if (!hasLayoutChanged && grid._hasListeners(EVENT_LAYOUT_END)) { + grid._emit(EVENT_LAYOUT_END, layout.items.slice(0)); + } + } + + if (!itemsToLayout.length) { + tryFinish(); + return this; + } + + this._isLayoutFinished = false; + + for (i = 0; i < itemsToLayout.length; i++) { + if (this._layout.id !== layout.id) break; + itemsToLayout[i]._layout.start(instant === true, tryFinish); + } + + if (this._layout.id === layout.id) { + itemsToLayout.length = 0; + } + + return this; + }; +})(); + /** * Show or hide Grid instance's items. * * @private - * @memberof Grid.prototype - * @param {GridMultiItemQuery} items + * @param {Item[]} items * @param {Boolean} toVisible * @param {Object} [options] * @param {Boolean} [options.instant=false] - * @param {(ShowCallback|HideCallback)} [options.onFinish] - * @param {(Boolean|LayoutCallback|String)} [options.layout=true] + * @param {Boolean} [options.syncWithLayout=true] + * @param {Function} [options.onFinish] + * @param {(Boolean|Function|String)} [options.layout=true] */ -Grid.prototype._setItemsVisibility = function(items, toVisible, options) { +Grid.prototype._setItemsVisibility = function (items, toVisible, options) { var grid = this; - var targetItems = this.getItems(items); - var opts = options || 0; + var targetItems = items.slice(0); + var opts = options || {}; var isInstant = opts.instant === true; var callback = opts.onFinish; var layout = opts.layout ? opts.layout : opts.layout === undefined; var counter = targetItems.length; - var startEvent = toVisible ? eventShowStart : eventHideStart; - var endEvent = toVisible ? eventShowEnd : eventHideEnd; + var startEvent = toVisible ? EVENT_SHOW_START : EVENT_HIDE_START; + var endEvent = toVisible ? EVENT_SHOW_END : EVENT_HIDE_END; var method = toVisible ? 'show' : 'hide'; var needsLayout = false; var completedItems = []; @@ -1364,12 +1477,7 @@ Grid.prototype._setItemsVisibility = function(items, toVisible, options) { return; } - // Emit showStart/hideStart event. - if (this._hasListeners(startEvent)) { - this._emit(startEvent, targetItems.slice(0)); - } - - // Show/hide items. + // Prepare the items. for (i = 0; i < targetItems.length; i++) { item = targetItems[i]; @@ -1381,9 +1489,7 @@ Grid.prototype._setItemsVisibility = function(items, toVisible, options) { // If inactive item is shown we also need to do a little hack to make the // item not animate it's next positioning (layout). - if (toVisible && !item._isActive) { - item._layout._skipNextAnimation = true; - } + item._layout._skipNextAnimation = !!(toVisible && !item._isActive); // If a hidden item is being shown we need to refresh the item's // dimensions. @@ -1391,27 +1497,66 @@ Grid.prototype._setItemsVisibility = function(items, toVisible, options) { hiddenItems.push(item); } - // Show/hide the item. - item._visibility[method](isInstant, function(interrupted, item) { - // If the current item's animation was not interrupted add it to the - // completedItems array. - if (!interrupted) completedItems.push(item); - - // If all items have finished their animations call the callback - // and emit showEnd/hideEnd event. - if (--counter < 1) { - if (isFunction(callback)) callback(completedItems.slice(0)); - if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); + // Add item to layout or remove it from layout. + if (toVisible) { + item._addToLayout(); + } else { + item._removeFromLayout(); + } + } + + // Force refresh the dimensions of all hidden items. + if (hiddenItems.length) { + this.refreshItems(hiddenItems, true); + hiddenItems.length = 0; + } + + // Show the items in sync with the next layout. + function triggerVisibilityChange() { + if (needsLayout && opts.syncWithLayout !== false) { + grid.off(EVENT_LAYOUT_START, triggerVisibilityChange); + } + + if (grid._hasListeners(startEvent)) { + grid._emit(startEvent, targetItems.slice(0)); + } + + for (i = 0; i < targetItems.length; i++) { + // Make sure the item is still in the original grid. There is a chance + // that the item starts migrating before tiggerVisibilityChange is called. + if (targetItems[i]._gridId !== grid._id) { + if (--counter < 1) { + if (isFunction(callback)) callback(completedItems.slice(0)); + if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); + } + continue; } - }); + + targetItems[i]._visibility[method](isInstant, function (interrupted, item) { + // If the current item's animation was not interrupted add it to the + // completedItems array. + if (!interrupted) completedItems.push(item); + + // If all items have finished their animations call the callback + // and emit showEnd/hideEnd event. + if (--counter < 1) { + if (isFunction(callback)) callback(completedItems.slice(0)); + if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); + } + }); + } } - // Refresh hidden items. - if (hiddenItems.length) this.refreshItems(hiddenItems); + // Trigger the visibility change, either async with layout or instantly. + if (needsLayout && opts.syncWithLayout !== false) { + this.on(EVENT_LAYOUT_START, triggerVisibilityChange); + } else { + triggerVisibilityChange(); + } - // Layout if needed. + // Trigger layout if needed. if (needsLayout && layout) { - this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); + this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); } }; @@ -1433,19 +1578,29 @@ Grid.prototype._setItemsVisibility = function(items, toVisible, options) { */ function mergeSettings(defaultSettings, userSettings) { // Create a fresh copy of default settings. - var ret = mergeObjects({}, defaultSettings); + var settings = mergeObjects({}, defaultSettings); // Merge user settings to default settings. if (userSettings) { - ret = mergeObjects(ret, userSettings); + settings = mergeObjects(settings, userSettings); } // Handle visible/hidden styles manually so that the whole object is // overridden instead of the props. - ret.visibleStyles = (userSettings || 0).visibleStyles || (defaultSettings || 0).visibleStyles; - ret.hiddenStyles = (userSettings || 0).hiddenStyles || (defaultSettings || 0).hiddenStyles; - return ret; + if (userSettings && userSettings.visibleStyles) { + settings.visibleStyles = userSettings.visibleStyles; + } else if (defaultSettings && defaultSettings.visibleStyles) { + settings.visibleStyles = defaultSettings.visibleStyles; + } + + if (userSettings && userSettings.hiddenStyles) { + settings.hiddenStyles = userSettings.hiddenStyles; + } else if (defaultSettings && defaultSettings.hiddenStyles) { + settings.hiddenStyles = defaultSettings.hiddenStyles; + } + + return settings; } /** @@ -1498,4 +1653,122 @@ function mergeObjects(target, source) { return target; } +/** + * Collect and return initial items for grid. + * + * @param {HTMLElement} gridElement + * @param {?(HTMLElement[]|NodeList|HtmlCollection|String)} elements + * @returns {(HTMLElement[]|NodeList|HtmlCollection)} + */ +function getInitialGridElements(gridElement, elements) { + // If we have a wildcard selector let's return all the children. + if (elements === '*') { + return gridElement.children; + } + + // If we have some more specific selector, let's filter the elements. + if (typeof elements === STRING_TYPE) { + var result = []; + var children = gridElement.children; + for (var i = 0; i < children.length; i++) { + if (elementMatches(children[i], elements)) { + result.push(children[i]); + } + } + return result; + } + + // If we have an array of elements or a node list. + if (Array.isArray(elements) || isNodeList(elements)) { + return elements; + } + + // Otherwise just return an empty array. + return []; +} + +/** + * Bind grid's resize handler to window. + * + * @param {Grid} grid + * @param {(Number|Boolean)} delay + */ +function bindLayoutOnResize(grid, delay) { + if (typeof delay !== NUMBER_TYPE) { + delay = delay === true ? 0 : -1; + } + + if (delay >= 0) { + grid._resizeHandler = debounce(function () { + grid.refreshItems().layout(); + }, delay); + + window.addEventListener('resize', grid._resizeHandler); + } +} + +/** + * Unbind grid's resize handler from window. + * + * @param {Grid} grid + */ +function unbindLayoutOnResize(grid) { + if (grid._resizeHandler) { + grid._resizeHandler(true); + window.removeEventListener('resize', grid._resizeHandler); + grid._resizeHandler = null; + } +} + +/** + * Normalize style declaration object, returns a normalized (new) styles object + * (prefixed properties and invalid properties removed). + * + * @param {Object} styles + * @returns {Object} + */ +function normalizeStyles(styles) { + var normalized = {}; + var docElemStyle = document.documentElement.style; + var prop, prefixedProp; + + // Normalize visible styles (prefix and remove invalid). + for (prop in styles) { + if (!styles[prop]) continue; + prefixedProp = getPrefixedPropName(docElemStyle, prop); + if (!prefixedProp) continue; + normalized[prefixedProp] = styles[prop]; + } + + return normalized; +} + +/** + * Create index map from items. + * + * @param {Item[]} items + * @returns {Object} + */ +function createIndexMap(items) { + var result = {}; + for (var i = 0; i < items.length; i++) { + result[items[i]._id] = i; + } + return result; +} + +/** + * Sort comparer function for items' index map. + * + * @param {Object} indexMap + * @param {Item} itemA + * @param {Item} itemB + * @returns {Number} + */ +function compareIndexMap(indexMap, itemA, itemB) { + var indexA = indexMap[itemA._id]; + var indexB = indexMap[itemB._id]; + return indexA - indexB; +} + export default Grid; diff --git a/src/Item/Item.js b/src/Item/Item.js index 247978c4..6616327f 100644 --- a/src/Item/Item.js +++ b/src/Item/Item.js @@ -4,15 +4,15 @@ * https://github.com/haltu/muuri/blob/master/LICENSE.md */ -import { gridInstances } from '../shared'; +import { GRID_INSTANCES, ITEM_ELEMENT_MAP } from '../constants'; -import ItemAnimate from './ItemAnimate'; import ItemDrag from './ItemDrag'; import ItemDragPlaceholder from './ItemDragPlaceholder'; +import ItemDragRelease from './ItemDragRelease'; import ItemLayout from './ItemLayout'; import ItemMigrate from './ItemMigrate'; -import ItemRelease from './ItemRelease'; import ItemVisibility from './ItemVisibility'; +import Emitter from '../Emitter/Emitter'; import addClass from '../utils/addClass'; import createUid from '../utils/createUid'; @@ -20,7 +20,7 @@ import getStyle from '../utils/getStyle'; import getStyleAsFloat from '../utils/getStyleAsFloat'; import getTranslateString from '../utils/getTranslateString'; import removeClass from '../utils/removeClass'; -import { transformProp } from '../utils/supportedTransform'; +import transformProp from '../utils/transformProp'; /** * Creates a new Item instance for a Grid instance. @@ -33,25 +33,35 @@ import { transformProp } from '../utils/supportedTransform'; function Item(grid, element, isActive) { var settings = grid._settings; - // Create instance id. - this._id = createUid(); + // Store item/element pair to a map (for faster item querying by element). + if (ITEM_ELEMENT_MAP) { + if (ITEM_ELEMENT_MAP.has(element)) { + throw new Error('You can only create one Muuri Item per element!'); + } else { + ITEM_ELEMENT_MAP.set(element, this); + } + } - // Reference to connected Grid instance's id. + this._id = createUid(); this._gridId = grid._id; - - // Destroyed flag. + this._element = element; this._isDestroyed = false; - - // Set up initial positions. this._left = 0; this._top = 0; - - // The elements. - this._element = element; - this._child = element.children[0]; + this._width = 0; + this._height = 0; + this._marginLeft = 0; + this._marginRight = 0; + this._marginTop = 0; + this._marginBottom = 0; + this._tX = undefined; + this._tY = undefined; + this._sortData = null; + this._emitter = new Emitter(); // If the provided item element is not a direct child of the grid container - // element, append it to the grid container. + // element, append it to the grid container. Note, we are indeed reading the + // DOM here but it's a property that does not cause reflowing. if (element.parentNode !== grid._element) { grid._element.appendChild(element); } @@ -59,7 +69,9 @@ function Item(grid, element, isActive) { // Set item class. addClass(element, settings.itemClass); - // If isActive is not defined, let's try to auto-detect it. + // If isActive is not defined, let's try to auto-detect it. Note, we are + // indeed reading the DOM here but it's a property that does not cause + // reflowing. if (typeof isActive !== 'boolean') { isActive = getStyle(element, 'display') !== 'none'; } @@ -68,15 +80,6 @@ function Item(grid, element, isActive) { // or not). this._isActive = isActive; - // Set element's initial position styles. - element.style.left = '0'; - element.style.top = '0'; - element.style[transformProp] = getTranslateString(0, 0); - - // Initiate item's animation controllers. - this._animate = new ItemAnimate(element); - this._animateChild = new ItemAnimate(this._child); - // Setup visibility handler. this._visibility = new ItemVisibility(this); @@ -86,22 +89,25 @@ function Item(grid, element, isActive) { // Set up migration handler data. this._migrate = new ItemMigrate(this); + // Set up drag handler. + this._drag = settings.dragEnabled ? new ItemDrag(this) : null; + // Set up release handler. Note that although this is fully linked to dragging // this still needs to be always instantiated to handle migration scenarios // correctly. - this._release = new ItemRelease(this); + this._dragRelease = new ItemDragRelease(this); // Set up drag placeholder handler. Note that although this is fully linked to // dragging this still needs to be always instantiated to handle migration // scenarios correctly. this._dragPlaceholder = new ItemDragPlaceholder(this); - // Set up drag handler. - this._drag = settings.dragEnabled ? new ItemDrag(this) : null; - - // Set up the initial dimensions and sort data. - this._refreshDimensions(); - this._refreshSortData(); + // Note! You must call the following methods before you start using the + // instance. They are deliberately not called in the end as it would cause + // potentially a massive amount of reflows if multiple items were instantiated + // in a loop. + // this._refreshDimensions(); + // this._refreshSortData(); } /** @@ -113,21 +119,19 @@ function Item(grid, element, isActive) { * Get the instance grid reference. * * @public - * @memberof Item.prototype * @returns {Grid} */ -Item.prototype.getGrid = function() { - return gridInstances[this._gridId]; +Item.prototype.getGrid = function () { + return GRID_INSTANCES[this._gridId]; }; /** * Get the instance element. * * @public - * @memberof Item.prototype * @returns {HTMLElement} */ -Item.prototype.getElement = function() { +Item.prototype.getElement = function () { return this._element; }; @@ -135,10 +139,9 @@ Item.prototype.getElement = function() { * Get instance element's cached width. * * @public - * @memberof Item.prototype * @returns {Number} */ -Item.prototype.getWidth = function() { +Item.prototype.getWidth = function () { return this._width; }; @@ -146,10 +149,9 @@ Item.prototype.getWidth = function() { * Get instance element's cached height. * * @public - * @memberof Item.prototype * @returns {Number} */ -Item.prototype.getHeight = function() { +Item.prototype.getHeight = function () { return this._height; }; @@ -157,17 +159,16 @@ Item.prototype.getHeight = function() { * Get instance element's cached margins. * * @public - * @memberof Item.prototype * @returns {Object} * - The returned object contains left, right, top and bottom properties * which indicate the item element's cached margins. */ -Item.prototype.getMargin = function() { +Item.prototype.getMargin = function () { return { left: this._marginLeft, right: this._marginRight, top: this._marginTop, - bottom: this._marginBottom + bottom: this._marginBottom, }; }; @@ -175,15 +176,14 @@ Item.prototype.getMargin = function() { * Get instance element's cached position. * * @public - * @memberof Item.prototype * @returns {Object} * - The returned object contains left and top properties which indicate the * item element's cached position in the grid. */ -Item.prototype.getPosition = function() { +Item.prototype.getPosition = function () { return { left: this._left, - top: this._top + top: this._top, }; }; @@ -191,10 +191,9 @@ Item.prototype.getPosition = function() { * Is the item active? * * @public - * @memberof Item.prototype * @returns {Boolean} */ -Item.prototype.isActive = function() { +Item.prototype.isActive = function () { return this._isActive; }; @@ -202,10 +201,9 @@ Item.prototype.isActive = function() { * Is the item visible? * * @public - * @memberof Item.prototype * @returns {Boolean} */ -Item.prototype.isVisible = function() { +Item.prototype.isVisible = function () { return !!this._visibility && !this._visibility._isHidden; }; @@ -213,10 +211,9 @@ Item.prototype.isVisible = function() { * Is the item being animated to visible? * * @public - * @memberof Item.prototype * @returns {Boolean} */ -Item.prototype.isShowing = function() { +Item.prototype.isShowing = function () { return !!(this._visibility && this._visibility._isShowing); }; @@ -224,10 +221,9 @@ Item.prototype.isShowing = function() { * Is the item being animated to hidden? * * @public - * @memberof Item.prototype * @returns {Boolean} */ -Item.prototype.isHiding = function() { +Item.prototype.isHiding = function () { return !!(this._visibility && this._visibility._isHiding); }; @@ -235,21 +231,19 @@ Item.prototype.isHiding = function() { * Is the item positioning? * * @public - * @memberof Item.prototype * @returns {Boolean} */ -Item.prototype.isPositioning = function() { +Item.prototype.isPositioning = function () { return !!(this._layout && this._layout._isActive); }; /** - * Is the item being dragged? + * Is the item being dragged (or queued for dragging)? * * @public - * @memberof Item.prototype * @returns {Boolean} */ -Item.prototype.isDragging = function() { +Item.prototype.isDragging = function () { return !!(this._drag && this._drag._isActive); }; @@ -257,21 +251,19 @@ Item.prototype.isDragging = function() { * Is the item being released? * * @public - * @memberof Item.prototype * @returns {Boolean} */ -Item.prototype.isReleasing = function() { - return !!(this._release && this._release._isActive); +Item.prototype.isReleasing = function () { + return !!(this._dragRelease && this._dragRelease._isActive); }; /** * Is the item destroyed? * * @public - * @memberof Item.prototype * @returns {Boolean} */ -Item.prototype.isDestroyed = function() { +Item.prototype.isDestroyed = function () { return this._isDestroyed; }; @@ -284,10 +276,11 @@ Item.prototype.isDestroyed = function() { * Recalculate item's dimensions. * * @private - * @memberof Item.prototype + * @param {Boolean} [force=false] */ -Item.prototype._refreshDimensions = function() { - if (this._isDestroyed || this._visibility._isHidden) return; +Item.prototype._refreshDimensions = function (force) { + if (this._isDestroyed) return; + if (force !== true && this._visibility._isHidden) return; var element = this._element; var dragPlaceholder = this._dragPlaceholder; @@ -304,18 +297,15 @@ Item.prototype._refreshDimensions = function() { this._marginBottom = Math.max(0, getStyleAsFloat(element, 'margin-bottom')); // Keep drag placeholder's dimensions synced with the item's. - if (dragPlaceholder) { - dragPlaceholder.updateDimensions(this._width, this._height); - } + if (dragPlaceholder) dragPlaceholder.updateDimensions(); }; /** * Fetch and store item's sort data. * * @private - * @memberof Item.prototype */ -Item.prototype._refreshSortData = function() { +Item.prototype._refreshSortData = function () { if (this._isDestroyed) return; var data = (this._sortData = {}); @@ -327,43 +317,100 @@ Item.prototype._refreshSortData = function() { } }; +/** + * Add item to layout. + * + * @private + */ +Item.prototype._addToLayout = function (left, top) { + if (this._isActive === true) return; + this._isActive = true; + this._left = left || 0; + this._top = top || 0; +}; + +/** + * Remove item from layout. + * + * @private + */ +Item.prototype._removeFromLayout = function () { + if (this._isActive === false) return; + this._isActive = false; + this._left = 0; + this._top = 0; +}; + +/** + * Check if the layout procedure can be skipped for the item. + * + * @private + * @param {Number} left + * @param {Number} top + * @returns {Boolean} + */ +Item.prototype._canSkipLayout = function (left, top) { + return ( + this._left === left && + this._top === top && + !this._migrate._isActive && + !this._layout._skipNextAnimation && + !this._dragRelease.isJustReleased() + ); +}; + +/** + * Set the provided left and top arguments as the item element's translate + * values in the DOM. This method keeps track of the currently applied + * translate values and skips the update operation if the provided values are + * identical to the currently applied values. Returns `false` if there was no + * need for update and `true` if the translate value was updated. + * + * @private + * @param {Number} left + * @param {Number} top + * @returns {Boolean} + */ +Item.prototype._setTranslate = function (left, top) { + if (this._tX === left && this._tY === top) return false; + this._tX = left; + this._tY = top; + this._element.style[transformProp] = getTranslateString(left, top); + return true; +}; + /** * Destroy item instance. * * @private - * @memberof Item.prototype * @param {Boolean} [removeElement=false] */ -Item.prototype._destroy = function(removeElement) { +Item.prototype._destroy = function (removeElement) { if (this._isDestroyed) return; var element = this._element; var grid = this.getGrid(); var settings = grid._settings; - var index = grid._items.indexOf(this); // Destroy handlers. - this._release.destroy(); + this._dragPlaceholder.destroy(); + this._dragRelease.destroy(); this._migrate.destroy(); this._layout.destroy(); this._visibility.destroy(); - this._animate.destroy(); - this._animateChild.destroy(); - this._dragPlaceholder.destroy(); - this._drag && this._drag.destroy(); + if (this._drag) this._drag.destroy(); - // Remove all inline styles. - element.removeAttribute('style'); - this._child.removeAttribute('style'); + // Destroy emitter. + this._emitter.destroy(); // Remove item class. removeClass(element, settings.itemClass); - // Remove item from Grid instance if it still exists there. - index > -1 && grid._items.splice(index, 1); - // Remove element from DOM. - removeElement && element.parentNode.removeChild(element); + if (removeElement) element.parentNode.removeChild(element); + + // Remove item/element pair from map. + if (ITEM_ELEMENT_MAP) ITEM_ELEMENT_MAP.delete(element); // Reset state. this._isActive = false; diff --git a/src/Item/ItemDrag.js b/src/Item/ItemDrag.js index 496e710c..5af19da3 100644 --- a/src/Item/ItemDrag.js +++ b/src/Item/ItemDrag.js @@ -5,47 +5,54 @@ */ import { - actionMove, - actionSwap, - eventMove, - eventSend, - eventBeforeSend, - eventReceive, - eventBeforeReceive, - eventDragInit, - eventDragStart, - eventDragMove, - eventDragScroll, - eventDragEnd, - gridInstances -} from '../shared'; + ACTION_MOVE, + ACTION_SWAP, + EVENT_MOVE, + EVENT_SEND, + EVENT_BEFORE_SEND, + EVENT_RECEIVE, + EVENT_BEFORE_RECEIVE, + EVENT_DRAG_INIT, + EVENT_DRAG_START, + EVENT_DRAG_MOVE, + EVENT_DRAG_SCROLL, + EVENT_DRAG_END, + GRID_INSTANCES, +} from '../constants'; import Dragger from '../Dragger/Dragger'; +import AutoScroller from '../AutoScroller/AutoScroller'; -import { addMoveTick, cancelMoveTick, addScrollTick, cancelScrollTick } from '../ticker'; +import { + addDragStartTick, + cancelDragStartTick, + addDragMoveTick, + cancelDragMoveTick, + addDragScrollTick, + cancelDragScrollTick, + addDragSortTick, + cancelDragSortTick, +} from '../ticker'; import addClass from '../utils/addClass'; import arrayInsert from '../utils/arrayInsert'; import arrayMove from '../utils/arrayMove'; import arraySwap from '../utils/arraySwap'; -import debounce from '../utils/debounce'; -import elementMatches from '../utils/elementMatches'; import getContainingBlock from '../utils/getContainingBlock'; +import getIntersectionScore from '../utils/getIntersectionScore'; import getOffsetDiff from '../utils/getOffsetDiff'; import getScrollableAncestors from '../utils/getScrollableAncestors'; +import getStyle from '../utils/getStyle'; import getTranslate from '../utils/getTranslate'; -import getTranslateString from '../utils/getTranslateString'; +import hasPassiveEvents from '../utils/hasPassiveEvents'; import isFunction from '../utils/isFunction'; import normalizeArrayIndex from '../utils/normalizeArrayIndex'; import removeClass from '../utils/removeClass'; -import setStyles from '../utils/setStyles'; -import { transformProp } from '../utils/supportedTransform'; -// Drag start predicate states. -var startPredicateInactive = 0; -var startPredicatePending = 1; -var startPredicateResolved = 2; -var startPredicateRejected = 3; +var START_PREDICATE_INACTIVE = 0; +var START_PREDICATE_PENDING = 1; +var START_PREDICATE_RESOLVED = 2; +var SCROLL_LISTENER_OPTIONS = hasPassiveEvents() ? { passive: true } : false; /** * Bind touch interaction to an item. @@ -67,15 +74,17 @@ function ItemDrag(item) { this._startPredicate = isFunction(settings.dragStartPredicate) ? settings.dragStartPredicate : ItemDrag.defaultStartPredicate; - this._startPredicateState = startPredicateInactive; + this._startPredicateState = START_PREDICATE_INACTIVE; this._startPredicateResult = undefined; // Data for drag sort predicate heuristics. - this._hBlockedIndex = null; - this._hX1 = 0; - this._hX2 = 0; - this._hY1 = 0; - this._hY2 = 0; + this._isSortNeeded = false; + this._sortTimer = undefined; + this._blockedSortIndex = null; + this._sortX1 = 0; + this._sortX2 = 0; + this._sortY1 = 0; + this._sortY2 = 0; // Setup item's initial drag data. this._reset(); @@ -84,24 +93,38 @@ function ItemDrag(item) { this._preStartCheck = this._preStartCheck.bind(this); this._preEndCheck = this._preEndCheck.bind(this); this._onScroll = this._onScroll.bind(this); + this._prepareStart = this._prepareStart.bind(this); + this._applyStart = this._applyStart.bind(this); this._prepareMove = this._prepareMove.bind(this); this._applyMove = this._applyMove.bind(this); this._prepareScroll = this._prepareScroll.bind(this); this._applyScroll = this._applyScroll.bind(this); - this._checkOverlap = this._checkOverlap.bind(this); + this._handleSort = this._handleSort.bind(this); + this._handleSortDelayed = this._handleSortDelayed.bind(this); - // Create debounce overlap checker function. - var sortInterval = settings.dragSortHeuristics.sortInterval; - this._checkOverlapDebounce = debounce(this._checkOverlap, sortInterval); + // Get drag handle element. + this._handle = (settings.dragHandle && element.querySelector(settings.dragHandle)) || element; // Init dragger. - this._dragger = new Dragger(element, settings.dragCssProps); + this._dragger = new Dragger(this._handle, settings.dragCssProps); this._dragger.on('start', this._preStartCheck); this._dragger.on('move', this._preStartCheck); this._dragger.on('cancel', this._preEndCheck); this._dragger.on('end', this._preEndCheck); } +/** + * Public properties + * ***************** + */ + +/** + * @public + * @static + * @type {AutoScroller} + */ +ItemDrag.autoScroller = new AutoScroller(); + /** * Public static methods * ********************* @@ -115,18 +138,35 @@ function ItemDrag(item) { * the predicate will be called again on the next drag movement. * * @public - * @memberof ItemDrag + * @static * @param {Item} item - * @param {DraggerEvent} event + * @param {Object} event * @param {Object} [options] * - An optional options object which can be used to pass the predicate * it's options manually. By default the predicate retrieves the options * from the grid's settings. - * @returns {Boolean} + * @returns {(Boolean|undefined)} */ -ItemDrag.defaultStartPredicate = function(item, event, options) { +ItemDrag.defaultStartPredicate = function (item, event, options) { var drag = item._drag; - var predicate = drag._startPredicateData || drag._setupStartPredicate(options); + + // Make sure left button is pressed on mouse. + if (event.isFirst && event.srcEvent.button) { + return false; + } + + // If the start event is trusted, non-cancelable and it's default action has + // not been prevented it is in most cases a sign that the gesture would be + // cancelled anyways right after it has started (e.g. starting drag while + // the page is scrolling). + if ( + event.isFirst && + event.srcEvent.isTrusted === true && + event.srcEvent.defaultPrevented === false && + event.srcEvent.cancelable === false + ) { + return false; + } // Final event logic. At this stage return value does not matter anymore, // the predicate is either resolved or it's not and there's nothing to do @@ -137,13 +177,14 @@ ItemDrag.defaultStartPredicate = function(item, event, options) { return; } - // Find and store the handle element so we can check later on if the - // cursor is within the handle. If we have a handle selector let's find - // the corresponding element. Otherwise let's use the item element as the - // handle. - if (!predicate.handleElement) { - predicate.handleElement = drag._getStartPredicateHandle(event); - if (!predicate.handleElement) return false; + // Setup predicate data from options if not already set. + var predicate = drag._startPredicateData; + if (!predicate) { + var config = options || drag._getGrid()._settings.dragStartPredicate || {}; + drag._startPredicateData = predicate = { + distance: Math.max(config.distance, 0) || 0, + delay: Math.max(config.delay, 0) || 0, + }; } // If delay is defined let's keep track of the latest event and initiate @@ -151,7 +192,7 @@ ItemDrag.defaultStartPredicate = function(item, event, options) { if (predicate.delay) { predicate.event = event; if (!predicate.delayTimer) { - predicate.delayTimer = window.setTimeout(function() { + predicate.delayTimer = window.setTimeout(function () { predicate.delay = 0; if (drag._resolveStartPredicate(predicate.event)) { drag._forceResolveStartPredicate(predicate.event); @@ -168,20 +209,22 @@ ItemDrag.defaultStartPredicate = function(item, event, options) { * Default drag sort predicate. * * @public - * @memberof ItemDrag + * @static * @param {Item} item * @param {Object} [options] * @param {Number} [options.threshold=50] * @param {String} [options.action='move'] - * @returns {(Boolean|DragSortCommand)} - * - Returns false if no valid index was found. Otherwise returns drag sort + * @returns {?Object} + * - Returns `null` if no valid index was found. Otherwise returns drag sort * command. */ -ItemDrag.defaultSortPredicate = (function() { +ItemDrag.defaultSortPredicate = (function () { var itemRect = {}; var targetRect = {}; var returnData = {}; - var rootGridArray = []; + var gridsArray = []; + var minThreshold = 1; + var maxThreshold = 100; function getTargetGrid(item, rootGrid, threshold) { var target = null; @@ -190,18 +233,26 @@ ItemDrag.defaultSortPredicate = (function() { var gridScore; var grids; var grid; + var container; + var containerRect; + var left; + var top; + var right; + var bottom; var i; // Get potential target grids. if (dragSort === true) { - rootGridArray[0] = rootGrid; - grids = rootGridArray; - } else { + gridsArray[0] = rootGrid; + grids = gridsArray; + } else if (isFunction(dragSort)) { grids = dragSort.call(rootGrid, item); } // Return immediately if there are no grids. - if (!Array.isArray(grids)) return target; + if (!grids || !Array.isArray(grids) || !grids.length) { + return target; + } // Loop through the grids and get the best match. for (i = 0; i < grids.length; i++) { @@ -210,16 +261,53 @@ ItemDrag.defaultSortPredicate = (function() { // Filter out all destroyed grids. if (grid._isDestroyed) continue; - // We need to update the grid's offsets and dimensions since they might - // have changed (e.g during scrolling). + // Compute the grid's client rect an clamp the initial boundaries to + // viewport dimensions. grid._updateBoundingRect(); + left = Math.max(0, grid._left); + top = Math.max(0, grid._top); + right = Math.min(window.innerWidth, grid._right); + bottom = Math.min(window.innerHeight, grid._bottom); + + // The grid might be inside one or more elements that clip it's visibility + // (e.g overflow scroll/hidden) so we want to find out the visible portion + // of the grid in the viewport and use that in our calculations. + container = grid._element.parentNode; + while ( + container && + container !== document && + container !== document.documentElement && + container !== document.body + ) { + if (container.getRootNode && container instanceof DocumentFragment) { + container = container.getRootNode().host; + continue; + } + + if (getStyle(container, 'overflow') !== 'visible') { + containerRect = container.getBoundingClientRect(); + left = Math.max(left, containerRect.left); + top = Math.max(top, containerRect.top); + right = Math.min(right, containerRect.right); + bottom = Math.min(bottom, containerRect.bottom); + } + + if (getStyle(container, 'position') === 'fixed') { + break; + } + + container = container.parentNode; + } + + // No need to go further if target rect does not have visible area. + if (left >= right || top >= bottom) continue; // Check how much dragged element overlaps the container element. - targetRect.width = grid._width; - targetRect.height = grid._height; - targetRect.left = grid._left; - targetRect.top = grid._top; - gridScore = getRectOverlapScore(itemRect, targetRect); + targetRect.left = left; + targetRect.top = top; + targetRect.width = right - left; + targetRect.height = bottom - top; + gridScore = getIntersectionScore(itemRect, targetRect); // Check if this grid is the best match so far. if (gridScore > threshold && gridScore > bestScore) { @@ -228,38 +316,46 @@ ItemDrag.defaultSortPredicate = (function() { } } - // Always reset root grid array. - rootGridArray.length = 0; + // Always reset grids array. + gridsArray.length = 0; return target; } - return function(item, options) { + return function (item, options) { var drag = item._drag; var rootGrid = drag._getGrid(); // Get drag sort predicate settings. var sortThreshold = options && typeof options.threshold === 'number' ? options.threshold : 50; - var sortAction = options && options.action === actionSwap ? actionSwap : actionMove; + var sortAction = options && options.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; + var migrateAction = + options && options.migrateAction === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; + + // Sort threshold must be a positive number capped to a max value of 100. If + // that's not the case this function will not work correctly. So let's clamp + // the threshold just in case. + sortThreshold = Math.min(Math.max(sortThreshold, minThreshold), maxThreshold); // Populate item rect data. itemRect.width = item._width; itemRect.height = item._height; - itemRect.left = drag._elementClientX; - itemRect.top = drag._elementClientY; + itemRect.left = drag._clientX; + itemRect.top = drag._clientY; // Calculate the target grid. var grid = getTargetGrid(item, rootGrid, sortThreshold); // Return early if we found no grid container element that overlaps the // dragged item enough. - if (!grid) return false; + if (!grid) return null; + var isMigration = item.getGrid() !== grid; var gridOffsetLeft = 0; var gridOffsetTop = 0; - var matchScore = -1; - var matchIndex; - var hasValidTargets; + var matchScore = 0; + var matchIndex = -1; + var hasValidTargets = false; var target; var score; var i; @@ -294,7 +390,7 @@ ItemDrag.defaultSortPredicate = (function() { targetRect.height = target._height; targetRect.left = target._left + target._marginLeft + gridOffsetLeft; targetRect.top = target._top + target._marginTop + gridOffsetTop; - score = getRectOverlapScore(itemRect, targetRect); + score = getIntersectionScore(itemRect, targetRect); // Update best match index and score if the target's overlap score with // the dragged item is higher than the current best match score. @@ -304,22 +400,29 @@ ItemDrag.defaultSortPredicate = (function() { } } - // If there is no valid match and the item is being moved into another - // grid. - if (matchScore < sortThreshold && item.getGrid() !== grid) { - matchIndex = hasValidTargets ? -1 : 0; - matchScore = Infinity; + // If there is no valid match and the dragged item is being moved into + // another grid we need to do some guess work here. If there simply are no + // valid targets (which means that the dragged item will be the only active + // item in the new grid) we can just add it as the first item. If we have + // valid items in the new grid and the dragged item is overlapping one or + // more of the items in the new grid let's make an exception with the + // threshold and just pick the item which the dragged item is overlapping + // most. However, if the dragged item is not overlapping any of the valid + // items in the new grid let's position it as the last item in the grid. + if (isMigration && matchScore < sortThreshold) { + matchIndex = hasValidTargets ? matchIndex : 0; + matchScore = sortThreshold; } // Check if the best match overlaps enough to justify a placement switch. if (matchScore >= sortThreshold) { returnData.grid = grid; returnData.index = matchIndex; - returnData.action = sortAction; + returnData.action = isMigration ? migrateAction : sortAction; return returnData; } - return false; + return null; }; })(); @@ -332,62 +435,84 @@ ItemDrag.defaultSortPredicate = (function() { * Abort dragging and reset drag data. * * @public - * @memberof ItemDrag.prototype - * @returns {ItemDrag} */ -ItemDrag.prototype.stop = function() { - var item = this._item; - var element = item._element; - var grid = this._getGrid(); - - if (!this._isActive) return this; +ItemDrag.prototype.stop = function () { + if (!this._isActive) return; // If the item is being dropped into another grid, finish it up and return // immediately. if (this._isMigrating) { this._finishMigration(); - return this; + return; } - // Cancel queued move and scroll ticks. - cancelMoveTick(item._id); - cancelScrollTick(item._id); - - // Remove scroll listeners. - this._unbindScrollListeners(); - - // Cancel overlap check. - this._checkOverlapDebounce('cancel'); + // Cancel queued ticks. + var itemId = this._item._id; + cancelDragStartTick(itemId); + cancelDragMoveTick(itemId); + cancelDragScrollTick(itemId); + + // Cancel sort procedure. + this._cancelSort(); + + if (this._isStarted) { + // Remove scroll listeners. + this._unbindScrollListeners(); + + var element = item._element; + var grid = this._getGrid(); + var draggingClass = grid._settings.itemDraggingClass; + + // Append item element to the container if it's not it's child. Also make + // sure the translate values are adjusted to account for the DOM shift. + if (element.parentNode !== grid._element) { + grid._element.appendChild(element); + item._setTranslate(this._gridX, this._gridY); + + // We need to do forced reflow to make sure the dragging class is removed + // gracefully. + // eslint-disable-next-line + if (draggingClass) element.clientWidth; + } - // Append item element to the container if it's not it's child. Also make - // sure the translate values are adjusted to account for the DOM shift. - if (element.parentNode !== grid._element) { - grid._element.appendChild(element); - element.style[transformProp] = getTranslateString(this._gridX, this._gridY); + // Remove dragging class. + removeClass(element, draggingClass); } - // Remove dragging class. - removeClass(element, grid._settings.itemDraggingClass); - // Reset drag data. this._reset(); +}; - return this; +/** + * Manually trigger drag sort. This is only needed for special edge cases where + * e.g. you have disabled sort and want to trigger a sort right after enabling + * it (and don't want to wait for the next move/scroll event). + * + * @private + * @param {Boolean} [force=false] + */ +ItemDrag.prototype.sort = function (force) { + var item = this._item; + if (item._isActive && this._dragMoveEvent) { + if (force === true) { + this._handleSort(); + } else { + addDragSortTick(item._id, this._handleSort); + } + } }; /** * Destroy instance. * * @public - * @memberof ItemDrag.prototype - * @returns {ItemDrag} */ -ItemDrag.prototype.destroy = function() { - if (this._isDestroyed) return this; +ItemDrag.prototype.destroy = function () { + if (this._isDestroyed) return; this.stop(); this._dragger.destroy(); + ItemDrag.autoScroller.removeItem(this._item); this._isDestroyed = true; - return this; }; /** @@ -399,22 +524,20 @@ ItemDrag.prototype.destroy = function() { * Get Grid instance. * * @private - * @memberof ItemDrag.prototype * @returns {?Grid} */ -ItemDrag.prototype._getGrid = function() { - return gridInstances[this._gridId] || null; +ItemDrag.prototype._getGrid = function () { + return GRID_INSTANCES[this._gridId] || null; }; /** * Setup/reset drag data. * * @private - * @memberof ItemDrag.prototype */ -ItemDrag.prototype._reset = function() { - // Is item being dragged? +ItemDrag.prototype._reset = function () { this._isActive = false; + this._isStarted = false; // The dragged item's container element. this._container = null; @@ -423,7 +546,9 @@ ItemDrag.prototype._reset = function() { this._containingBlock = null; // Drag/scroll event data. - this._dragEvent = null; + this._dragStartEvent = null; + this._dragMoveEvent = null; + this._dragPrevMoveEvent = null; this._scrollEvent = null; // All the elements which need to be listened for scroll events during @@ -440,8 +565,16 @@ ItemDrag.prototype._reset = function() { // Dragged element's current offset from window's northwest corner. Does // not account for element's margins. - this._elementClientX = 0; - this._elementClientY = 0; + this._clientX = 0; + this._clientY = 0; + + // Keep track of the clientX/Y diff for scrolling. + this._scrollDiffX = 0; + this._scrollDiffY = 0; + + // Keep track of the clientX/Y diff for moving. + this._moveDiffX = 0; + this._moveDiffY = 0; // Offset difference between the dragged element's temporary drag // container and it's original container. @@ -454,9 +587,8 @@ ItemDrag.prototype._reset = function() { * dragged element and the drag container element. * * @private - * @memberof ItemDrag.prototype */ -ItemDrag.prototype._bindScrollListeners = function() { +ItemDrag.prototype._bindScrollListeners = function () { var gridContainer = this._getGrid()._element; var dragContainer = this._container; var scrollers = this._scrollers; @@ -465,14 +597,14 @@ ItemDrag.prototype._bindScrollListeners = function() { // Get dragged element's scrolling parents. scrollers.length = 0; - getScrollableAncestors(this._item._element, false, scrollers); + getScrollableAncestors(this._item._element.parentNode, scrollers); // If drag container is defined and it's not the same element as grid // container then we need to add the grid container and it's scroll parents // to the elements which are going to be listener for scroll events. if (dragContainer !== gridContainer) { gridScrollers = []; - getScrollableAncestors(gridContainer, true, gridScrollers); + getScrollableAncestors(gridContainer, gridScrollers); for (i = 0; i < gridScrollers.length; i++) { if (scrollers.indexOf(gridScrollers[i]) < 0) { scrollers.push(gridScrollers[i]); @@ -482,7 +614,7 @@ ItemDrag.prototype._bindScrollListeners = function() { // Bind scroll listeners. for (i = 0; i < scrollers.length; i++) { - scrollers[i].addEventListener('scroll', this._onScroll); + scrollers[i].addEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS); } }; @@ -491,107 +623,42 @@ ItemDrag.prototype._bindScrollListeners = function() { * elements of the dragged element and the drag container element. * * @private - * @memberof ItemDrag.prototype */ -ItemDrag.prototype._unbindScrollListeners = function() { +ItemDrag.prototype._unbindScrollListeners = function () { var scrollers = this._scrollers; var i; for (i = 0; i < scrollers.length; i++) { - scrollers[i].removeEventListener('scroll', this._onScroll); + scrollers[i].removeEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS); } scrollers.length = 0; }; -/** - * Setup default start predicate. - * - * @private - * @memberof ItemDrag.prototype - * @param {Object} [options] - * @returns {Object} - */ -ItemDrag.prototype._setupStartPredicate = function(options) { - var config = options || this._getGrid()._settings.dragStartPredicate || 0; - return (this._startPredicateData = { - distance: Math.abs(config.distance) || 0, - delay: Math.max(config.delay, 0) || 0, - handle: typeof config.handle === 'string' ? config.handle : false - }); -}; - -/** - * Setup default start predicate handle. - * - * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event - * @returns {?HTMLElement} - */ -ItemDrag.prototype._getStartPredicateHandle = function(event) { - var predicate = this._startPredicateData; - var element = this._item._element; - var handleElement = element; - - // No handle, no hassle -> let's use the item element as the handle. - if (!predicate.handle) return handleElement; - - // If there is a specific predicate handle defined, let's try to get it. - handleElement = event.target; - while (handleElement && !elementMatches(handleElement, predicate.handle)) { - handleElement = handleElement !== element ? handleElement.parentElement : null; - } - return handleElement || null; -}; - /** * Unbind currently bound drag scroll handlers from all scrollable ancestor * elements of the dragged element and the drag container element. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event * @returns {Boolean} */ -ItemDrag.prototype._resolveStartPredicate = function(event) { +ItemDrag.prototype._resolveStartPredicate = function (event) { var predicate = this._startPredicateData; - - // If the moved distance is smaller than the threshold distance or there is - // some delay left, ignore this predicate cycle. if (event.distance < predicate.distance || predicate.delay) return; - - // Get handle rect data. - var handleRect = predicate.handleElement.getBoundingClientRect(); - var handleLeft = handleRect.left + (window.pageXOffset || 0); - var handleTop = handleRect.top + (window.pageYOffset || 0); - var handleWidth = handleRect.width; - var handleHeight = handleRect.height; - - // Reset predicate data. this._resetStartPredicate(); - - // If the cursor is still within the handle let's start the drag. - return ( - handleWidth && - handleHeight && - event.pageX >= handleLeft && - event.pageX < handleLeft + handleWidth && - event.pageY >= handleTop && - event.pageY < handleTop + handleHeight - ); + return true; }; /** * Forcefully resolve drag start predicate. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ -ItemDrag.prototype._forceResolveStartPredicate = function(event) { - if (!this._isDestroyed && this._startPredicateState === startPredicatePending) { - this._startPredicateState = startPredicateResolved; +ItemDrag.prototype._forceResolveStartPredicate = function (event) { + if (!this._isDestroyed && this._startPredicateState === START_PREDICATE_PENDING) { + this._startPredicateState = START_PREDICATE_RESOLVED; this._onStart(event); } }; @@ -600,10 +667,9 @@ ItemDrag.prototype._forceResolveStartPredicate = function(event) { * Finalize start predicate. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ -ItemDrag.prototype._finishStartPredicate = function(event) { +ItemDrag.prototype._finishStartPredicate = function (event) { var element = this._item._element; // Check if this is a click (very subjective heuristics). @@ -621,13 +687,13 @@ ItemDrag.prototype._finishStartPredicate = function(event) { * Reset drag sort heuristics. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Number} x + * @param {Number} y */ -ItemDrag.prototype._resetHeuristics = function(event) { - this._hBlockedIndex = null; - this._hX1 = this._hX2 = event.clientX; - this._hY1 = this._hY2 = event.clientY; +ItemDrag.prototype._resetHeuristics = function (x, y) { + this._blockedSortIndex = null; + this._sortX1 = this._sortX2 = x; + this._sortY1 = this._sortY2 = y; }; /** @@ -635,30 +701,28 @@ ItemDrag.prototype._resetHeuristics = function(event) { * if it can not. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Number} x + * @param {Number} y * @returns {Boolean} */ -ItemDrag.prototype._checkHeuristics = function(event) { +ItemDrag.prototype._checkHeuristics = function (x, y) { var settings = this._getGrid()._settings.dragSortHeuristics; var minDist = settings.minDragDistance; // Skip heuristics if not needed. if (minDist <= 0) { - this._hBlockedIndex = null; + this._blockedSortIndex = null; return true; } - var x = event.clientX; - var y = event.clientY; - var diffX = x - this._hX2; - var diffY = y - this._hY2; + var diffX = x - this._sortX2; + var diffY = y - this._sortY2; // If we can't do proper bounce back check make sure that the blocked index // is not set. var canCheckBounceBack = minDist > 3 && settings.minBounceBackAngle > 0; if (!canCheckBounceBack) { - this._hBlockedIndex = null; + this._blockedSortIndex = null; } if (Math.abs(diffX) > minDist || Math.abs(diffY) > minDist) { @@ -666,18 +730,18 @@ ItemDrag.prototype._checkHeuristics = function(event) { // minimum value of 3 for minDragDistance to function properly. if (canCheckBounceBack) { var angle = Math.atan2(diffX, diffY); - var prevAngle = Math.atan2(this._hX2 - this._hX1, this._hY2 - this._hY1); + var prevAngle = Math.atan2(this._sortX2 - this._sortX1, this._sortY2 - this._sortY1); var deltaAngle = Math.atan2(Math.sin(angle - prevAngle), Math.cos(angle - prevAngle)); if (Math.abs(deltaAngle) > settings.minBounceBackAngle) { - this._hBlockedIndex = null; + this._blockedSortIndex = null; } } // Update points. - this._hX1 = this._hX2; - this._hY1 = this._hY2; - this._hX2 = x; - this._hY2 = y; + this._sortX1 = this._sortX2; + this._sortY1 = this._sortY2; + this._sortX2 = x; + this._sortY2 = y; return true; } @@ -689,9 +753,8 @@ ItemDrag.prototype._checkHeuristics = function(event) { * Reset for default drag start predicate function. * * @private - * @memberof ItemDrag.prototype */ -ItemDrag.prototype._resetStartPredicate = function() { +ItemDrag.prototype._resetStartPredicate = function () { var predicate = this._startPredicateData; if (predicate) { if (predicate.delayTimer) { @@ -701,14 +764,97 @@ ItemDrag.prototype._resetStartPredicate = function() { } }; +/** + * Handle the sorting procedure. Manage drag sort heuristics/interval and + * check overlap when necessary. + * + * @private + */ +ItemDrag.prototype._handleSort = function () { + var settings = this._getGrid()._settings; + + // No sorting when drag sort is disabled. Also, account for the scenario where + // dragSort is temporarily disabled during drag procedure so we need to reset + // sort timer heuristics state too. + if ( + !settings.dragSort || + (!settings.dragAutoScroll.sortDuringScroll && ItemDrag.autoScroller.isItemScrolling(this._item)) + ) { + this._sortX1 = this._sortX2 = this._gridX; + this._sortY1 = this._sortY2 = this._gridY; + // We set this to true intentionally so that overlap check would be + // triggered as soon as possible after sort becomes enabled again. + this._isSortNeeded = true; + if (this._sortTimer !== undefined) { + this._sortTimer = window.clearTimeout(this._sortTimer); + } + return; + } + + // If sorting is enabled we always need to run the heuristics check to keep + // the tracked coordinates updated. We also allow an exception when the sort + // timer is finished because the heuristics are intended to prevent overlap + // checks based on the dragged element's immediate movement and a delayed + // overlap check is valid if it comes through, because it was valid when it + // was invoked. + var shouldSort = this._checkHeuristics(this._gridX, this._gridY); + if (!this._isSortNeeded && !shouldSort) return; + + var sortInterval = settings.dragSortHeuristics.sortInterval; + if (sortInterval <= 0 || this._isSortNeeded) { + this._isSortNeeded = false; + if (this._sortTimer !== undefined) { + this._sortTimer = window.clearTimeout(this._sortTimer); + } + this._checkOverlap(); + } else if (this._sortTimer === undefined) { + this._sortTimer = window.setTimeout(this._handleSortDelayed, sortInterval); + } +}; + +/** + * Delayed sort handler. + * + * @private + */ +ItemDrag.prototype._handleSortDelayed = function () { + this._isSortNeeded = true; + this._sortTimer = undefined; + addDragSortTick(this._item._id, this._handleSort); +}; + +/** + * Cancel and reset sort procedure. + * + * @private + */ +ItemDrag.prototype._cancelSort = function () { + this._isSortNeeded = false; + if (this._sortTimer !== undefined) { + this._sortTimer = window.clearTimeout(this._sortTimer); + } + cancelDragSortTick(this._item._id); +}; + +/** + * Handle the ending of the drag procedure for sorting. + * + * @private + */ +ItemDrag.prototype._finishSort = function () { + var isSortEnabled = this._getGrid()._settings.dragSort; + var needsFinalCheck = isSortEnabled && (this._isSortNeeded || this._sortTimer !== undefined); + this._cancelSort(); + if (needsFinalCheck) this._checkOverlap(); +}; + /** * Check (during drag) if an item is overlapping other items and based on * the configuration layout the items. * * @private - * @memberof ItemDrag.prototype */ -ItemDrag.prototype._checkOverlap = function() { +ItemDrag.prototype._checkOverlap = function () { if (!this._isActive) return; var item = this._item; @@ -718,12 +864,13 @@ ItemDrag.prototype._checkOverlap = function() { var currentIndex; var targetGrid; var targetIndex; + var targetItem; var sortAction; var isMigration; // Get overlap check result. if (isFunction(settings.dragSortPredicate)) { - result = settings.dragSortPredicate(item, this._dragEvent); + result = settings.dragSortPredicate(item, this._dragMoveEvent); } else { result = ItemDrag.defaultSortPredicate(item, settings.dragSortPredicate); } @@ -731,15 +878,19 @@ ItemDrag.prototype._checkOverlap = function() { // Let's make sure the result object has a valid index before going further. if (!result || typeof result.index !== 'number') return; + sortAction = result.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; currentGrid = item.getGrid(); targetGrid = result.grid || currentGrid; isMigration = currentGrid !== targetGrid; currentIndex = currentGrid._items.indexOf(item); - targetIndex = normalizeArrayIndex(targetGrid._items, result.index, isMigration); - sortAction = result.action === actionSwap ? actionSwap : actionMove; + targetIndex = normalizeArrayIndex( + targetGrid._items, + result.index, + isMigration && sortAction === ACTION_MOVE ? 1 : 0 + ); // Prevent position bounce. - if (!isMigration && targetIndex === this._hBlockedIndex) { + if (!isMigration && targetIndex === this._blockedSortIndex) { return; } @@ -747,22 +898,22 @@ ItemDrag.prototype._checkOverlap = function() { if (!isMigration) { // Make sure the target index is not the current index. if (currentIndex !== targetIndex) { - this._hBlockedIndex = currentIndex; + this._blockedSortIndex = currentIndex; // Do the sort. - (sortAction === actionSwap ? arraySwap : arrayMove)( + (sortAction === ACTION_SWAP ? arraySwap : arrayMove)( currentGrid._items, currentIndex, targetIndex ); // Emit move event. - if (currentGrid._hasListeners(eventMove)) { - currentGrid._emit(eventMove, { + if (currentGrid._hasListeners(EVENT_MOVE)) { + currentGrid._emit(EVENT_MOVE, { item: item, fromIndex: currentIndex, toIndex: targetIndex, - action: sortAction + action: sortAction, }); } @@ -773,27 +924,30 @@ ItemDrag.prototype._checkOverlap = function() { // If the item was moved to another grid. else { - this._hBlockedIndex = null; + this._blockedSortIndex = null; + + // Let's fetch the target item when it's still in it's original index. + targetItem = targetGrid._items[targetIndex]; // Emit beforeSend event. - if (currentGrid._hasListeners(eventBeforeSend)) { - currentGrid._emit(eventBeforeSend, { + if (currentGrid._hasListeners(EVENT_BEFORE_SEND)) { + currentGrid._emit(EVENT_BEFORE_SEND, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } // Emit beforeReceive event. - if (targetGrid._hasListeners(eventBeforeReceive)) { - targetGrid._emit(eventBeforeReceive, { + if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) { + targetGrid._emit(EVENT_BEFORE_RECEIVE, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } @@ -807,33 +961,48 @@ ItemDrag.prototype._checkOverlap = function() { currentGrid._items.splice(currentIndex, 1); arrayInsert(targetGrid._items, item, targetIndex); - // Set sort data as null, which is an indicator for the item comparison - // function that the sort data of this specific item should be fetched - // lazily. + // Reset sort data. item._sortData = null; // Emit send event. - if (currentGrid._hasListeners(eventSend)) { - currentGrid._emit(eventSend, { + if (currentGrid._hasListeners(EVENT_SEND)) { + currentGrid._emit(EVENT_SEND, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } // Emit receive event. - if (targetGrid._hasListeners(eventReceive)) { - targetGrid._emit(eventReceive, { + if (targetGrid._hasListeners(EVENT_RECEIVE)) { + targetGrid._emit(EVENT_RECEIVE, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } + // If the sort action is "swap" let's respect it and send the target item + // (if it exists) from the target grid to the originating grid. This process + // is done on purpose after the dragged item placed within the target grid + // so that we can keep this implementation as simple as possible utilizing + // the existing API. + if (sortAction === ACTION_SWAP && targetItem && targetItem.isActive()) { + // Sanity check to make sure that the target item is still part of the + // target grid. It could have been manipulated in the event handlers. + if (targetGrid._items.indexOf(targetItem) > -1) { + targetGrid.send(targetItem, currentGrid, currentIndex, { + appendTo: this._container || document.body, + layoutSender: false, + layoutReceiver: false, + }); + } + } + // Layout both grids. currentGrid.layout(); targetGrid.layout(); @@ -845,11 +1014,10 @@ ItemDrag.prototype._checkOverlap = function() { * gracefully. * * @private - * @memberof ItemDrag.prototype */ -ItemDrag.prototype._finishMigration = function() { +ItemDrag.prototype._finishMigration = function () { var item = this._item; - var release = item._release; + var release = item._dragRelease; var element = item._element; var isActive = item._isActive; var targetGrid = item.getGrid(); @@ -858,6 +1026,10 @@ ItemDrag.prototype._finishMigration = function() { var targetContainer = targetSettings.dragContainer || targetGridElement; var currentSettings = this._getGrid()._settings; var currentContainer = element.parentNode; + var currentVisClass = isActive + ? currentSettings.itemVisibleClass + : currentSettings.itemHiddenClass; + var nextVisClass = isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass; var translate; var offsetDiff; @@ -867,14 +1039,17 @@ ItemDrag.prototype._finishMigration = function() { this._isMigrating = false; this.destroy(); - // Remove current classnames. - removeClass(element, currentSettings.itemClass); - removeClass(element, currentSettings.itemVisibleClass); - removeClass(element, currentSettings.itemHiddenClass); + // Update item class. + if (currentSettings.itemClass !== targetSettings.itemClass) { + removeClass(element, currentSettings.itemClass); + addClass(element, targetSettings.itemClass); + } - // Add new classnames. - addClass(element, targetSettings.itemClass); - addClass(element, isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass); + // Update visibility class. + if (currentVisClass !== nextVisClass) { + removeClass(element, currentVisClass); + addClass(element, nextVisClass); + } // Move the item inside the target container if it's different than the // current container. @@ -886,9 +1061,8 @@ ItemDrag.prototype._finishMigration = function() { translate.y -= offsetDiff.top; } - // Update item's cached dimensions and sort data. + // Update item's cached dimensions. item._refreshDimensions(); - item._refreshSortData(); // Calculate the offset difference between target's drag container (if any) // and actual grid container element. We save it later for the release @@ -903,12 +1077,11 @@ ItemDrag.prototype._finishMigration = function() { // Adjust the position of the item element if it was moved from a container // to another. if (targetContainer !== currentContainer) { - element.style[transformProp] = getTranslateString(translate.x, translate.y); + item._setTranslate(translate.x, translate.y); } // Update child element's styles to reflect the current visibility state. - item._child.removeAttribute('style'); - setStyles(item._child, isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles); + item._visibility.setStyles(isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles); // Start the release. release.start(); @@ -918,28 +1091,29 @@ ItemDrag.prototype._finishMigration = function() { * Drag pre-start handler. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ -ItemDrag.prototype._preStartCheck = function(event) { +ItemDrag.prototype._preStartCheck = function (event) { // Let's activate drag start predicate state. - if (this._startPredicateState === startPredicateInactive) { - this._startPredicateState = startPredicatePending; + if (this._startPredicateState === START_PREDICATE_INACTIVE) { + this._startPredicateState = START_PREDICATE_PENDING; } // If predicate is pending try to resolve it. - if (this._startPredicateState === startPredicatePending) { + if (this._startPredicateState === START_PREDICATE_PENDING) { this._startPredicateResult = this._startPredicate(this._item, event); if (this._startPredicateResult === true) { - this._startPredicateState = startPredicateResolved; + this._startPredicateState = START_PREDICATE_RESOLVED; this._onStart(event); } else if (this._startPredicateResult === false) { - this._startPredicateState = startPredicateRejected; + this._resetStartPredicate(event); + this._dragger._reset(); + this._startPredicateState = START_PREDICATE_INACTIVE; } } // Otherwise if predicate is resolved and drag is active, move the item. - else if (this._startPredicateState === startPredicateResolved && this._isActive) { + else if (this._startPredicateState === START_PREDICATE_RESOLVED && this._isActive) { this._onMove(event); } }; @@ -948,238 +1122,241 @@ ItemDrag.prototype._preStartCheck = function(event) { * Drag pre-end handler. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ -ItemDrag.prototype._preEndCheck = function(event) { - // Check if the start predicate was resolved during drag. - var isResolved = this._startPredicateState === startPredicateResolved; +ItemDrag.prototype._preEndCheck = function (event) { + var isResolved = this._startPredicateState === START_PREDICATE_RESOLVED; // Do final predicate check to allow user to unbind stuff for the current // drag procedure within the predicate callback. The return value of this // check will have no effect to the state of the predicate. this._startPredicate(this._item, event); - // Reset start predicate state. - this._startPredicateState = startPredicateInactive; + this._startPredicateState = START_PREDICATE_INACTIVE; - // If predicate is resolved and dragging is active, call the end handler. - if (isResolved && this._isActive) this._onEnd(event); + if (!isResolved || !this._isActive) return; + + if (this._isStarted) { + this._onEnd(event); + } else { + this.stop(); + } }; /** * Drag start handler. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ -ItemDrag.prototype._onStart = function(event) { +ItemDrag.prototype._onStart = function (event) { var item = this._item; + if (!item._isActive) return; + + this._isActive = true; + this._dragStartEvent = event; + ItemDrag.autoScroller.addItem(item); - // If item is not active, don't start the drag. + addDragStartTick(item._id, this._prepareStart, this._applyStart); +}; + +/** + * Prepare item to be dragged. + * + * @private + * ItemDrag.prototype + */ +ItemDrag.prototype._prepareStart = function () { + var item = this._item; if (!item._isActive) return; var element = item._element; var grid = this._getGrid(); var settings = grid._settings; - var release = item._release; - var migrate = item._migrate; var gridContainer = grid._element; var dragContainer = settings.dragContainer || gridContainer; - var containingBlock = getContainingBlock(dragContainer, true); + var containingBlock = getContainingBlock(dragContainer); var translate = getTranslate(element); - var currentLeft = translate.x; - var currentTop = translate.y; var elementRect = element.getBoundingClientRect(); var hasDragContainer = dragContainer !== gridContainer; - var offsetDiff; - // Reset heuristics data. - this._resetHeuristics(event); + this._container = dragContainer; + this._containingBlock = containingBlock; + this._clientX = elementRect.left; + this._clientY = elementRect.top; + this._left = this._gridX = translate.x; + this._top = this._gridY = translate.y; + this._scrollDiffX = this._scrollDiffY = 0; + this._moveDiffX = this._moveDiffY = 0; + + this._resetHeuristics(this._gridX, this._gridY); - // If grid container is not the drag container, we need to calculate the - // offset difference between grid container and drag container's containing - // element. + // If a specific drag container is set and it is different from the + // grid's container element we store the offset between containers. if (hasDragContainer) { - offsetDiff = getOffsetDiff(containingBlock, gridContainer); + var offsetDiff = getOffsetDiff(containingBlock, gridContainer); + this._containerDiffX = offsetDiff.left; + this._containerDiffY = offsetDiff.top; } +}; + +/** + * Start drag for the item. + * + * @private + */ +ItemDrag.prototype._applyStart = function () { + var item = this._item; + if (!item._isActive) return; + + var grid = this._getGrid(); + var element = item._element; + var release = item._dragRelease; + var migrate = item._migrate; + var hasDragContainer = this._container !== grid._element; - // Stop current positioning animation. if (item.isPositioning()) { - item._layout.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); + item._layout.stop(true, this._left, this._top); } - // Stop current migration animation. if (migrate._isActive) { - currentLeft -= migrate._containerDiffX; - currentTop -= migrate._containerDiffY; - migrate.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); + this._left -= migrate._containerDiffX; + this._top -= migrate._containerDiffY; + this._gridX -= migrate._containerDiffX; + this._gridY -= migrate._containerDiffY; + migrate.stop(true, this._left, this._top); } - // If item is being released reset release data. - if (item.isReleasing()) release._reset(); - - // Setup drag data. - this._isActive = true; - this._dragEvent = event; - this._container = dragContainer; - this._containingBlock = containingBlock; - this._elementClientX = elementRect.left; - this._elementClientY = elementRect.top; - this._left = this._gridX = currentLeft; - this._top = this._gridY = currentTop; + if (item.isReleasing()) { + release._reset(); + } - // Create placeholder (if necessary). - if (settings.dragPlaceholder.enabled) { + if (grid._settings.dragPlaceholder.enabled) { item._dragPlaceholder.create(); } - // Emit dragInit event. - grid._emit(eventDragInit, item, event); + this._isStarted = true; - // If a specific drag container is set and it is different from the - // grid's container element we need to cast some extra spells. - if (hasDragContainer) { - // Store the container offset diffs to drag data. - this._containerDiffX = offsetDiff.left; - this._containerDiffY = offsetDiff.top; + grid._emit(EVENT_DRAG_INIT, item, this._dragStartEvent); + if (hasDragContainer) { // If the dragged element is a child of the drag container all we need to // do is setup the relative drag position data. - if (element.parentNode === dragContainer) { - this._gridX = currentLeft - this._containerDiffX; - this._gridY = currentTop - this._containerDiffY; + if (element.parentNode === this._container) { + this._gridX -= this._containerDiffX; + this._gridY -= this._containerDiffY; } - // Otherwise we need to append the element inside the correct container, // setup the actual drag position data and adjust the element's translate // values to account for the DOM position shift. else { - this._left = currentLeft + this._containerDiffX; - this._top = currentTop + this._containerDiffY; - dragContainer.appendChild(element); - element.style[transformProp] = getTranslateString(this._left, this._top); + this._left += this._containerDiffX; + this._top += this._containerDiffY; + this._container.appendChild(element); + item._setTranslate(this._left, this._top); } } - // Set drag class and bind scrollers. - addClass(element, settings.itemDraggingClass); + addClass(element, grid._settings.itemDraggingClass); this._bindScrollListeners(); - - // Emit dragStart event. - grid._emit(eventDragStart, item, event); + grid._emit(EVENT_DRAG_START, item, this._dragStartEvent); }; /** * Drag move handler. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ -ItemDrag.prototype._onMove = function(event) { +ItemDrag.prototype._onMove = function (event) { var item = this._item; - // If item is not active, reset drag. if (!item._isActive) { this.stop(); return; } + this._dragMoveEvent = event; + addDragMoveTick(item._id, this._prepareMove, this._applyMove); + addDragSortTick(item._id, this._handleSort); +}; + +/** + * Prepare dragged item for moving. + * + * @private + */ +ItemDrag.prototype._prepareMove = function () { + var item = this._item; + + if (!item._isActive) return; + var settings = this._getGrid()._settings; var axis = settings.dragAxis; + var nextEvent = this._dragMoveEvent; + var prevEvent = this._dragPrevMoveEvent || this._dragStartEvent || nextEvent; // Update horizontal position data. if (axis !== 'y') { - var xDiff = event.clientX - this._dragEvent.clientX; - this._left += xDiff; - this._gridX += xDiff; - this._elementClientX += xDiff; + var moveDiffX = nextEvent.clientX - prevEvent.clientX; + this._left = this._left - this._moveDiffX + moveDiffX; + this._gridX = this._gridX - this._moveDiffX + moveDiffX; + this._clientX = this._clientX - this._moveDiffX + moveDiffX; + this._moveDiffX = moveDiffX; } // Update vertical position data. if (axis !== 'x') { - var yDiff = event.clientY - this._dragEvent.clientY; - this._top += yDiff; - this._gridY += yDiff; - this._elementClientY += yDiff; + var moveDiffY = nextEvent.clientY - prevEvent.clientY; + this._top = this._top - this._moveDiffY + moveDiffY; + this._gridY = this._gridY - this._moveDiffY + moveDiffY; + this._clientY = this._clientY - this._moveDiffY + moveDiffY; + this._moveDiffY = moveDiffY; } - // Update event data. - this._dragEvent = event; - - // Do move prepare/apply handling in the next tick. - addMoveTick(item._id, this._prepareMove, this._applyMove); -}; - -/** - * Prepare dragged item for moving. - * - * @private - * @memberof ItemDrag.prototype - */ -ItemDrag.prototype._prepareMove = function() { - // Do nothing if item is not active. - if (!this._item._isActive) return; - - // If drag sort is enabled -> check overlap. - if (this._getGrid()._settings.dragSort) { - if (this._checkHeuristics(this._dragEvent)) { - this._checkOverlapDebounce(); - } - } + this._dragPrevMoveEvent = nextEvent; }; /** * Apply movement to dragged item. * * @private - * @memberof ItemDrag.prototype */ -ItemDrag.prototype._applyMove = function() { +ItemDrag.prototype._applyMove = function () { var item = this._item; - - // Do nothing if item is not active. if (!item._isActive) return; - // Update element's translateX/Y values. - item._element.style[transformProp] = getTranslateString(this._left, this._top); - - // Emit dragMove event. - this._getGrid()._emit(eventDragMove, item, this._dragEvent); + this._moveDiffX = this._moveDiffY = 0; + item._setTranslate(this._left, this._top); + this._getGrid()._emit(EVENT_DRAG_MOVE, item, this._dragMoveEvent); + ItemDrag.autoScroller.updateItem(item); }; /** * Drag scroll handler. * * @private - * @memberof ItemDrag.prototype - * @param {Event} event + * @param {Object} event */ -ItemDrag.prototype._onScroll = function(event) { +ItemDrag.prototype._onScroll = function (event) { var item = this._item; - // If item is not active, reset drag. if (!item._isActive) { this.stop(); return; } - // Update last scroll event. this._scrollEvent = event; - - // Do scroll prepare/apply handling in the next tick. - addScrollTick(item._id, this._prepareScroll, this._applyScroll); + addDragScrollTick(item._id, this._prepareScroll, this._applyScroll); + addDragSortTick(item._id, this._handleSort); }; /** * Prepare dragged item for scrolling. * * @private - * @memberof ItemDrag.prototype */ -ItemDrag.prototype._prepareScroll = function() { +ItemDrag.prototype._prepareScroll = function () { var item = this._item; // If item is not active do nothing. @@ -1187,71 +1364,64 @@ ItemDrag.prototype._prepareScroll = function() { var element = item._element; var grid = this._getGrid(); - var settings = grid._settings; - var axis = settings.dragAxis; var gridContainer = grid._element; - var offsetDiff; - - // Calculate element's rect and x/y diff. + var axis = grid._settings.dragAxis; + var moveX = axis !== 'y'; + var moveY = axis !== 'x'; var rect = element.getBoundingClientRect(); - var xDiff = this._elementClientX - rect.left; - var yDiff = this._elementClientY - rect.top; // Update container diff. if (this._container !== gridContainer) { - offsetDiff = getOffsetDiff(this._containingBlock, gridContainer); + var offsetDiff = getOffsetDiff(this._containingBlock, gridContainer); this._containerDiffX = offsetDiff.left; this._containerDiffY = offsetDiff.top; } // Update horizontal position data. - if (axis !== 'y') { - this._left += xDiff; - this._gridX = this._left - this._containerDiffX; + if (moveX) { + var scrollDiffX = this._clientX - this._moveDiffX - this._scrollDiffX - rect.left; + this._left = this._left - this._scrollDiffX + scrollDiffX; + this._scrollDiffX = scrollDiffX; } // Update vertical position data. - if (axis !== 'x') { - this._top += yDiff; - this._gridY = this._top - this._containerDiffY; + if (moveY) { + var scrollDiffY = this._clientY - this._moveDiffY - this._scrollDiffY - rect.top; + this._top = this._top - this._scrollDiffY + scrollDiffY; + this._scrollDiffY = scrollDiffY; } - // Overlap handling. - if (settings.dragSort) this._checkOverlapDebounce(); + // Update grid position. + this._gridX = this._left - this._containerDiffX; + this._gridY = this._top - this._containerDiffY; }; /** * Apply scroll to dragged item. * * @private - * @memberof ItemDrag.prototype */ -ItemDrag.prototype._applyScroll = function() { +ItemDrag.prototype._applyScroll = function () { var item = this._item; - - // If item is not active do nothing. if (!item._isActive) return; - // Update element's translateX/Y values. - item._element.style[transformProp] = getTranslateString(this._left, this._top); - - // Emit dragScroll event. - this._getGrid()._emit(eventDragScroll, item, this._scrollEvent); + this._scrollDiffX = this._scrollDiffY = 0; + item._setTranslate(this._left, this._top); + this._getGrid()._emit(EVENT_DRAG_SCROLL, item, this._scrollEvent); }; /** * Drag end handler. * * @private - * @memberof ItemDrag.prototype - * @param {DraggerEvent} event + * @param {Object} event */ -ItemDrag.prototype._onEnd = function(event) { +ItemDrag.prototype._onEnd = function (event) { var item = this._item; var element = item._element; var grid = this._getGrid(); var settings = grid._settings; - var release = item._release; + var release = item._dragRelease; // If item is not active, reset drag. if (!item._isActive) { @@ -1259,12 +1429,13 @@ ItemDrag.prototype._onEnd = function(event) { return; } - // Cancel queued move and scroll ticks. - cancelMoveTick(item._id); - cancelScrollTick(item._id); + // Cancel queued ticks. + cancelDragStartTick(item._id); + cancelDragMoveTick(item._id); + cancelDragScrollTick(item._id); - // Finish currently queued overlap check. - settings.dragSort && this._checkOverlapDebounce('finish'); + // Finish sort procedure (does final overlap check if needed). + this._finishSort(); // Remove scroll listeners. this._unbindScrollListeners(); @@ -1279,8 +1450,11 @@ ItemDrag.prototype._onEnd = function(event) { // Remove drag class name from element. removeClass(element, settings.itemDraggingClass); + // Stop auto-scroll. + ItemDrag.autoScroller.removeItem(item); + // Emit dragEnd event. - grid._emit(eventDragEnd, item, event); + grid._emit(EVENT_DRAG_END, item, event); // Finish up the migration process or start the release process. this._isMigrating ? this._finishMigration() : release.start(); @@ -1291,35 +1465,6 @@ ItemDrag.prototype._onEnd = function(event) { * *************** */ -/** - * Calculate how many percent the intersection area of two rectangles is from - * the maximum potential intersection area between the rectangles. - * - * @param {Rectangle} a - * @param {Rectangle} b - * @returns {Number} - * - A number between 0-100. - */ -function getRectOverlapScore(a, b) { - // Return 0 immediately if the rectangles do not overlap. - if ( - a.left + a.width <= b.left || - b.left + b.width <= a.left || - a.top + a.height <= b.top || - b.top + b.height <= a.top - ) { - return 0; - } - - // Calculate intersection area's width, height, max height and max width. - var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left); - var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top); - var maxWidth = Math.min(a.width, b.width); - var maxHeight = Math.min(a.height, b.height); - - return ((width * height) / (maxWidth * maxHeight)) * 100; -} - /** * Check if an element is an anchor element and open the href url if possible. * diff --git a/src/Item/ItemDragPlaceholder.js b/src/Item/ItemDragPlaceholder.js index 08f816b8..c5430f0c 100644 --- a/src/Item/ItemDragPlaceholder.js +++ b/src/Item/ItemDragPlaceholder.js @@ -4,11 +4,21 @@ * https://github.com/haltu/muuri/blob/master/LICENSE.md */ -import { addPlaceholderTick, cancelPlaceholderTick } from '../ticker'; - -import { eventBeforeSend, eventDragReleaseEnd, eventLayoutStart } from '../shared'; - -import ItemAnimate from '../Item/ItemAnimate'; +import { + addPlaceholderLayoutTick, + cancelPlaceholderLayoutTick, + addPlaceholderResizeTick, + cancelPlaceholderResizeTick, +} from '../ticker'; + +import { + EVENT_BEFORE_SEND, + EVENT_DRAG_RELEASE_END, + EVENT_LAYOUT_START, + EVENT_HIDE_START, +} from '../constants'; + +import Animator from '../Animator/Animator'; import addClass from '../utils/addClass'; import getTranslateString from '../utils/getTranslateString'; @@ -16,6 +26,7 @@ import getTranslate from '../utils/getTranslate'; import isFunction from '../utils/isFunction'; import setStyles from '../utils/setStyles'; import removeClass from '../utils/removeClass'; +import transformProp from '../utils/transformProp'; /** * Drag placeholder. @@ -25,25 +36,29 @@ import removeClass from '../utils/removeClass'; */ function ItemDragPlaceholder(item) { this._item = item; - this._animate = new ItemAnimate(); + this._animation = new Animator(); this._element = null; this._className = ''; this._didMigrate = false; this._resetAfterLayout = false; - this._currentLeft = 0; - this._currentTop = 0; - this._nextLeft = 0; - this._nextTop = 0; + this._left = 0; + this._top = 0; + this._transX = 0; + this._transY = 0; + this._nextTransX = 0; + this._nextTransY = 0; // Bind animation handlers. this._setupAnimation = this._setupAnimation.bind(this); this._startAnimation = this._startAnimation.bind(this); + this._updateDimensions = this._updateDimensions.bind(this); // Bind event handlers. this._onLayoutStart = this._onLayoutStart.bind(this); this._onLayoutEnd = this._onLayoutEnd.bind(this); this._onReleaseEnd = this._onReleaseEnd.bind(this); this._onMigrate = this._onMigrate.bind(this); + this._onHide = this._onHide.bind(this); } /** @@ -51,47 +66,67 @@ function ItemDragPlaceholder(item) { * ************************* */ +/** + * Update placeholder's dimensions to match the item's dimensions. + * + * @private + */ +ItemDragPlaceholder.prototype._updateDimensions = function () { + if (!this.isActive()) return; + setStyles(this._element, { + width: this._item._width + 'px', + height: this._item._height + 'px', + }); +}; + /** * Move placeholder to a new position. * * @private - * @memberof ItemDragPlaceholder.prototype + * @param {Item[]} items + * @param {Boolean} isInstant */ -ItemDragPlaceholder.prototype._onLayoutStart = function() { +ItemDragPlaceholder.prototype._onLayoutStart = function (items, isInstant) { var item = this._item; - var grid = item.getGrid(); - // Find out the item's new (unapplied) left and top position. - var itemIndex = grid._items.indexOf(item); - var nextLeft = grid._layout.slots[itemIndex * 2]; - var nextTop = grid._layout.slots[itemIndex * 2 + 1]; + // If the item is not part of the layout anymore reset placeholder. + if (items.indexOf(item) === -1) { + this.reset(); + return; + } + + var nextLeft = item._left; + var nextTop = item._top; + var currentLeft = this._left; + var currentTop = this._top; + + // Keep track of item layout position. + this._left = nextLeft; + this._top = nextTop; - // If item's position did not change and the item did not migrate we can - // safely skip layout. - if (!this._didMigrate && item._left === nextLeft && item._top === nextTop) { + // If item's position did not change, and the item did not migrate and the + // layout is not instant and we can safely skip layout. + if (!isInstant && !this._didMigrate && currentLeft === nextLeft && currentTop === nextTop) { return; } // Slots data is calculated with item margins added to them so we need to add // item's left and top margin to the slot data to get the placeholder's // next position. - nextLeft += item._marginLeft; - nextTop += item._marginTop; + var nextX = nextLeft + item._marginLeft; + var nextY = nextTop + item._marginTop; // Just snap to new position without any animations if no animation is // required or if placeholder moves between grids. - var animEnabled = grid._settings.dragPlaceholder.duration > 0; + var grid = item.getGrid(); + var animEnabled = !isInstant && grid._settings.layoutDuration > 0; if (!animEnabled || this._didMigrate) { // Cancel potential (queued) layout tick. - cancelPlaceholderTick(item._id); + cancelPlaceholderLayoutTick(item._id); // Snap placeholder to correct position. - var targetStyles = { transform: getTranslateString(nextLeft, nextTop) }; - if (this._animate.isAnimating()) { - this._animate.stop(targetStyles); - } else { - setStyles(this._element, targetStyles); - } + this._element.style[transformProp] = getTranslateString(nextX, nextY); + this._animation.stop(); // Move placeholder inside correct container after migration. if (this._didMigrate) { @@ -104,55 +139,58 @@ ItemDragPlaceholder.prototype._onLayoutStart = function() { // Start the placeholder's layout animation in the next tick. We do this to // avoid layout thrashing. - this._nextLeft = nextLeft; - this._nextTop = nextTop; - addPlaceholderTick(item._id, this._setupAnimation, this._startAnimation); + this._nextTransX = nextX; + this._nextTransY = nextY; + addPlaceholderLayoutTick(item._id, this._setupAnimation, this._startAnimation); }; /** * Prepare placeholder for layout animation. * * @private - * @memberof ItemDragPlaceholder.prototype */ -ItemDragPlaceholder.prototype._setupAnimation = function() { +ItemDragPlaceholder.prototype._setupAnimation = function () { if (!this.isActive()) return; var translate = getTranslate(this._element); - this._currentLeft = translate.x; - this._currentTop = translate.y; + this._transX = translate.x; + this._transY = translate.y; }; /** * Start layout animation. * * @private - * @memberof ItemDragPlaceholder.prototype */ -ItemDragPlaceholder.prototype._startAnimation = function() { +ItemDragPlaceholder.prototype._startAnimation = function () { if (!this.isActive()) return; - var animation = this._animate; - var currentLeft = this._currentLeft; - var currentTop = this._currentTop; - var nextLeft = this._nextLeft; - var nextTop = this._nextTop; - var targetStyles = { transform: getTranslateString(nextLeft, nextTop) }; + var animation = this._animation; + var currentX = this._transX; + var currentY = this._transY; + var nextX = this._nextTransX; + var nextY = this._nextTransY; // If placeholder is already in correct position let's just stop animation // and be done with it. - if (currentLeft === nextLeft && currentTop === nextTop) { - if (animation.isAnimating()) animation.stop(targetStyles); + if (currentX === nextX && currentY === nextY) { + if (animation.isAnimating()) { + this._element.style[transformProp] = getTranslateString(nextX, nextY); + animation.stop(); + } return; } // Otherwise let's start the animation. - var settings = this._item.getGrid()._settings.dragPlaceholder; - var currentStyles = { transform: getTranslateString(currentLeft, currentTop) }; + var settings = this._item.getGrid()._settings; + var currentStyles = {}; + var targetStyles = {}; + currentStyles[transformProp] = getTranslateString(currentX, currentY); + targetStyles[transformProp] = getTranslateString(nextX, nextY); animation.start(currentStyles, targetStyles, { - duration: settings.duration, - easing: settings.easing, - onFinish: this._onLayoutEnd + duration: settings.layoutDuration, + easing: settings.layoutEasing, + onFinish: this._onLayoutEnd, }); }; @@ -160,9 +198,8 @@ ItemDragPlaceholder.prototype._startAnimation = function() { * Layout end handler. * * @private - * @memberof ItemDragPlaceholder.prototype */ -ItemDragPlaceholder.prototype._onLayoutEnd = function() { +ItemDragPlaceholder.prototype._onLayoutEnd = function () { if (this._resetAfterLayout) { this.reset(); } @@ -173,13 +210,12 @@ ItemDragPlaceholder.prototype._onLayoutEnd = function() { * emitted and receives the event data as it's argument. * * @private - * @memberof ItemDragPlaceholder.prototype * @param {Item} item */ -ItemDragPlaceholder.prototype._onReleaseEnd = function(item) { +ItemDragPlaceholder.prototype._onReleaseEnd = function (item) { if (item._id === this._item._id) { // If the placeholder is not animating anymore we can safely reset it. - if (!this._animate.isAnimating()) { + if (!this._animation.isAnimating()) { this.reset(); return; } @@ -195,7 +231,6 @@ ItemDragPlaceholder.prototype._onReleaseEnd = function(item) { * emitted and receives the event data as it's argument. * * @private - * @memberof ItemDragPlaceholder.prototype * @param {Object} data * @param {Item} data.item * @param {Grid} data.fromGrid @@ -203,7 +238,7 @@ ItemDragPlaceholder.prototype._onReleaseEnd = function(item) { * @param {Grid} data.toGrid * @param {Number} data.toIndex */ -ItemDragPlaceholder.prototype._onMigrate = function(data) { +ItemDragPlaceholder.prototype._onMigrate = function (data) { // Make sure we have a matching item. if (data.item !== this._item) return; @@ -211,19 +246,31 @@ ItemDragPlaceholder.prototype._onMigrate = function(data) { var nextGrid = data.toGrid; // Unbind listeners from current grid. - grid.off(eventDragReleaseEnd, this._onReleaseEnd); - grid.off(eventLayoutStart, this._onLayoutStart); - grid.off(eventBeforeSend, this._onMigrate); + grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + grid.off(EVENT_LAYOUT_START, this._onLayoutStart); + grid.off(EVENT_BEFORE_SEND, this._onMigrate); + grid.off(EVENT_HIDE_START, this._onHide); // Bind listeners to the next grid. - nextGrid.on(eventDragReleaseEnd, this._onReleaseEnd); - nextGrid.on(eventLayoutStart, this._onLayoutStart); - nextGrid.on(eventBeforeSend, this._onMigrate); + nextGrid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + nextGrid.on(EVENT_LAYOUT_START, this._onLayoutStart); + nextGrid.on(EVENT_BEFORE_SEND, this._onMigrate); + nextGrid.on(EVENT_HIDE_START, this._onHide); // Mark the item as migrated. this._didMigrate = true; }; +/** + * Reset placeholder if the associated item is hidden. + * + * @private + * @param {Item[]} items + */ +ItemDragPlaceholder.prototype._onHide = function (items) { + if (items.indexOf(this._item) > -1) this.reset(); +}; + /** * Public prototype methods * ************************ @@ -235,9 +282,8 @@ ItemDragPlaceholder.prototype._onMigrate = function(data) { * thrashing when it's called at the end of the drag start procedure. * * @public - * @memberof ItemDragPlaceholder.prototype */ -ItemDragPlaceholder.prototype.create = function() { +ItemDragPlaceholder.prototype.create = function () { // If we already have placeholder set up we can skip the initiation logic. if (this.isActive()) { this._resetAfterLayout = false; @@ -247,14 +293,18 @@ ItemDragPlaceholder.prototype.create = function() { var item = this._item; var grid = item.getGrid(); var settings = grid._settings; - var animation = this._animate; + var animation = this._animation; + + // Keep track of layout position. + this._left = item._left; + this._top = item._top; // Create placeholder element. var element; if (isFunction(settings.dragPlaceholder.createElement)) { element = settings.dragPlaceholder.createElement(item); } else { - element = window.document.createElement('div'); + element = document.createElement('div'); } this._element = element; @@ -267,23 +317,26 @@ ItemDragPlaceholder.prototype.create = function() { addClass(element, this._className); } - // Position the placeholder item correctly. - var left = item._left + item._marginLeft; - var top = item._top + item._marginTop; + // Set initial styles. setStyles(element, { - display: 'block', position: 'absolute', - left: '0', - top: '0', + left: '0px', + top: '0px', width: item._width + 'px', height: item._height + 'px', - transform: getTranslateString(left, top) }); + // Set initial position. + element.style[transformProp] = getTranslateString( + item._left + item._marginLeft, + item._top + item._marginTop + ); + // Bind event listeners. - grid.on(eventLayoutStart, this._onLayoutStart); - grid.on(eventDragReleaseEnd, this._onReleaseEnd); - grid.on(eventBeforeSend, this._onMigrate); + grid.on(EVENT_LAYOUT_START, this._onLayoutStart); + grid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + grid.on(EVENT_BEFORE_SEND, this._onMigrate); + grid.on(EVENT_HIDE_START, this._onHide); // onCreate hook. if (isFunction(settings.dragPlaceholder.onCreate)) { @@ -298,31 +351,32 @@ ItemDragPlaceholder.prototype.create = function() { * Reset placeholder data. * * @public - * @memberof ItemDragPlaceholder.prototype */ -ItemDragPlaceholder.prototype.reset = function() { +ItemDragPlaceholder.prototype.reset = function () { if (!this.isActive()) return; var element = this._element; var item = this._item; var grid = item.getGrid(); var settings = grid._settings; - var animation = this._animate; + var animation = this._animation; // Reset flag. this._resetAfterLayout = false; // Cancel potential (queued) layout tick. - cancelPlaceholderTick(item._id); + cancelPlaceholderLayoutTick(item._id); + cancelPlaceholderResizeTick(item._id); // Reset animation instance. animation.stop(); animation._element = null; // Unbind event listeners. - grid.off(eventDragReleaseEnd, this._onReleaseEnd); - grid.off(eventLayoutStart, this._onLayoutStart); - grid.off(eventBeforeSend, this._onMigrate); + grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); + grid.off(EVENT_LAYOUT_START, this._onLayoutStart); + grid.off(EVENT_BEFORE_SEND, this._onMigrate); + grid.off(EVENT_HIDE_START, this._onHide); // Remove placeholder class from the placeholder element. if (this._className) { @@ -343,43 +397,46 @@ ItemDragPlaceholder.prototype.reset = function() { }; /** - * Update placeholder's dimensions. + * Check if placeholder is currently active (visible). * * @public - * @memberof ItemDragPlaceholder.prototype - * @param {Number} width - * @param {height} height + * @returns {Boolean} */ -ItemDragPlaceholder.prototype.updateDimensions = function(width, height) { - if (this.isActive()) { - setStyles(this._element, { - width: width + 'px', - height: height + 'px' - }); - } +ItemDragPlaceholder.prototype.isActive = function () { + return !!this._element; }; /** - * Check if placeholder is currently active (visible). + * Get placeholder element. * * @public - * @memberof ItemDragPlaceholder.prototype - * @returns {Boolean} + * @returns {?HTMLElement} */ -ItemDragPlaceholder.prototype.isActive = function() { - return !!this._element; +ItemDragPlaceholder.prototype.getElement = function () { + return this._element; +}; + +/** + * Update placeholder's dimensions to match the item's dimensions. Note that + * the updating is done asynchronously in the next tick to avoid layout + * thrashing. + * + * @public + */ +ItemDragPlaceholder.prototype.updateDimensions = function () { + if (!this.isActive()) return; + addPlaceholderResizeTick(this._item._id, this._updateDimensions); }; /** * Destroy placeholder instance. * * @public - * @memberof ItemDragPlaceholder.prototype */ -ItemDragPlaceholder.prototype.destroy = function() { +ItemDragPlaceholder.prototype.destroy = function () { this.reset(); - this._animate.destroy(); - this._item = this._animate = null; + this._animation.destroy(); + this._item = this._animation = null; }; export default ItemDragPlaceholder; diff --git a/src/Item/ItemDragRelease.js b/src/Item/ItemDragRelease.js new file mode 100644 index 00000000..9441100d --- /dev/null +++ b/src/Item/ItemDragRelease.js @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +import { EVENT_DRAG_RELEASE_START, EVENT_DRAG_RELEASE_END } from '../constants'; + +import addClass from '../utils/addClass'; +import getTranslate from '../utils/getTranslate'; +import removeClass from '../utils/removeClass'; + +/** + * The release process handler constructor. Although this might seem as proper + * fit for the drag process this needs to be separated into it's own logic + * because there might be a scenario where drag is disabled, but the release + * process still needs to be implemented (dragging from a grid to another). + * + * @class + * @param {Item} item + */ +function ItemDragRelease(item) { + this._item = item; + this._isActive = false; + this._isDestroyed = false; + this._isPositioningStarted = false; + this._containerDiffX = 0; + this._containerDiffY = 0; +} + +/** + * Public prototype methods + * ************************ + */ + +/** + * Start the release process of an item. + * + * @public + */ +ItemDragRelease.prototype.start = function () { + if (this._isDestroyed || this._isActive) return; + + var item = this._item; + var grid = item.getGrid(); + var settings = grid._settings; + + this._isActive = true; + addClass(item._element, settings.itemReleasingClass); + if (!settings.dragRelease.useDragContainer) { + this._placeToGrid(); + } + grid._emit(EVENT_DRAG_RELEASE_START, item); + + // Let's start layout manually _only_ if there is no unfinished layout in + // about to finish. + if (!grid._nextLayoutData) item._layout.start(false); +}; + +/** + * End the release process of an item. This method can be used to abort an + * ongoing release process (animation) or finish the release process. + * + * @public + * @param {Boolean} [abort=false] + * - Should the release be aborted? When true, the release end event won't be + * emitted. Set to true only when you need to abort the release process + * while the item is animating to it's position. + * @param {Number} [left] + * - The element's current translateX value (optional). + * @param {Number} [top] + * - The element's current translateY value (optional). + */ +ItemDragRelease.prototype.stop = function (abort, left, top) { + if (this._isDestroyed || !this._isActive) return; + + var item = this._item; + var grid = item.getGrid(); + + if (!abort && (left === undefined || top === undefined)) { + left = item._left; + top = item._top; + } + + var didReparent = this._placeToGrid(left, top); + this._reset(didReparent); + + if (!abort) grid._emit(EVENT_DRAG_RELEASE_END, item); +}; + +ItemDragRelease.prototype.isJustReleased = function () { + return this._isActive && this._isPositioningStarted === false; +}; + +/** + * Destroy instance. + * + * @public + */ +ItemDragRelease.prototype.destroy = function () { + if (this._isDestroyed) return; + this.stop(true); + this._item = null; + this._isDestroyed = true; +}; + +/** + * Private prototype methods + * ************************* + */ + +/** + * Move the element back to the grid container element if it does not exist + * there already. + * + * @private + * @param {Number} [left] + * - The element's current translateX value (optional). + * @param {Number} [top] + * - The element's current translateY value (optional). + * @returns {Boolean} + * - Returns `true` if the element was reparented. + */ +ItemDragRelease.prototype._placeToGrid = function (left, top) { + if (this._isDestroyed) return; + + var item = this._item; + var element = item._element; + var container = item.getGrid()._element; + var didReparent = false; + + if (element.parentNode !== container) { + if (left === undefined || top === undefined) { + var translate = getTranslate(element); + left = translate.x - this._containerDiffX; + top = translate.y - this._containerDiffY; + } + + container.appendChild(element); + item._setTranslate(left, top); + didReparent = true; + } + + this._containerDiffX = 0; + this._containerDiffY = 0; + + return didReparent; +}; + +/** + * Reset data and remove releasing class. + * + * @private + * @param {Boolean} [needsReflow] + */ +ItemDragRelease.prototype._reset = function (needsReflow) { + if (this._isDestroyed) return; + + var item = this._item; + var releasingClass = item.getGrid()._settings.itemReleasingClass; + + this._isActive = false; + this._isPositioningStarted = false; + this._containerDiffX = 0; + this._containerDiffY = 0; + + // If the element was just reparented we need to do a forced reflow to remove + // the class gracefully. + if (releasingClass) { + // eslint-disable-next-line + if (needsReflow) item._element.clientWidth; + removeClass(item._element, releasingClass); + } +}; + +export default ItemDragRelease; diff --git a/src/Item/ItemLayout.js b/src/Item/ItemLayout.js index 8008ba3f..0e405686 100644 --- a/src/Item/ItemLayout.js +++ b/src/Item/ItemLayout.js @@ -6,37 +6,51 @@ import { addLayoutTick, cancelLayoutTick } from '../ticker'; -import Queue from '../Queue/Queue'; +import Animator from '../Animator/Animator'; import addClass from '../utils/addClass'; import getTranslate from '../utils/getTranslate'; import getTranslateString from '../utils/getTranslateString'; import isFunction from '../utils/isFunction'; import removeClass from '../utils/removeClass'; -import setStyles from '../utils/setStyles'; +import transformProp from '../utils/transformProp'; + +var MIN_ANIMATION_DISTANCE = 2; /** - * Layout manager for Item instance. + * Layout manager for Item instance, handles the positioning of an item. * * @class * @param {Item} item */ function ItemLayout(item) { + var element = item._element; + var elementStyle = element.style; + this._item = item; this._isActive = false; this._isDestroyed = false; this._isInterrupted = false; this._currentStyles = {}; this._targetStyles = {}; - this._currentLeft = 0; - this._currentTop = 0; + this._nextLeft = 0; + this._nextTop = 0; this._offsetLeft = 0; this._offsetTop = 0; this._skipNextAnimation = false; - this._animateOptions = { - onFinish: this._finish.bind(this) + this._animOptions = { + onFinish: this._finish.bind(this), + duration: 0, + easing: 0, }; - this._queue = new Queue(); + + // Set element's initial position styles. + elementStyle.left = '0px'; + elementStyle.top = '0px'; + item._setTranslate(0, 0); + + this._animation = new Animator(element); + this._queue = 'layout-' + item._id; // Bind animation handlers and finish method. this._setupAnimation = this._setupAnimation.bind(this); @@ -52,72 +66,68 @@ function ItemLayout(item) { * Start item layout based on it's current data. * * @public - * @memberof ItemLayout.prototype - * @param {Boolean} [instant=false] + * @param {Boolean} instant * @param {Function} [onFinish] - * @returns {ItemLayout} */ -ItemLayout.prototype.start = function(instant, onFinish) { +ItemLayout.prototype.start = function (instant, onFinish) { if (this._isDestroyed) return; var item = this._item; - var element = item._element; - var release = item._release; + var release = item._dragRelease; var gridSettings = item.getGrid()._settings; var isPositioning = this._isActive; - var isJustReleased = release._isActive && release._isPositioningStarted === false; + var isJustReleased = release.isJustReleased(); var animDuration = isJustReleased - ? gridSettings.dragReleaseDuration + ? gridSettings.dragRelease.duration : gridSettings.layoutDuration; - var animEasing = isJustReleased ? gridSettings.dragReleaseEasing : gridSettings.layoutEasing; + var animEasing = isJustReleased ? gridSettings.dragRelease.easing : gridSettings.layoutEasing; var animEnabled = !instant && !this._skipNextAnimation && animDuration > 0; - var isAnimating; - // If the item is currently positioning process current layout callback - // queue with interrupted flag on. - if (isPositioning) this._queue.flush(true, item); + // If the item is currently positioning cancel potential queued layout tick + // and process current layout callback queue with interrupted flag on. + if (isPositioning) { + cancelLayoutTick(item._id); + item._emitter.burst(this._queue, true, item); + } // Mark release positioning as started. if (isJustReleased) release._isPositioningStarted = true; // Push the callback to the callback queue. - if (isFunction(onFinish)) this._queue.add(onFinish); + if (isFunction(onFinish)) { + item._emitter.on(this._queue, onFinish); + } + + // Reset animation skipping flag. + this._skipNextAnimation = false; // If no animations are needed, easy peasy! if (!animEnabled) { this._updateOffsets(); - this._updateTargetStyles(); - isAnimating = item._animate.isAnimating(); - this.stop(false, this._targetStyles); - !isAnimating && setStyles(element, this._targetStyles); - this._skipNextAnimation = false; - return this._finish(); + item._setTranslate(this._nextLeft, this._nextTop); + this._animation.stop(); + this._finish(); + return; } - // Set item active and store some data for the animation that is about to be - // triggered. + // Kick off animation to be started in the next tick. this._isActive = true; - this._animateOptions.easing = animEasing; - this._animateOptions.duration = animDuration; + this._animOptions.easing = animEasing; + this._animOptions.duration = animDuration; this._isInterrupted = isPositioning; - - // Start the item's layout animation in the next tick. addLayoutTick(item._id, this._setupAnimation, this._startAnimation); - - return this; }; /** * Stop item's position animation if it is currently animating. * * @public - * @memberof ItemLayout.prototype - * @param {Boolean} [processCallbackQueue=false] - * @param {Object} [targetStyles] - * @returns {ItemLayout} + * @param {Boolean} processCallbackQueue + * @param {Number} [left] + * @param {Number} [top] */ -ItemLayout.prototype.stop = function(processCallbackQueue, targetStyles) { - if (this._isDestroyed || !this._isActive) return this; +ItemLayout.prototype.stop = function (processCallbackQueue, left, top) { + if (this._isDestroyed || !this._isActive) return; var item = this._item; @@ -125,7 +135,15 @@ ItemLayout.prototype.stop = function(processCallbackQueue, targetStyles) { cancelLayoutTick(item._id); // Stop animation. - item._animate.stop(targetStyles); + if (this._animation.isAnimating()) { + if (left === undefined || top === undefined) { + var translate = getTranslate(item._element); + left = translate.x; + top = translate.y; + } + item._setTranslate(left, top); + this._animation.stop(); + } // Remove positioning class. removeClass(item._element, item.getGrid()._settings.itemPositioningClass); @@ -134,25 +152,34 @@ ItemLayout.prototype.stop = function(processCallbackQueue, targetStyles) { this._isActive = false; // Process callback queue if needed. - if (processCallbackQueue) this._queue.flush(true, item); - - return this; + if (processCallbackQueue) { + item._emitter.burst(this._queue, true, item); + } }; /** * Destroy the instance and stop current animation if it is running. * * @public - * @memberof ItemLayout.prototype - * @returns {ItemLayout} */ -ItemLayout.prototype.destroy = function() { - if (this._isDestroyed) return this; - this.stop(true, {}); - this._queue.destroy(); - this._item = this._currentStyles = this._targetStyles = this._animateOptions = null; +ItemLayout.prototype.destroy = function () { + if (this._isDestroyed) return; + + var elementStyle = this._item._element.style; + + this.stop(true, 0, 0); + this._item._emitter.clear(this._queue); + this._animation.destroy(); + + elementStyle[transformProp] = ''; + elementStyle.left = ''; + elementStyle.top = ''; + + this._item = null; + this._currentStyles = null; + this._targetStyles = null; + this._animOptions = null; this._isDestroyed = true; - return this; }; /** @@ -164,14 +191,13 @@ ItemLayout.prototype.destroy = function() { * Calculate and update item's current layout offset data. * * @private - * @memberof ItemLayout.prototype */ -ItemLayout.prototype._updateOffsets = function() { +ItemLayout.prototype._updateOffsets = function () { if (this._isDestroyed) return; var item = this._item; var migrate = item._migrate; - var release = item._release; + var release = item._dragRelease; this._offsetLeft = release._isActive ? release._containerDiffX @@ -184,34 +210,26 @@ ItemLayout.prototype._updateOffsets = function() { : migrate._isActive ? migrate._containerDiffY : 0; -}; -/** - * Calculate and update item's layout target styles. - * - * @private - * @memberof ItemLayout.prototype - */ -ItemLayout.prototype._updateTargetStyles = function() { - if (this._isDestroyed) return; - this._targetStyles.transform = getTranslateString( - this._item._left + this._offsetLeft, - this._item._top + this._offsetTop - ); + this._nextLeft = this._item._left + this._offsetLeft; + this._nextTop = this._item._top + this._offsetTop; }; /** * Finish item layout procedure. * * @private - * @memberof ItemLayout.prototype */ -ItemLayout.prototype._finish = function() { +ItemLayout.prototype._finish = function () { if (this._isDestroyed) return; var item = this._item; var migrate = item._migrate; - var release = item._release; + var release = item._dragRelease; + + // Update internal translate values. + item._tX = this._nextLeft; + item._tY = this._nextTop; // Mark the item as inactive and remove positioning classes. if (this._isActive) { @@ -224,42 +242,46 @@ ItemLayout.prototype._finish = function() { if (migrate._isActive) migrate.stop(); // Process the callback queue. - this._queue.flush(false, item); + item._emitter.burst(this._queue, false, item); }; /** * Prepare item for layout animation. * * @private - * @memberof ItemLayout.prototype */ -ItemLayout.prototype._setupAnimation = function() { - var translate = getTranslate(this._item._element); - this._currentLeft = translate.x; - this._currentTop = translate.y; +ItemLayout.prototype._setupAnimation = function () { + var item = this._item; + if (item._tX === undefined || item._tY === undefined) { + var translate = getTranslate(item._element); + item._tX = translate.x; + item._tY = translate.y; + } }; /** * Start layout animation. * * @private - * @memberof ItemLayout.prototype */ -ItemLayout.prototype._startAnimation = function() { +ItemLayout.prototype._startAnimation = function () { var item = this._item; var settings = item.getGrid()._settings; + var isInstant = this._animOptions.duration <= 0; // Let's update the offset data and target styles. this._updateOffsets(); - this._updateTargetStyles(); - - // If the item is already in correct position let's quit early. - if ( - item._left === this._currentLeft - this._offsetLeft && - item._top === this._currentTop - this._offsetTop - ) { - if (this._isInterrupted) this.stop(false, this._targetStyles); - this._isActive = false; + + var xDiff = Math.abs(item._left - (item._tX - this._offsetLeft)); + var yDiff = Math.abs(item._top - (item._tY - this._offsetTop)); + + // If there is no need for animation or if the item is already in correct + // position (or near it) let's finish the process early. + if (isInstant || (xDiff < MIN_ANIMATION_DISTANCE && yDiff < MIN_ANIMATION_DISTANCE)) { + if (xDiff || yDiff || this._isInterrupted) { + item._setTranslate(this._nextLeft, this._nextTop); + } + this._animation.stop(); this._finish(); return; } @@ -269,11 +291,18 @@ ItemLayout.prototype._startAnimation = function() { addClass(item._element, settings.itemPositioningClass); } - // Get current styles for animation. - this._currentStyles.transform = getTranslateString(this._currentLeft, this._currentTop); + // Get current/next styles for animation. + this._currentStyles[transformProp] = getTranslateString(item._tX, item._tY); + this._targetStyles[transformProp] = getTranslateString(this._nextLeft, this._nextTop); + + // Set internal translation values to undefined for the duration of the + // animation since they will be changing on each animation frame for the + // duration of the animation and tracking them would mean reading the DOM on + // each frame, which is pretty darn expensive. + item._tX = item._tY = undefined; - // Animate. - item._animate.start(this._currentStyles, this._targetStyles, this._animateOptions); + // Start animation. + this._animation.start(this._currentStyles, this._targetStyles, this._animOptions); }; export default ItemLayout; diff --git a/src/Item/ItemMigrate.js b/src/Item/ItemMigrate.js index 4584502f..88a60226 100644 --- a/src/Item/ItemMigrate.js +++ b/src/Item/ItemMigrate.js @@ -4,21 +4,16 @@ * https://github.com/haltu/muuri/blob/master/LICENSE.md */ -import { eventBeforeSend, eventBeforeReceive, eventSend, eventReceive } from '../shared'; +import { EVENT_BEFORE_SEND, EVENT_BEFORE_RECEIVE, EVENT_SEND, EVENT_RECEIVE } from '../constants'; import ItemDrag from './ItemDrag'; import addClass from '../utils/addClass'; import getOffsetDiff from '../utils/getOffsetDiff'; import getTranslate from '../utils/getTranslate'; -import getTranslateString from '../utils/getTranslateString'; import arrayInsert from '../utils/arrayInsert'; import normalizeArrayIndex from '../utils/normalizeArrayIndex'; import removeClass from '../utils/removeClass'; -import setStyles from '../utils/setStyles'; -import { transformProp } from '../utils/supportedTransform'; - -var tempStyles = {}; /** * The migrate process handler constructor. @@ -45,17 +40,16 @@ function ItemMigrate(item) { * Start the migrate process of an item. * * @public - * @memberof ItemMigrate.prototype * @param {Grid} targetGrid - * @param {GridSingleItemQuery} position + * @param {(HTMLElement|Number|Item)} position * @param {HTMLElement} [container] - * @returns {ItemMigrate} */ -ItemMigrate.prototype.start = function(targetGrid, position, container) { - if (this._isDestroyed) return this; +ItemMigrate.prototype.start = function (targetGrid, position, container) { + if (this._isDestroyed) return; var item = this._item; var element = item._element; + var isActive = item.isActive(); var isVisible = item.isVisible(); var grid = item.getGrid(); var settings = grid._settings; @@ -63,7 +57,7 @@ ItemMigrate.prototype.start = function(targetGrid, position, container) { var targetElement = targetGrid._element; var targetItems = targetGrid._items; var currentIndex = grid._items.indexOf(item); - var targetContainer = container || window.document.body; + var targetContainer = container || document.body; var targetIndex; var targetItem; var currentContainer; @@ -72,14 +66,15 @@ ItemMigrate.prototype.start = function(targetGrid, position, container) { var translate; var translateX; var translateY; + var currentVisClass; + var nextVisClass; // Get target index. if (typeof position === 'number') { - targetIndex = normalizeArrayIndex(targetItems, position, true); + targetIndex = normalizeArrayIndex(targetItems, position, 1); } else { - targetItem = targetGrid._getItem(position); - /** @todo Consider throwing an error here instead of silently failing. */ - if (!targetItem) return this; + targetItem = targetGrid.getItem(position); + if (!targetItem) return; targetIndex = targetItems.indexOf(targetItem); } @@ -92,62 +87,64 @@ ItemMigrate.prototype.start = function(targetGrid, position, container) { // Abort current positioning. if (item.isPositioning()) { - item._layout.stop(true, { transform: getTranslateString(translateX, translateY) }); + item._layout.stop(true, translateX, translateY); } // Abort current migration. if (this._isActive) { translateX -= this._containerDiffX; translateY -= this._containerDiffY; - this.stop(true, { transform: getTranslateString(translateX, translateY) }); + this.stop(true, translateX, translateY); } // Abort current release. if (item.isReleasing()) { - translateX -= item._release._containerDiffX; - translateY -= item._release._containerDiffY; - item._release.stop(true, { transform: getTranslateString(translateX, translateY) }); + translateX -= item._dragRelease._containerDiffX; + translateY -= item._dragRelease._containerDiffY; + item._dragRelease.stop(true, translateX, translateY); } - // Stop current visibility animations. - item._visibility._stopAnimation(); + // Stop current visibility animation. + item._visibility.stop(true); // Destroy current drag. if (item._drag) item._drag.destroy(); - // Process current visibility animation queue. - item._visibility._queue.flush(true, item); - // Emit beforeSend event. - if (grid._hasListeners(eventBeforeSend)) { - grid._emit(eventBeforeSend, { + if (grid._hasListeners(EVENT_BEFORE_SEND)) { + grid._emit(EVENT_BEFORE_SEND, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } // Emit beforeReceive event. - if (targetGrid._hasListeners(eventBeforeReceive)) { - targetGrid._emit(eventBeforeReceive, { + if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) { + targetGrid._emit(EVENT_BEFORE_RECEIVE, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } - // Remove current classnames. - removeClass(element, settings.itemClass); - removeClass(element, settings.itemVisibleClass); - removeClass(element, settings.itemHiddenClass); + // Update item class. + if (settings.itemClass !== targetSettings.itemClass) { + removeClass(element, settings.itemClass); + addClass(element, targetSettings.itemClass); + } - // Add new classnames. - addClass(element, targetSettings.itemClass); - addClass(element, isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass); + // Update visibility class. + currentVisClass = isVisible ? settings.itemVisibleClass : settings.itemHiddenClass; + nextVisClass = isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass; + if (currentVisClass !== nextVisClass) { + removeClass(element, currentVisClass); + addClass(element, nextVisClass); + } // Move item instance from current grid to target grid. grid._items.splice(currentIndex, 1); @@ -156,71 +153,80 @@ ItemMigrate.prototype.start = function(targetGrid, position, container) { // Update item's grid id reference. item._gridId = targetGrid._id; - // Get current container. - currentContainer = element.parentNode; - - // Move the item inside the target container if it's different than the + // If item is active we need to move the item inside the target container for + // the duration of the (potential) animation if it's different than the // current container. - if (targetContainer !== currentContainer) { - targetContainer.appendChild(element); - offsetDiff = getOffsetDiff(targetContainer, currentContainer, true); - if (!translate) { - translate = getTranslate(element); - translateX = translate.x; - translateY = translate.y; + if (isActive) { + currentContainer = element.parentNode; + if (targetContainer !== currentContainer) { + targetContainer.appendChild(element); + offsetDiff = getOffsetDiff(targetContainer, currentContainer, true); + if (!translate) { + translate = getTranslate(element); + translateX = translate.x; + translateY = translate.y; + } + item._setTranslate(translateX + offsetDiff.left, translateY + offsetDiff.top); } - element.style[transformProp] = getTranslateString( - translateX + offsetDiff.left, - translateY + offsetDiff.top - ); + } + // If item is not active let's just append it to the target grid's element. + else { + targetElement.appendChild(element); } // Update child element's styles to reflect the current visibility state. - item._child.removeAttribute('style'); - setStyles(item._child, isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles); + item._visibility.setStyles( + isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles + ); - // Update display style. - element.style.display = isVisible ? 'block' : 'hidden'; - - // Get offset diff for the migration data. - containerDiff = getOffsetDiff(targetContainer, targetElement, true); + // Get offset diff for the migration data, if the item is active. + if (isActive) { + containerDiff = getOffsetDiff(targetContainer, targetElement, true); + } - // Update item's cached dimensions and sort data. + // Update item's cached dimensions. item._refreshDimensions(); - item._refreshSortData(); + + // Reset item's sort data. + item._sortData = null; // Create new drag handler. item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null; // Setup migration data. - this._isActive = true; - this._container = targetContainer; - this._containerDiffX = containerDiff.left; - this._containerDiffY = containerDiff.top; + if (isActive) { + this._isActive = true; + this._container = targetContainer; + this._containerDiffX = containerDiff.left; + this._containerDiffY = containerDiff.top; + } else { + this._isActive = false; + this._container = null; + this._containerDiffX = 0; + this._containerDiffY = 0; + } // Emit send event. - if (grid._hasListeners(eventSend)) { - grid._emit(eventSend, { + if (grid._hasListeners(EVENT_SEND)) { + grid._emit(EVENT_SEND, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } // Emit receive event. - if (targetGrid._hasListeners(eventReceive)) { - targetGrid._emit(eventReceive, { + if (targetGrid._hasListeners(EVENT_RECEIVE)) { + targetGrid._emit(EVENT_RECEIVE, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, - toIndex: targetIndex + toIndex: targetIndex, }); } - - return this; }; /** @@ -228,15 +234,15 @@ ItemMigrate.prototype.start = function(targetGrid, position, container) { * ongoing migrate process (animation) or finish the migrate process. * * @public - * @memberof ItemMigrate.prototype * @param {Boolean} [abort=false] * - Should the migration be aborted? - * @param {Object} [currentStyles] - * - Optional current translateX and translateY styles. - * @returns {ItemMigrate} + * @param {Number} [left] + * - The element's current translateX value (optional). + * @param {Number} [top] + * - The element's current translateY value (optional). */ -ItemMigrate.prototype.stop = function(abort, currentStyles) { - if (this._isDestroyed || !this._isActive) return this; +ItemMigrate.prototype.stop = function (abort, left, top) { + if (this._isDestroyed || !this._isActive) return; var item = this._item; var element = item._element; @@ -245,43 +251,37 @@ ItemMigrate.prototype.stop = function(abort, currentStyles) { var translate; if (this._container !== gridElement) { - if (!currentStyles) { + if (left === undefined || top === undefined) { if (abort) { translate = getTranslate(element); - tempStyles.transform = getTranslateString( - translate.x - this._containerDiffX, - translate.y - this._containerDiffY - ); + left = translate.x - this._containerDiffX; + top = translate.y - this._containerDiffY; } else { - tempStyles.transform = getTranslateString(item._left, item._top); + left = item._left; + top = item._top; } - currentStyles = tempStyles; } + gridElement.appendChild(element); - setStyles(element, currentStyles); + item._setTranslate(left, top); } this._isActive = false; this._container = null; this._containerDiffX = 0; this._containerDiffY = 0; - - return this; }; /** * Destroy instance. * * @public - * @memberof ItemMigrate.prototype - * @returns {ItemMigrate} */ -ItemMigrate.prototype.destroy = function() { - if (this._isDestroyed) return this; +ItemMigrate.prototype.destroy = function () { + if (this._isDestroyed) return; this.stop(true); this._item = null; this._isDestroyed = true; - return this; }; export default ItemMigrate; diff --git a/src/Item/ItemRelease.js b/src/Item/ItemRelease.js deleted file mode 100644 index e5672e2a..00000000 --- a/src/Item/ItemRelease.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright (c) 2015-present, Haltu Oy - * Released under the MIT license - * https://github.com/haltu/muuri/blob/master/LICENSE.md - */ - -import { eventDragReleaseStart, eventDragReleaseEnd } from '../shared'; - -import addClass from '../utils/addClass'; -import getTranslate from '../utils/getTranslate'; -import getTranslateString from '../utils/getTranslateString'; -import removeClass from '../utils/removeClass'; -import setStyles from '../utils/setStyles'; - -var tempStyles = {}; - -/** - * The release process handler constructor. Although this might seem as proper - * fit for the drag process this needs to be separated into it's own logic - * because there might be a scenario where drag is disabled, but the release - * process still needs to be implemented (dragging from a grid to another). - * - * @class - * @param {Item} item - */ -function ItemRelease(item) { - this._item = item; - this._isActive = false; - this._isDestroyed = false; - this._isPositioningStarted = false; - this._containerDiffX = 0; - this._containerDiffY = 0; -} - -/** - * Public prototype methods - * ************************ - */ - -/** - * Start the release process of an item. - * - * @public - * @memberof ItemRelease.prototype - * @returns {ItemRelease} - */ -ItemRelease.prototype.start = function() { - if (this._isDestroyed || this._isActive) return this; - - var item = this._item; - var grid = item.getGrid(); - - // Flag release as active. - this._isActive = true; - - // Add release class name to the released element. - addClass(item._element, grid._settings.itemReleasingClass); - - // Emit dragReleaseStart event. - grid._emit(eventDragReleaseStart, item); - - // Position the released item. - item._layout.start(false); - - return this; -}; - -/** - * End the release process of an item. This method can be used to abort an - * ongoing release process (animation) or finish the release process. - * - * @public - * @memberof ItemRelease.prototype - * @param {Boolean} [abort=false] - * - Should the release be aborted? When true, the release end event won't be - * emitted. Set to true only when you need to abort the release process - * while the item is animating to it's position. - * @param {Object} [currentStyles] - * - Optional current translateX and translateY styles. - * @returns {ItemRelease} - */ -ItemRelease.prototype.stop = function(abort, currentStyles) { - if (this._isDestroyed || !this._isActive) return this; - - var item = this._item; - var element = item._element; - var grid = item.getGrid(); - var container = grid._element; - var translate; - - // Reset data and remove releasing class name from the element. - this._reset(); - - // If the released element is outside the grid's container element put it - // back there and adjust position accordingly. - if (element.parentNode !== container) { - if (!currentStyles) { - if (abort) { - translate = getTranslate(element); - tempStyles.transform = getTranslateString( - translate.x - this._containerDiffX, - translate.y - this._containerDiffY - ); - } else { - tempStyles.transform = getTranslateString(item._left, item._top); - } - currentStyles = tempStyles; - } - container.appendChild(element); - setStyles(element, currentStyles); - } - - // Emit dragReleaseEnd event. - if (!abort) grid._emit(eventDragReleaseEnd, item); - - return this; -}; - -/** - * Destroy instance. - * - * @public - * @memberof ItemRelease.prototype - * @returns {ItemRelease} - */ -ItemRelease.prototype.destroy = function() { - if (this._isDestroyed) return this; - this.stop(true); - this._item = null; - this._isDestroyed = true; - return this; -}; - -/** - * Private prototype methods - * ************************* - */ - -/** - * Reset public data and remove releasing class. - * - * @private - * @memberof ItemRelease.prototype - */ -ItemRelease.prototype._reset = function() { - if (this._isDestroyed) return; - var item = this._item; - this._isActive = false; - this._isPositioningStarted = false; - this._containerDiffX = 0; - this._containerDiffY = 0; - removeClass(item._element, item.getGrid()._settings.itemReleasingClass); -}; - -export default ItemRelease; diff --git a/src/Item/ItemVisibility.js b/src/Item/ItemVisibility.js index b8bfc796..f89cd753 100644 --- a/src/Item/ItemVisibility.js +++ b/src/Item/ItemVisibility.js @@ -6,17 +6,16 @@ import { addVisibilityTick, cancelVisibilityTick } from '../ticker'; -import Queue from '../Queue/Queue'; +import Animator from '../Animator/Animator'; import addClass from '../utils/addClass'; import getCurrentStyles from '../utils/getCurrentStyles'; -import getTranslateString from '../utils/getTranslateString'; import isFunction from '../utils/isFunction'; import removeClass from '../utils/removeClass'; import setStyles from '../utils/setStyles'; /** - * Visibility manager for Item instance. + * Visibility manager for Item instance, handles visibility of an item. * * @class * @param {Item} item @@ -24,31 +23,28 @@ import setStyles from '../utils/setStyles'; function ItemVisibility(item) { var isActive = item._isActive; var element = item._element; + var childElement = element.children[0]; var settings = item.getGrid()._settings; + if (!childElement) { + throw new Error('No valid child element found within item element.'); + } + this._item = item; this._isDestroyed = false; - - // Set up visibility states. this._isHidden = !isActive; this._isHiding = false; this._isShowing = false; - - // Callback queue. - this._queue = new Queue(); - - // Bind show/hide finishers. + this._childElement = childElement; + this._currentStyleProps = []; + this._animation = new Animator(childElement); + this._queue = 'visibility-' + item._id; this._finishShow = this._finishShow.bind(this); this._finishHide = this._finishHide.bind(this); - // Force item to be either visible or hidden on init. - element.style.display = isActive ? 'block' : 'none'; - - // Set visible/hidden class. + element.style.display = isActive ? '' : 'none'; addClass(element, isActive ? settings.itemVisibleClass : settings.itemHiddenClass); - - // Set initial styles for the child element. - setStyles(item._child, isActive ? settings.visibleStyles : settings.hiddenStyles); + this.setStyles(isActive ? settings.visibleStyles : settings.hiddenStyles); } /** @@ -60,17 +56,14 @@ function ItemVisibility(item) { * Show item. * * @public - * @memberof ItemVisibility.prototype * @param {Boolean} instant * @param {Function} [onFinish] - * @returns {ItemVisibility} */ -ItemVisibility.prototype.show = function(instant, onFinish) { - if (this._isDestroyed) return this; +ItemVisibility.prototype.show = function (instant, onFinish) { + if (this._isDestroyed) return; var item = this._item; var element = item._element; - var queue = this._queue; var callback = isFunction(onFinish) ? onFinish : null; var grid = item.getGrid(); var settings = grid._settings; @@ -78,54 +71,49 @@ ItemVisibility.prototype.show = function(instant, onFinish) { // If item is visible call the callback and be done with it. if (!this._isShowing && !this._isHidden) { callback && callback(false, item); - return this; + return; } // If item is showing and does not need to be shown instantly, let's just // push callback to the callback queue and be done with it. if (this._isShowing && !instant) { - callback && queue.add(callback); - return this; + callback && item._emitter.on(this._queue, callback); + return; } // If the item is hiding or hidden process the current visibility callback // queue with the interrupted flag active, update classes and set display // to block if necessary. if (!this._isShowing) { - queue.flush(true, item); + item._emitter.burst(this._queue, true, item); removeClass(element, settings.itemHiddenClass); addClass(element, settings.itemVisibleClass); - if (!this._isHiding) element.style.display = 'block'; + if (!this._isHiding) element.style.display = ''; } // Push callback to the callback queue. - callback && queue.add(callback); + callback && item._emitter.on(this._queue, callback); // Update visibility states. - item._isActive = this._isShowing = true; + this._isShowing = true; this._isHiding = this._isHidden = false; // Finally let's start show animation. this._startAnimation(true, instant, this._finishShow); - - return this; }; /** * Hide item. * * @public - * @memberof ItemVisibility.prototype * @param {Boolean} instant * @param {Function} [onFinish] - * @returns {ItemVisibility} */ -ItemVisibility.prototype.hide = function(instant, onFinish) { - if (this._isDestroyed) return this; +ItemVisibility.prototype.hide = function (instant, onFinish) { + if (this._isDestroyed) return; var item = this._item; var element = item._element; - var queue = this._queue; var callback = isFunction(onFinish) ? onFinish : null; var grid = item.getGrid(); var settings = grid._settings; @@ -133,70 +121,98 @@ ItemVisibility.prototype.hide = function(instant, onFinish) { // If item is already hidden call the callback and be done with it. if (!this._isHiding && this._isHidden) { callback && callback(false, item); - return this; + return; } // If item is hiding and does not need to be hidden instantly, let's just // push callback to the callback queue and be done with it. if (this._isHiding && !instant) { - callback && queue.add(callback); - return this; + callback && item._emitter.on(this._queue, callback); + return; } // If the item is showing or visible process the current visibility callback // queue with the interrupted flag active, update classes and set display // to block if necessary. if (!this._isHiding) { - queue.flush(true, item); + item._emitter.burst(this._queue, true, item); addClass(element, settings.itemHiddenClass); removeClass(element, settings.itemVisibleClass); } // Push callback to the callback queue. - callback && queue.add(callback); + callback && item._emitter.on(this._queue, callback); // Update visibility states. this._isHidden = this._isHiding = true; - item._isActive = this._isShowing = false; + this._isShowing = false; // Finally let's start hide animation. this._startAnimation(false, instant, this._finishHide); +}; + +/** + * Stop current hiding/showing process. + * + * @public + * @param {Boolean} processCallbackQueue + */ +ItemVisibility.prototype.stop = function (processCallbackQueue) { + if (this._isDestroyed) return; + if (!this._isHiding && !this._isShowing) return; + + var item = this._item; + + cancelVisibilityTick(item._id); + this._animation.stop(); + if (processCallbackQueue) { + item._emitter.burst(this._queue, true, item); + } +}; - return this; +/** + * Reset all existing visibility styles and apply new visibility styles to the + * visibility element. This method should be used to set styles when there is a + * chance that the current style properties differ from the new ones (basically + * on init and on migrations). + * + * @public + * @param {Object} styles + */ +ItemVisibility.prototype.setStyles = function (styles) { + var childElement = this._childElement; + var currentStyleProps = this._currentStyleProps; + this._removeCurrentStyles(); + for (var prop in styles) { + currentStyleProps.push(prop); + childElement.style[prop] = styles[prop]; + } }; /** * Destroy the instance and stop current animation if it is running. * * @public - * @memberof ItemVisibility.prototype - * @returns {ItemVisibility} */ -ItemVisibility.prototype.destroy = function() { - if (this._isDestroyed) return this; +ItemVisibility.prototype.destroy = function () { + if (this._isDestroyed) return; var item = this._item; var element = item._element; var grid = item.getGrid(); - var queue = this._queue; var settings = grid._settings; - // Stop visibility animation. - this._stopAnimation({}); - - // Fire all uncompleted callbacks with interrupted flag and destroy the queue. - queue.flush(true, item).destroy(); - - // Remove visible/hidden classes. + this.stop(true); + item._emitter.clear(this._queue); + this._animation.destroy(); + this._removeCurrentStyles(); removeClass(element, settings.itemVisibleClass); removeClass(element, settings.itemHiddenClass); + element.style.display = ''; // Reset state. - this._item = null; this._isHiding = this._isShowing = false; this._isDestroyed = this._isHidden = true; - - return this; }; /** @@ -208,19 +224,20 @@ ItemVisibility.prototype.destroy = function() { * Start visibility animation. * * @private - * @memberof ItemVisibility.prototype * @param {Boolean} toVisible * @param {Boolean} [instant] * @param {Function} [onFinish] */ -ItemVisibility.prototype._startAnimation = function(toVisible, instant, onFinish) { +ItemVisibility.prototype._startAnimation = function (toVisible, instant, onFinish) { if (this._isDestroyed) return; var item = this._item; + var animation = this._animation; + var childElement = this._childElement; var settings = item.getGrid()._settings; var targetStyles = toVisible ? settings.visibleStyles : settings.hiddenStyles; - var duration = parseInt(toVisible ? settings.showDuration : settings.hideDuration) || 0; - var easing = (toVisible ? settings.showEasing : settings.hideEasing) || 'ease'; + var duration = toVisible ? settings.showDuration : settings.hideDuration; + var easing = toVisible ? settings.showEasing : settings.hideEasing; var isInstant = instant || duration <= 0; var currentStyles; @@ -235,11 +252,8 @@ ItemVisibility.prototype._startAnimation = function(toVisible, instant, onFinish // If we need to apply the styles instantly without animation. if (isInstant) { - if (item._animateChild.isAnimating()) { - item._animateChild.stop(targetStyles); - } else { - setStyles(item._child, targetStyles); - } + setStyles(childElement, targetStyles); + animation.stop(); onFinish && onFinish(); return; } @@ -247,60 +261,58 @@ ItemVisibility.prototype._startAnimation = function(toVisible, instant, onFinish // Start the animation in the next tick (to avoid layout thrashing). addVisibilityTick( item._id, - function() { - currentStyles = getCurrentStyles(item._child, targetStyles); + function () { + currentStyles = getCurrentStyles(childElement, targetStyles); }, - function() { - item._animateChild.start(currentStyles, targetStyles, { + function () { + animation.start(currentStyles, targetStyles, { duration: duration, easing: easing, - onFinish: onFinish + onFinish: onFinish, }); } ); }; -/** - * Stop visibility animation. - * - * @private - * @memberof ItemVisibility.prototype - * @param {Object} [targetStyles] - */ -ItemVisibility.prototype._stopAnimation = function(targetStyles) { - if (this._isDestroyed) return; - var item = this._item; - cancelVisibilityTick(item._id); - item._animateChild.stop(targetStyles); -}; - /** * Finish show procedure. * * @private - * @memberof ItemVisibility.prototype */ -ItemVisibility.prototype._finishShow = function() { +ItemVisibility.prototype._finishShow = function () { if (this._isHidden) return; this._isShowing = false; - this._queue.flush(false, this._item); + this._item._emitter.burst(this._queue, false, this._item); }; /** * Finish hide procedure. * * @private - * @memberof ItemVisibility.prototype */ -var finishStyles = {}; -ItemVisibility.prototype._finishHide = function() { +ItemVisibility.prototype._finishHide = function () { if (!this._isHidden) return; var item = this._item; this._isHiding = false; - finishStyles.transform = getTranslateString(0, 0); - item._layout.stop(true, finishStyles); + item._layout.stop(true, 0, 0); item._element.style.display = 'none'; - this._queue.flush(false, item); + item._emitter.burst(this._queue, false, item); +}; + +/** + * Remove currently applied visibility related inline style properties. + * + * @private + */ +ItemVisibility.prototype._removeCurrentStyles = function () { + var childElement = this._childElement; + var currentStyleProps = this._currentStyleProps; + + for (var i = 0; i < currentStyleProps.length; i++) { + childElement.style[currentStyleProps[i]] = ''; + } + + currentStyleProps.length = 0; }; export default ItemVisibility; diff --git a/src/Packer/Packer.js b/src/Packer/Packer.js index 93528c0a..f69a7fc0 100644 --- a/src/Packer/Packer.js +++ b/src/Packer/Packer.js @@ -5,487 +5,279 @@ * https://github.com/haltu/muuri/blob/master/src/Packer/LICENSE.md */ -/** - * This is the default layout algorithm for Muuri. Based on MAXRECTS approach - * as described by Jukka Jylänki in his survey: "A Thousand Ways to Pack the - * Bin - A Practical Approach to Two-Dimensional Rectangle Bin Packing.". - * - * @class - */ -function Packer() { - this._slots = []; - this._slotSizes = []; - this._freeSlots = []; - this._newSlots = []; - this._rectItem = {}; - this._rectStore = []; - this._rectId = 0; - - // The layout return data, which will be populated in getLayout. - this._layout = { - slots: null, - setWidth: false, - setHeight: false, - width: false, - height: false - }; - - // Bind sort handlers. - this._sortRectsLeftTop = this._sortRectsLeftTop.bind(this); - this._sortRectsTopLeft = this._sortRectsTopLeft.bind(this); -} +import PackerProcessor, { + createWorkerProcessors, + destroyWorkerProcessors, + isWorkerProcessorsSupported, +} from './PackerProcessor'; + +export var FILL_GAPS = 1; +export var HORIZONTAL = 2; +export var ALIGN_RIGHT = 4; +export var ALIGN_BOTTOM = 8; +export var ROUNDING = 16; +export var PACKET_INDEX_ID = 0; +export var PACKET_INDEX_WIDTH = 1; +export var PACKET_INDEX_HEIGHT = 2; +export var PACKET_INDEX_OPTIONS = 3; +export var PACKET_HEADER_SLOTS = 4; /** - * @public - * @memberof Packer.prototype - * @param {Item[]} items - * @param {Number} width - * @param {Number} height - * @param {Number[]} [slots] + * @class + * @param {Number} [numWorkers=0] * @param {Object} [options] * @param {Boolean} [options.fillGaps=false] * @param {Boolean} [options.horizontal=false] * @param {Boolean} [options.alignRight=false] * @param {Boolean} [options.alignBottom=false] - * @returns {LayoutData} + * @param {Boolean} [options.rounding=false] */ -Packer.prototype.getLayout = function(items, width, height, slots, options) { - var layout = this._layout; - var fillGaps = !!(options && options.fillGaps); - var isHorizontal = !!(options && options.horizontal); - var alignRight = !!(options && options.alignRight); - var alignBottom = !!(options && options.alignBottom); - var rounding = !!(options && options.rounding); - var slotSizes = this._slotSizes; - var i; - - // Reset layout data. - layout.slots = slots ? slots : this._slots; - layout.width = isHorizontal ? 0 : rounding ? Math.round(width) : width; - layout.height = !isHorizontal ? 0 : rounding ? Math.round(height) : height; - layout.setWidth = isHorizontal; - layout.setHeight = !isHorizontal; - - // Make sure slots and slot size arrays are reset. - layout.slots.length = 0; - slotSizes.length = 0; - - // No need to go further if items do not exist. - if (!items.length) return layout; - - // Find slots for items. - for (i = 0; i < items.length; i++) { - this._addSlot(items[i], isHorizontal, fillGaps, rounding, alignRight || alignBottom); - } - - // If the alignment is set to right we need to adjust the results. - if (alignRight) { - for (i = 0; i < layout.slots.length; i = i + 2) { - layout.slots[i] = layout.width - (layout.slots[i] + slotSizes[i]); +function Packer(numWorkers, options) { + this._options = 0; + this._processor = null; + this._layoutQueue = []; + this._layouts = {}; + this._layoutCallbacks = {}; + this._layoutWorkers = {}; + this._layoutWorkerData = {}; + this._workers = []; + this._onWorkerMessage = this._onWorkerMessage.bind(this); + + // Set initial options. + this.setOptions(options); + + // Init the worker(s) or the processor if workers can't be used. + numWorkers = typeof numWorkers === 'number' ? Math.max(0, numWorkers) : 0; + if (numWorkers && isWorkerProcessorsSupported()) { + try { + this._workers = createWorkerProcessors(numWorkers, this._onWorkerMessage); + } catch (e) { + this._processor = new PackerProcessor(); } + } else { + this._processor = new PackerProcessor(); } +} - // If the alignment is set to bottom we need to adjust the results. - if (alignBottom) { - for (i = 1; i < layout.slots.length; i = i + 2) { - layout.slots[i] = layout.height - (layout.slots[i] + slotSizes[i]); - } - } +Packer.prototype._sendToWorker = function () { + if (!this._layoutQueue.length || !this._workers.length) return; - // Reset slots arrays and rect id. - slotSizes.length = 0; - this._freeSlots.length = 0; - this._newSlots.length = 0; - this._rectId = 0; + var layoutId = this._layoutQueue.shift(); + var worker = this._workers.pop(); + var data = this._layoutWorkerData[layoutId]; - return layout; + delete this._layoutWorkerData[layoutId]; + this._layoutWorkers[layoutId] = worker; + worker.postMessage(data.buffer, [data.buffer]); }; -/** - * Calculate position for the layout item. Returns the left and top position - * of the item in pixels. - * - * @private - * @memberof Packer.prototype - * @param {Item} item - * @param {Boolean} isHorizontal - * @param {Boolean} fillGaps - * @param {Boolean} rounding - * @returns {Array} - */ -Packer.prototype._addSlot = (function() { - var eps = 0.001; - var itemSlot = {}; - return function(item, isHorizontal, fillGaps, rounding, trackSize) { - var layout = this._layout; - var freeSlots = this._freeSlots; - var newSlots = this._newSlots; - var rect; - var rectId; - var potentialSlots; - var ignoreCurrentSlots; - var i; - var ii; - - // Reset new slots. - newSlots.length = 0; - - // Set item slot initial data. - itemSlot.left = null; - itemSlot.top = null; - itemSlot.width = item._width + item._marginLeft + item._marginRight; - itemSlot.height = item._height + item._marginTop + item._marginBottom; - - // Round item slot width and height if needed. - if (rounding) { - itemSlot.width = Math.round(itemSlot.width); - itemSlot.height = Math.round(itemSlot.height); - } - - // Try to find a slot for the item. - for (i = 0; i < freeSlots.length; i++) { - rectId = freeSlots[i]; - if (!rectId) continue; - rect = this._getRect(rectId); - if (itemSlot.width <= rect.width + eps && itemSlot.height <= rect.height + eps) { - itemSlot.left = rect.left; - itemSlot.top = rect.top; - break; - } - } - - // If no slot was found for the item. - if (itemSlot.left === null) { - // Position the item in to the bottom left (vertical mode) or top right - // (horizontal mode) of the grid. - itemSlot.left = !isHorizontal ? 0 : layout.width; - itemSlot.top = !isHorizontal ? layout.height : 0; - - // If gaps don't need filling do not add any current slots to the new - // slots array. - if (!fillGaps) { - ignoreCurrentSlots = true; - } - } +Packer.prototype._onWorkerMessage = function (msg) { + var data = new Float32Array(msg.data); + var layoutId = data[PACKET_INDEX_ID]; + var layout = this._layouts[layoutId]; + var callback = this._layoutCallbacks[layoutId]; + var worker = this._layoutWorkers[layoutId]; + + if (layout) delete this._layoutCallbacks[layoutId]; + if (callback) delete this._layoutCallbacks[layoutId]; + if (worker) delete this._layoutWorkers[layoutId]; + + if (layout && callback) { + layout.width = data[PACKET_INDEX_WIDTH]; + layout.height = data[PACKET_INDEX_HEIGHT]; + layout.slots = data.subarray(PACKET_HEADER_SLOTS, data.length); + this._finalizeLayout(layout); + callback(layout); + } - // In vertical mode, if the item's bottom overlaps the grid's bottom. - if (!isHorizontal && itemSlot.top + itemSlot.height > layout.height) { - // If item is not aligned to the left edge, create a new slot. - if (itemSlot.left > 0) { - newSlots.push(this._addRect(0, layout.height, itemSlot.left, Infinity)); - } - - // If item is not aligned to the right edge, create a new slot. - if (itemSlot.left + itemSlot.width < layout.width) { - newSlots.push( - this._addRect( - itemSlot.left + itemSlot.width, - layout.height, - layout.width - itemSlot.left - itemSlot.width, - Infinity - ) - ); - } - - // Update grid height. - layout.height = itemSlot.top + itemSlot.height; - } + if (worker) { + this._workers.push(worker); + this._sendToWorker(); + } +}; - // In horizontal mode, if the item's right overlaps the grid's right edge. - if (isHorizontal && itemSlot.left + itemSlot.width > layout.width) { - // If item is not aligned to the top, create a new slot. - if (itemSlot.top > 0) { - newSlots.push(this._addRect(layout.width, 0, Infinity, itemSlot.top)); - } - - // If item is not aligned to the bottom, create a new slot. - if (itemSlot.top + itemSlot.height < layout.height) { - newSlots.push( - this._addRect( - layout.width, - itemSlot.top + itemSlot.height, - Infinity, - layout.height - itemSlot.top - itemSlot.height - ) - ); - } - - // Update grid width. - layout.width = itemSlot.left + itemSlot.width; - } +Packer.prototype._finalizeLayout = function (layout) { + var grid = layout._grid; + var isHorizontal = layout._settings & HORIZONTAL; + var isBorderBox = grid._boxSizing === 'border-box'; - // Clean up the current slots making sure there are no old slots that - // overlap with the item. If an old slot overlaps with the item, split it - // into smaller slots if necessary. - for (i = fillGaps ? 0 : ignoreCurrentSlots ? freeSlots.length : i; i < freeSlots.length; i++) { - rectId = freeSlots[i]; - if (!rectId) continue; - rect = this._getRect(rectId); - potentialSlots = this._splitRect(rect, itemSlot); - for (ii = 0; ii < potentialSlots.length; ii++) { - rectId = potentialSlots[ii]; - rect = this._getRect(rectId); - // Let's make sure here that we have a big enough slot - // (width/height > 0.49px) and also let's make sure that the slot is - // within the boundaries of the grid. - if ( - rect.width > 0.49 && - rect.height > 0.49 && - ((!isHorizontal && rect.top < layout.height) || - (isHorizontal && rect.left < layout.width)) - ) { - newSlots.push(rectId); - } - } - } + delete layout._grid; + delete layout._settings; - // Sanitize new slots. - if (newSlots.length) { - this._purgeRects(newSlots).sort( - isHorizontal ? this._sortRectsLeftTop : this._sortRectsTopLeft - ); - } + layout.styles = {}; - // Update layout width/height. - if (isHorizontal) { - layout.width = Math.max(layout.width, itemSlot.left + itemSlot.width); - } else { - layout.height = Math.max(layout.height, itemSlot.top + itemSlot.height); - } - - // Add item slot data to layout slots (and store the slot size for later - // usage too if necessary). - layout.slots.push(itemSlot.left, itemSlot.top); - if (trackSize) this._slotSizes.push(itemSlot.width, itemSlot.height); + if (isHorizontal) { + layout.styles.width = + (isBorderBox ? layout.width + grid._borderLeft + grid._borderRight : layout.width) + 'px'; + } else { + layout.styles.height = + (isBorderBox ? layout.height + grid._borderTop + grid._borderBottom : layout.height) + 'px'; + } - // Free/new slots switcheroo! - this._freeSlots = newSlots; - this._newSlots = freeSlots; - }; -})(); + return layout; +}; /** - * Add a new rectangle to the rectangle store. Returns the id of the new - * rectangle. - * - * @private - * @memberof Packer.prototype - * @param {Number} left - * @param {Number} top - * @param {Number} width - * @param {Number} height - * @returns {RectId} + * @public + * @param {Object} [options] + * @param {Boolean} [options.fillGaps] + * @param {Boolean} [options.horizontal] + * @param {Boolean} [options.alignRight] + * @param {Boolean} [options.alignBottom] + * @param {Boolean} [options.rounding] */ -Packer.prototype._addRect = function(left, top, width, height) { - var rectId = ++this._rectId; - var rectStore = this._rectStore; +Packer.prototype.setOptions = function (options) { + if (!options) return; + + var fillGaps; + if (typeof options.fillGaps === 'boolean') { + fillGaps = options.fillGaps ? FILL_GAPS : 0; + } else { + fillGaps = this._options & FILL_GAPS; + } - rectStore[rectId] = left || 0; - rectStore[++this._rectId] = top || 0; - rectStore[++this._rectId] = width || 0; - rectStore[++this._rectId] = height || 0; + var horizontal; + if (typeof options.horizontal === 'boolean') { + horizontal = options.horizontal ? HORIZONTAL : 0; + } else { + horizontal = this._options & HORIZONTAL; + } - return rectId; -}; + var alignRight; + if (typeof options.alignRight === 'boolean') { + alignRight = options.alignRight ? ALIGN_RIGHT : 0; + } else { + alignRight = this._options & ALIGN_RIGHT; + } -/** - * Get rectangle data from the rectangle store by id. Optionally you can - * provide a target object where the rectangle data will be written in. By - * default an internal object is reused as a target object. - * - * @private - * @memberof Packer.prototype - * @param {RectId} id - * @param {Object} [target] - * @returns {Object} - */ -Packer.prototype._getRect = function(id, target) { - var rectItem = target ? target : this._rectItem; - var rectStore = this._rectStore; + var alignBottom; + if (typeof options.alignBottom === 'boolean') { + alignBottom = options.alignBottom ? ALIGN_BOTTOM : 0; + } else { + alignBottom = this._options & ALIGN_BOTTOM; + } - rectItem.left = rectStore[id] || 0; - rectItem.top = rectStore[++id] || 0; - rectItem.width = rectStore[++id] || 0; - rectItem.height = rectStore[++id] || 0; + var rounding; + if (typeof options.rounding === 'boolean') { + rounding = options.rounding ? ROUNDING : 0; + } else { + rounding = this._options & ROUNDING; + } - return rectItem; + this._options = fillGaps | horizontal | alignRight | alignBottom | rounding; }; /** - * Punch a hole into a rectangle and split the remaining area into smaller - * rectangles (4 at max). - * - * @private - * @memberof Packer.prototype - * @param {Rectangle} rect - * @param {Rectangle} hole - * @returns {RectId[]} + * @public + * @param {Grid} grid + * @param {Number} layoutId + * @param {Item[]} items + * @param {Number} width + * @param {Number} height + * @param {Function} callback + * @returns {?Function} */ -Packer.prototype._splitRect = (function() { - var results = []; - return function(rect, hole) { - // Reset old results. - results.length = 0; - - // If the rect does not overlap with the hole add rect to the return data - // as is. - if (!this._doRectsOverlap(rect, hole)) { - results.push(this._addRect(rect.left, rect.top, rect.width, rect.height)); - return results; - } +Packer.prototype.createLayout = function (grid, layoutId, items, width, height, callback) { + if (this._layouts[layoutId]) { + throw new Error('A layout with the provided id is currently being processed.'); + } - // Left split. - if (rect.left < hole.left) { - results.push(this._addRect(rect.left, rect.top, hole.left - rect.left, rect.height)); - } + var horizontal = this._options & HORIZONTAL; + var layout = { + id: layoutId, + items: items, + slots: null, + width: horizontal ? 0 : width, + height: !horizontal ? 0 : height, + // Temporary data, which will be removed before sending the layout data + // outside of Packer's context. + _grid: grid, + _settings: this._options, + }; - // Right split. - if (rect.left + rect.width > hole.left + hole.width) { - results.push( - this._addRect( - hole.left + hole.width, - rect.top, - rect.left + rect.width - (hole.left + hole.width), - rect.height - ) - ); - } + // If there are no items let's call the callback immediately. + if (!items.length) { + layout.slots = []; + this._finalizeLayout(layout); + callback(layout); + return; + } - // Top split. - if (rect.top < hole.top) { - results.push(this._addRect(rect.left, rect.top, rect.width, hole.top - rect.top)); - } + // Create layout synchronously if needed. + if (this._processor) { + layout.slots = window.Float32Array + ? new Float32Array(items.length * 2) + : new Array(items.length * 2); + this._processor.computeLayout(layout, layout._settings); + this._finalizeLayout(layout); + callback(layout); + return; + } - // Bottom split. - if (rect.top + rect.height > hole.top + hole.height) { - results.push( - this._addRect( - rect.left, - hole.top + hole.height, - rect.width, - rect.top + rect.height - (hole.top + hole.height) - ) - ); - } + // Worker data. + var data = new Float32Array(PACKET_HEADER_SLOTS + items.length * 2); + + // Worker data header. + data[PACKET_INDEX_ID] = layoutId; + data[PACKET_INDEX_WIDTH] = layout.width; + data[PACKET_INDEX_HEIGHT] = layout.height; + data[PACKET_INDEX_OPTIONS] = layout._settings; + + // Worker data items. + var i, j, item; + for (i = 0, j = PACKET_HEADER_SLOTS - 1, item; i < items.length; i++) { + item = items[i]; + data[++j] = item._width + item._marginLeft + item._marginRight; + data[++j] = item._height + item._marginTop + item._marginBottom; + } - return results; - }; -})(); + this._layoutQueue.push(layoutId); + this._layouts[layoutId] = layout; + this._layoutCallbacks[layoutId] = callback; + this._layoutWorkerData[layoutId] = data; -/** - * Check if two rectangles overlap. - * - * @private - * @memberof Packer.prototype - * @param {Rectangle} a - * @param {Rectangle} b - * @returns {Boolean} - */ -Packer.prototype._doRectsOverlap = function(a, b) { - return !( - a.left + a.width <= b.left || - b.left + b.width <= a.left || - a.top + a.height <= b.top || - b.top + b.height <= a.top - ); -}; + this._sendToWorker(); -/** - * Check if a rectangle is fully within another rectangle. - * - * @private - * @memberof Packer.prototype - * @param {Rectangle} a - * @param {Rectangle} b - * @returns {Boolean} - */ -Packer.prototype._isRectWithinRect = function(a, b) { - return ( - a.left >= b.left && - a.top >= b.top && - a.left + a.width <= b.left + b.width && - a.top + a.height <= b.top + b.height - ); + return this.cancelLayout.bind(this, layoutId); }; /** - * Loops through an array of rectangle ids and resets all that are fully - * within another rectangle in the array. Resetting in this case means that - * the rectangle id value is replaced with zero. - * - * @private - * @memberof Packer.prototype - * @param {RectId[]} rectIds - * @returns {RectId[]} + * @public + * @param {Number} layoutId */ -Packer.prototype._purgeRects = (function() { - var rectA = {}; - var rectB = {}; - return function(rectIds) { - var i = rectIds.length; - var ii; - - while (i--) { - ii = rectIds.length; - if (!rectIds[i]) continue; - this._getRect(rectIds[i], rectA); - while (ii--) { - if (!rectIds[ii] || i === ii) continue; - if (this._isRectWithinRect(rectA, this._getRect(rectIds[ii], rectB))) { - rectIds[i] = 0; - break; - } - } - } +Packer.prototype.cancelLayout = function (layoutId) { + var layout = this._layouts[layoutId]; + if (!layout) return; - return rectIds; - }; -})(); + delete this._layouts[layoutId]; + delete this._layoutCallbacks[layoutId]; -/** - * Sort rectangles with top-left gravity. - * - * @private - * @memberof Packer.prototype - * @param {RectId} aId - * @param {RectId} bId - * @returns {Number} - */ -Packer.prototype._sortRectsTopLeft = (function() { - var rectA = {}; - var rectB = {}; - return function(aId, bId) { - this._getRect(aId, rectA); - this._getRect(bId, rectB); - // prettier-ignore - return rectA.top < rectB.top ? -1 : - rectA.top > rectB.top ? 1 : - rectA.left < rectB.left ? -1 : - rectA.left > rectB.left ? 1 : 0; - }; -})(); + if (this._layoutWorkerData[layoutId]) { + delete this._layoutWorkerData[layoutId]; + var queueIndex = this._layoutQueue.indexOf(layoutId); + if (queueIndex > -1) this._layoutQueue.splice(queueIndex, 1); + } +}; /** - * Sort rectangles with left-top gravity. - * - * @private - * @memberof Packer.prototype - * @param {RectId} aId - * @param {RectId} bId - * @returns {Number} + * @public */ -Packer.prototype._sortRectsLeftTop = (function() { - var rectA = {}; - var rectB = {}; - return function(aId, bId) { - this._getRect(aId, rectA); - this._getRect(bId, rectB); - // prettier-ignore - return rectA.left < rectB.left ? -1 : - rectA.left > rectB.left ? 1 : - rectA.top < rectB.top ? -1 : - rectA.top > rectB.top ? 1 : 0; - }; -})(); +Packer.prototype.destroy = function () { + // Move all currently used workers back in the workers array. + for (var key in this._layoutWorkers) { + this._workers.push(this._layoutWorkers[key]); + } + + // Destroy all instance's workers. + destroyWorkerProcessors(this._workers); + + // Reset data. + this._workers.length = 0; + this._layoutQueue.length = 0; + this._layouts = {}; + this._layoutCallbacks = {}; + this._layoutWorkers = {}; + this._layoutWorkerData = {}; +}; export default Packer; diff --git a/src/Packer/PackerProcessor.js b/src/Packer/PackerProcessor.js new file mode 100644 index 00000000..645e7f93 --- /dev/null +++ b/src/Packer/PackerProcessor.js @@ -0,0 +1,596 @@ +/** + * Muuri Packer + * Copyright (c) 2016-present, Niklas Rämö + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/src/Packer/LICENSE.md + */ + +function createPackerProcessor(isWorker = false) { + var FILL_GAPS = 1; + var HORIZONTAL = 2; + var ALIGN_RIGHT = 4; + var ALIGN_BOTTOM = 8; + var ROUNDING = 16; + + var EPS = 0.001; + var MIN_SLOT_SIZE = 0.5; + + // Rounds number first to three decimal precision and then floors the result + // to two decimal precision. + // Math.floor(Math.round(number * 1000) / 10) / 100 + function roundNumber(number) { + return ((((number * 1000 + 0.5) << 0) / 10) << 0) / 100; + } + + /** + * @class + */ + function PackerProcessor() { + this.currentRects = []; + this.nextRects = []; + this.rectTarget = {}; + this.rectStore = []; + this.slotSizes = []; + this.rectId = 0; + this.slotIndex = -1; + this.slotData = { left: 0, top: 0, width: 0, height: 0 }; + this.sortRectsLeftTop = this.sortRectsLeftTop.bind(this); + this.sortRectsTopLeft = this.sortRectsTopLeft.bind(this); + } + + /** + * Takes a layout object as an argument and computes positions (slots) for the + * layout items. Also computes the final width and height of the layout. The + * provided layout object's slots array is mutated as well as the width and + * height properties. + * + * @param {Object} layout + * @param {Number} layout.width + * - The start (current) width of the layout in pixels. + * @param {Number} layout.height + * - The start (current) height of the layout in pixels. + * @param {(Item[]|Number[])} layout.items + * - List of Muuri.Item instances or a list of item dimensions + * (e.g [ item1Width, item1Height, item2Width, item2Height, ... ]). + * @param {(Array|Float32Array)} layout.slots + * - An Array/Float32Array instance which's length should equal to + * the amount of items times two. The position (width and height) of each + * item will be written into this array. + * @param {Number} settings + * - The layout's settings as bitmasks. + * @returns {Object} + */ + PackerProcessor.prototype.computeLayout = function (layout, settings) { + var items = layout.items; + var slots = layout.slots; + var fillGaps = !!(settings & FILL_GAPS); + var horizontal = !!(settings & HORIZONTAL); + var alignRight = !!(settings & ALIGN_RIGHT); + var alignBottom = !!(settings & ALIGN_BOTTOM); + var rounding = !!(settings & ROUNDING); + var isPreProcessed = typeof items[0] === 'number'; + var i, bump, item, slotWidth, slotHeight, slot; + + // No need to go further if items do not exist. + if (!items.length) return layout; + + // Compute slots for the items. + bump = isPreProcessed ? 2 : 1; + for (i = 0; i < items.length; i += bump) { + // If items are pre-processed it means that items array contains only + // the raw dimensions of the items. Otherwise we assume it is an array + // of normal Muuri items. + if (isPreProcessed) { + slotWidth = items[i]; + slotHeight = items[i + 1]; + } else { + item = items[i]; + slotWidth = item._width + item._marginLeft + item._marginRight; + slotHeight = item._height + item._marginTop + item._marginBottom; + } + + // If rounding is enabled let's round the item's width and height to + // make the layout algorithm a bit more stable. This has a performance + // cost so don't use this if not necessary. + if (rounding) { + slotWidth = roundNumber(slotWidth); + slotHeight = roundNumber(slotHeight); + } + + // Get slot data. + slot = this.computeNextSlot(layout, slotWidth, slotHeight, fillGaps, horizontal); + + // Update layout width/height. + if (horizontal) { + if (slot.left + slot.width > layout.width) { + layout.width = slot.left + slot.width; + } + } else { + if (slot.top + slot.height > layout.height) { + layout.height = slot.top + slot.height; + } + } + + // Add item slot data to layout slots. + slots[++this.slotIndex] = slot.left; + slots[++this.slotIndex] = slot.top; + + // Store the size too (for later usage) if needed. + if (alignRight || alignBottom) { + this.slotSizes.push(slot.width, slot.height); + } + } + + // If the alignment is set to right we need to adjust the results. + if (alignRight) { + for (i = 0; i < slots.length; i += 2) { + slots[i] = layout.width - (slots[i] + this.slotSizes[i]); + } + } + + // If the alignment is set to bottom we need to adjust the results. + if (alignBottom) { + for (i = 1; i < slots.length; i += 2) { + slots[i] = layout.height - (slots[i] + this.slotSizes[i]); + } + } + + // Reset stuff. + this.slotSizes.length = 0; + this.currentRects.length = 0; + this.nextRects.length = 0; + this.rectId = 0; + this.slotIndex = -1; + + return layout; + }; + + /** + * Calculate next slot in the layout. Returns a slot object with position and + * dimensions data. The returned object is reused between calls. + * + * @param {Object} layout + * @param {Number} slotWidth + * @param {Number} slotHeight + * @param {Boolean} fillGaps + * @param {Boolean} horizontal + * @returns {Object} + */ + PackerProcessor.prototype.computeNextSlot = function ( + layout, + slotWidth, + slotHeight, + fillGaps, + horizontal + ) { + var slot = this.slotData; + var currentRects = this.currentRects; + var nextRects = this.nextRects; + var ignoreCurrentRects = false; + var rect; + var rectId; + var shards; + var i; + var j; + + // Reset new slots. + nextRects.length = 0; + + // Set item slot initial data. + slot.left = null; + slot.top = null; + slot.width = slotWidth; + slot.height = slotHeight; + + // Try to find position for the slot from the existing free spaces in the + // layout. + for (i = 0; i < currentRects.length; i++) { + rectId = currentRects[i]; + if (!rectId) continue; + rect = this.getRect(rectId); + if (slot.width <= rect.width + EPS && slot.height <= rect.height + EPS) { + slot.left = rect.left; + slot.top = rect.top; + break; + } + } + + // If no position was found for the slot let's position the slot to + // the bottom left (in vertical mode) or top right (in horizontal mode) of + // the layout. + if (slot.left === null) { + if (horizontal) { + slot.left = layout.width; + slot.top = 0; + } else { + slot.left = 0; + slot.top = layout.height; + } + + // If gaps don't need filling let's throw away all the current free spaces + // (currentRects). + if (!fillGaps) { + ignoreCurrentRects = true; + } + } + + // In vertical mode, if the slot's bottom overlaps the layout's bottom. + if (!horizontal && slot.top + slot.height > layout.height + EPS) { + // If slot is not aligned to the left edge, create a new free space to the + // left of the slot. + if (slot.left > MIN_SLOT_SIZE) { + nextRects.push(this.addRect(0, layout.height, slot.left, Infinity)); + } + + // If slot is not aligned to the right edge, create a new free space to + // the right of the slot. + if (slot.left + slot.width < layout.width - MIN_SLOT_SIZE) { + nextRects.push( + this.addRect( + slot.left + slot.width, + layout.height, + layout.width - slot.left - slot.width, + Infinity + ) + ); + } + + // Update layout height. + layout.height = slot.top + slot.height; + } + + // In horizontal mode, if the slot's right overlaps the layout's right edge. + if (horizontal && slot.left + slot.width > layout.width + EPS) { + // If slot is not aligned to the top, create a new free space above the + // slot. + if (slot.top > MIN_SLOT_SIZE) { + nextRects.push(this.addRect(layout.width, 0, Infinity, slot.top)); + } + + // If slot is not aligned to the bottom, create a new free space below + // the slot. + if (slot.top + slot.height < layout.height - MIN_SLOT_SIZE) { + nextRects.push( + this.addRect( + layout.width, + slot.top + slot.height, + Infinity, + layout.height - slot.top - slot.height + ) + ); + } + + // Update layout width. + layout.width = slot.left + slot.width; + } + + // Clean up the current free spaces making sure none of them overlap with + // the slot. Split all overlapping free spaces into smaller shards that do + // not overlap with the slot. + if (!ignoreCurrentRects) { + if (fillGaps) i = 0; + for (; i < currentRects.length; i++) { + rectId = currentRects[i]; + if (!rectId) continue; + rect = this.getRect(rectId); + shards = this.splitRect(rect, slot); + for (j = 0; j < shards.length; j++) { + rectId = shards[j]; + rect = this.getRect(rectId); + // Make sure that the free space is within the boundaries of the + // layout. This routine is critical to the algorithm as it makes sure + // that there are no leftover spaces with infinite height/width. + // It's also essential that we don't compare values absolutely to each + // other but leave a little headroom (EPSILON) to get rid of false + // positives. + if ( + horizontal ? rect.left + EPS < layout.width - EPS : rect.top + EPS < layout.height - EPS + ) { + nextRects.push(rectId); + } + } + } + } + + // Sanitize and sort all the new free spaces that will be used in the next + // iteration. This procedure is critical to make the bin-packing algorithm + // work. The free spaces have to be in correct order in the beginning of the + // next iteration. + if (nextRects.length > 1) { + this.purgeRects(nextRects).sort(horizontal ? this.sortRectsLeftTop : this.sortRectsTopLeft); + } + + // Finally we need to make sure that `this.currentRects` points to + // `nextRects` array as that is used in the next iteration's beginning when + // we try to find a space for the next slot. + this.currentRects = nextRects; + this.nextRects = currentRects; + + return slot; + }; + + /** + * Add a new rectangle to the rectangle store. Returns the id of the new + * rectangle. + * + * @param {Number} left + * @param {Number} top + * @param {Number} width + * @param {Number} height + * @returns {Number} + */ + PackerProcessor.prototype.addRect = function (left, top, width, height) { + var rectId = ++this.rectId; + this.rectStore[rectId] = left || 0; + this.rectStore[++this.rectId] = top || 0; + this.rectStore[++this.rectId] = width || 0; + this.rectStore[++this.rectId] = height || 0; + return rectId; + }; + + /** + * Get rectangle data from the rectangle store by id. Optionally you can + * provide a target object where the rectangle data will be written in. By + * default an internal object is reused as a target object. + * + * @param {Number} id + * @param {Object} [target] + * @returns {Object} + */ + PackerProcessor.prototype.getRect = function (id, target) { + if (!target) target = this.rectTarget; + target.left = this.rectStore[id] || 0; + target.top = this.rectStore[++id] || 0; + target.width = this.rectStore[++id] || 0; + target.height = this.rectStore[++id] || 0; + return target; + }; + + /** + * Punch a hole into a rectangle and return the shards (1-4). + * + * @param {Object} rect + * @param {Object} hole + * @returns {Number[]} + */ + PackerProcessor.prototype.splitRect = (function () { + var shards = []; + var width = 0; + var height = 0; + return function (rect, hole) { + // Reset old shards. + shards.length = 0; + + // If the slot does not overlap with the hole add slot to the return data + // as is. Note that in this case we are eager to keep the slot as is if + // possible so we use the EPSILON in favour of that logic. + if ( + rect.left + rect.width <= hole.left + EPS || + hole.left + hole.width <= rect.left + EPS || + rect.top + rect.height <= hole.top + EPS || + hole.top + hole.height <= rect.top + EPS + ) { + shards.push(this.addRect(rect.left, rect.top, rect.width, rect.height)); + return shards; + } + + // Left split. + width = hole.left - rect.left; + if (width >= MIN_SLOT_SIZE) { + shards.push(this.addRect(rect.left, rect.top, width, rect.height)); + } + + // Right split. + width = rect.left + rect.width - (hole.left + hole.width); + if (width >= MIN_SLOT_SIZE) { + shards.push(this.addRect(hole.left + hole.width, rect.top, width, rect.height)); + } + + // Top split. + height = hole.top - rect.top; + if (height >= MIN_SLOT_SIZE) { + shards.push(this.addRect(rect.left, rect.top, rect.width, height)); + } + + // Bottom split. + height = rect.top + rect.height - (hole.top + hole.height); + if (height >= MIN_SLOT_SIZE) { + shards.push(this.addRect(rect.left, hole.top + hole.height, rect.width, height)); + } + + return shards; + }; + })(); + + /** + * Check if a rectangle is fully within another rectangle. + * + * @param {Object} a + * @param {Object} b + * @returns {Boolean} + */ + PackerProcessor.prototype.isRectAWithinRectB = function (a, b) { + return ( + a.left + EPS >= b.left && + a.top + EPS >= b.top && + a.left + a.width - EPS <= b.left + b.width && + a.top + a.height - EPS <= b.top + b.height + ); + }; + + /** + * Loops through an array of rectangle ids and resets all that are fully + * within another rectangle in the array. Resetting in this case means that + * the rectangle id value is replaced with zero. + * + * @param {Number[]} rectIds + * @returns {Number[]} + */ + PackerProcessor.prototype.purgeRects = (function () { + var rectA = {}; + var rectB = {}; + return function (rectIds) { + var i = rectIds.length; + var j; + + while (i--) { + j = rectIds.length; + if (!rectIds[i]) continue; + this.getRect(rectIds[i], rectA); + while (j--) { + if (!rectIds[j] || i === j) continue; + this.getRect(rectIds[j], rectB); + if (this.isRectAWithinRectB(rectA, rectB)) { + rectIds[i] = 0; + break; + } + } + } + + return rectIds; + }; + })(); + + /** + * Sort rectangles with top-left gravity. + * + * @param {Number} aId + * @param {Number} bId + * @returns {Number} + */ + PackerProcessor.prototype.sortRectsTopLeft = (function () { + var rectA = {}; + var rectB = {}; + return function (aId, bId) { + this.getRect(aId, rectA); + this.getRect(bId, rectB); + + return rectA.top < rectB.top && rectA.top + EPS < rectB.top + ? -1 + : rectA.top > rectB.top && rectA.top - EPS > rectB.top + ? 1 + : rectA.left < rectB.left && rectA.left + EPS < rectB.left + ? -1 + : rectA.left > rectB.left && rectA.left - EPS > rectB.left + ? 1 + : 0; + }; + })(); + + /** + * Sort rectangles with left-top gravity. + * + * @param {Number} aId + * @param {Number} bId + * @returns {Number} + */ + PackerProcessor.prototype.sortRectsLeftTop = (function () { + var rectA = {}; + var rectB = {}; + return function (aId, bId) { + this.getRect(aId, rectA); + this.getRect(bId, rectB); + return rectA.left < rectB.left && rectA.left + EPS < rectB.left + ? -1 + : rectA.left > rectB.left && rectA.left - EPS < rectB.left + ? 1 + : rectA.top < rectB.top && rectA.top + EPS < rectB.top + ? -1 + : rectA.top > rectB.top && rectA.top - EPS > rectB.top + ? 1 + : 0; + }; + })(); + + if (isWorker) { + var PACKET_INDEX_WIDTH = 1; + var PACKET_INDEX_HEIGHT = 2; + var PACKET_INDEX_OPTIONS = 3; + var PACKET_HEADER_SLOTS = 4; + var processor = new PackerProcessor(); + + self.onmessage = function (msg) { + var data = new Float32Array(msg.data); + var items = data.subarray(PACKET_HEADER_SLOTS, data.length); + var slots = new Float32Array(items.length); + var settings = data[PACKET_INDEX_OPTIONS]; + var layout = { + items: items, + slots: slots, + width: data[PACKET_INDEX_WIDTH], + height: data[PACKET_INDEX_HEIGHT], + }; + + // Compute the layout (width / height / slots). + processor.computeLayout(layout, settings); + + // Copy layout data to the return data. + data[PACKET_INDEX_WIDTH] = layout.width; + data[PACKET_INDEX_HEIGHT] = layout.height; + data.set(layout.slots, PACKET_HEADER_SLOTS); + + // Send layout back to the main thread. + postMessage(data.buffer, [data.buffer]); + }; + } + + return PackerProcessor; +} + +var PackerProcessor = createPackerProcessor(); +export default PackerProcessor; + +// +// WORKER UTILS +// + +var blobUrl = null; +var activeWorkers = []; + +export function createWorkerProcessors(amount, onmessage) { + var workers = []; + + if (amount > 0) { + if (!blobUrl) { + blobUrl = URL.createObjectURL( + new Blob(['(' + createPackerProcessor.toString() + ')(true)'], { + type: 'application/javascript', + }) + ); + } + + for (var i = 0, worker; i < amount; i++) { + worker = new Worker(blobUrl); + if (onmessage) worker.onmessage = onmessage; + workers.push(worker); + activeWorkers.push(worker); + } + } + + return workers; +} + +export function destroyWorkerProcessors(workers) { + var worker; + var index; + + for (var i = 0; i < workers.length; i++) { + worker = workers[i]; + worker.onmessage = null; + worker.onerror = null; + worker.onmessageerror = null; + worker.terminate(); + + index = activeWorkers.indexOf(worker); + if (index > -1) activeWorkers.splice(index, 1); + } + + if (blobUrl && !activeWorkers.length) { + URL.revokeObjectURL(blobUrl); + blobUrl = null; + } +} + +export function isWorkerProcessorsSupported() { + return !!(window.Worker && window.URL && window.Blob); +} diff --git a/src/Queue/Queue.js b/src/Queue/Queue.js deleted file mode 100644 index eea77d55..00000000 --- a/src/Queue/Queue.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Muuri Queue - * Copyright (c) 2018-present, Niklas Rämö - * Released under the MIT license - * https://github.com/haltu/muuri/blob/master/src/Queue/LICENSE.md - */ - -/** - * Queue constructor. - * - * @class - */ -function Queue() { - this._queue = []; - this._isDestroyed = false; -} - -/** - * Public prototype methods - * ************************ - */ - -/** - * Add callback to the queue. - * - * @public - * @memberof Queue.prototype - * @param {Function} callback - * @returns {Queue} - */ -Queue.prototype.add = function(callback) { - if (this._isDestroyed) return this; - this._queue.push(callback); - return this; -}; - -/** - * Process queue callbacks and reset the queue. - * - * @public - * @memberof Queue.prototype - * @param {*} arg1 - * @param {*} arg2 - * @returns {Queue} - */ -Queue.prototype.flush = function(arg1, arg2) { - if (this._isDestroyed) return this; - - var queue = this._queue; - var length = queue.length; - var i; - - // Quit early if the queue is empty. - if (!length) return this; - - var singleCallback = length === 1; - var snapshot = singleCallback ? queue[0] : queue.slice(0); - - // Reset queue. - queue.length = 0; - - // If we only have a single callback let's just call it. - if (singleCallback) { - snapshot(arg1, arg2); - return this; - } - - // If we have multiple callbacks, let's process them. - for (i = 0; i < length; i++) { - snapshot[i](arg1, arg2); - if (this._isDestroyed) break; - } - - return this; -}; - -/** - * Destroy Queue instance. - * - * @public - * @memberof Queue.prototype - * @returns {Queue} - */ -Queue.prototype.destroy = function() { - if (this._isDestroyed) return this; - - this._isDestroyed = true; - this._queue.length = 0; - - return this; -}; - -export default Queue; diff --git a/src/Ticker/Ticker.js b/src/Ticker/Ticker.js index 78ebe0ff..d206fe74 100644 --- a/src/Ticker/Ticker.js +++ b/src/Ticker/Ticker.js @@ -9,105 +9,86 @@ import raf from '../utils/raf'; /** * A ticker system for handling DOM reads and writes in an efficient way. - * Contains a read queue and a write queue that are processed on the next - * animation frame when needed. * * @class */ -function Ticker() { +function Ticker(numLanes) { this._nextStep = null; - - this._queue = []; - this._reads = {}; - this._writes = {}; - - this._batch = []; - this._batchReads = {}; - this._batchWrites = {}; - + this._lanes = []; + this._stepQueue = []; + this._stepCallbacks = {}; this._step = this._step.bind(this); -} - -Ticker.prototype.add = function(id, readOperation, writeOperation, isPrioritized) { - // First, let's check if an item has been added to the queues with the same id - // and if so -> remove it. - var currentIndex = this._queue.indexOf(id); - if (currentIndex > -1) this._queue[currentIndex] = undefined; - - // Add entry. - isPrioritized ? this._queue.unshift(id) : this._queue.push(id); - this._reads[id] = readOperation; - this._writes[id] = writeOperation; - - // Finally, let's kick-start the next tick if it is not running yet. - if (!this._nextStep) this._nextStep = raf(this._step); -}; - -Ticker.prototype.cancel = function(id) { - var currentIndex = this._queue.indexOf(id); - if (currentIndex > -1) { - this._queue[currentIndex] = undefined; - delete this._reads[id]; - delete this._writes[id]; + for (var i = 0; i < numLanes; i++) { + this._lanes.push(new TickerLane()); } -}; +} -Ticker.prototype._step = function() { - var queue = this._queue; - var reads = this._reads; - var writes = this._writes; - var batch = this._batch; - var batchReads = this._batchReads; - var batchWrites = this._batchWrites; - var length = queue.length; - var id; - var i; +Ticker.prototype._step = function (time) { + var lanes = this._lanes; + var stepQueue = this._stepQueue; + var stepCallbacks = this._stepCallbacks; + var i, j, id, laneQueue, laneCallbacks, laneIndices; - // Reset ticker. this._nextStep = null; - // Setup queues and callback placeholders. - for (i = 0; i < length; i++) { - id = queue[i]; - if (!id) continue; - - batch.push(id); - - batchReads[id] = reads[id]; - delete reads[id]; + for (i = 0; i < lanes.length; i++) { + laneQueue = lanes[i].queue; + laneCallbacks = lanes[i].callbacks; + laneIndices = lanes[i].indices; + for (j = 0; j < laneQueue.length; j++) { + id = laneQueue[j]; + if (!id) continue; + stepQueue.push(id); + stepCallbacks[id] = laneCallbacks[id]; + delete laneCallbacks[id]; + delete laneIndices[id]; + } + laneQueue.length = 0; + } - batchWrites[id] = writes[id]; - delete writes[id]; + for (i = 0; i < stepQueue.length; i++) { + id = stepQueue[i]; + if (stepCallbacks[id]) stepCallbacks[id](time); + delete stepCallbacks[id]; } - // Reset queue. - queue.length = 0; + stepQueue.length = 0; +}; - // Process read callbacks. - for (i = 0; i < length; i++) { - id = batch[i]; - if (batchReads[id]) { - batchReads[id](); - delete batchReads[id]; - } - } +Ticker.prototype.add = function (laneIndex, id, callback) { + this._lanes[laneIndex].add(id, callback); + if (!this._nextStep) this._nextStep = raf(this._step); +}; - // Process write callbacks. - for (i = 0; i < length; i++) { - id = batch[i]; - if (batchWrites[id]) { - batchWrites[id](); - delete batchWrites[id]; - } - } +Ticker.prototype.remove = function (laneIndex, id) { + this._lanes[laneIndex].remove(id); +}; - // Reset batch. - batch.length = 0; +/** + * A lane for ticker. + * + * @class + */ +function TickerLane() { + this.queue = []; + this.indices = {}; + this.callbacks = {}; +} - // Restart the ticker if needed. - if (!this._nextStep && queue.length) { - this._nextStep = raf(this._step); - } +TickerLane.prototype.add = function (id, callback) { + var index = this.indices[id]; + if (index !== undefined) this.queue[index] = undefined; + this.queue.push(id); + this.callbacks[id] = callback; + this.indices[id] = this.queue.length - 1; +}; + +TickerLane.prototype.remove = function (id) { + var index = this.indices[id]; + if (index === undefined) return; + this.queue[index] = undefined; + delete this.callbacks[id]; + delete this.indices[id]; }; export default Ticker; diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 00000000..1fb892d6 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +export var GRID_INSTANCES = {}; +export var ITEM_ELEMENT_MAP = typeof Map === 'function' ? new Map() : null; + +export var ACTION_SWAP = 'swap'; +export var ACTION_MOVE = 'move'; + +export var EVENT_SYNCHRONIZE = 'synchronize'; +export var EVENT_LAYOUT_START = 'layoutStart'; +export var EVENT_LAYOUT_END = 'layoutEnd'; +export var EVENT_LAYOUT_ABORT = 'layoutAbort'; +export var EVENT_ADD = 'add'; +export var EVENT_REMOVE = 'remove'; +export var EVENT_SHOW_START = 'showStart'; +export var EVENT_SHOW_END = 'showEnd'; +export var EVENT_HIDE_START = 'hideStart'; +export var EVENT_HIDE_END = 'hideEnd'; +export var EVENT_FILTER = 'filter'; +export var EVENT_SORT = 'sort'; +export var EVENT_MOVE = 'move'; +export var EVENT_SEND = 'send'; +export var EVENT_BEFORE_SEND = 'beforeSend'; +export var EVENT_RECEIVE = 'receive'; +export var EVENT_BEFORE_RECEIVE = 'beforeReceive'; +export var EVENT_DRAG_INIT = 'dragInit'; +export var EVENT_DRAG_START = 'dragStart'; +export var EVENT_DRAG_MOVE = 'dragMove'; +export var EVENT_DRAG_SCROLL = 'dragScroll'; +export var EVENT_DRAG_END = 'dragEnd'; +export var EVENT_DRAG_RELEASE_START = 'dragReleaseStart'; +export var EVENT_DRAG_RELEASE_END = 'dragReleaseEnd'; +export var EVENT_DESTROY = 'destroy'; + +export var HAS_TOUCH_EVENTS = 'ontouchstart' in window; +export var HAS_POINTER_EVENTS = !!window.PointerEvent; +export var HAS_MS_POINTER_EVENTS = !!window.navigator.msPointerEnabled; + +export var MAX_SAFE_FLOAT32_INTEGER = 16777216; diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 00000000..cbf5f24a --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,595 @@ +export interface StyleDeclaration { + [styleProperty: string]: string; +} + +export type EventListener = (...args: any[]) => any; + +export interface DraggerCssProps { + touchAction?: string; + userSelect?: string; + userDrag?: string; + tapHighlightColor?: string; + touchCallout?: string; + contentZooming?: string; +} + +export interface DraggerEvent { + type: 'start' | 'move' | 'end' | 'cancel'; + srcEvent: PointerEvent | TouchEvent | MouseEvent; + distance: number; + deltaX: number; + deltaY: number; + deltaTime: number; + isFirst: boolean; + isFinal: boolean; + pointerType: 'mouse' | 'pen' | 'touch'; + identifier: number; + screenX: number; + screenY: number; + clientX: number; + clientY: number; + pageX: number; + pageY: number; + target: HTMLElement; +} + +export interface DraggerStartEvent extends DraggerEvent { + type: 'start'; + distance: 0; + deltaX: 0; + deltaY: 0; + deltaTime: 0; + isFirst: true; + isFinal: false; +} + +export interface DraggerMoveEvent extends DraggerEvent { + type: 'move'; + isFirst: false; + isFinal: false; +} + +export interface DraggerEndEvent extends DraggerEvent { + type: 'end'; + isFirst: false; + isFinal: true; +} + +export interface DraggerCancelEvent extends DraggerEvent { + type: 'cancel'; + isFirst: false; + isFinal: true; +} + +export interface DraggerEvents { + start(event: DraggerStartEvent): any; + move(event: DraggerMoveEvent): any; + end(event: DraggerMoveEvent): any; + cancel(event: DraggerCancelEvent): any; +} + +export interface ScrollEvent extends Event { + type: 'scroll'; +} + +export interface GridEvents { + synchronize(): any; + layoutStart(items: Item[], isInstant: boolean): any; + layoutEnd(items: Item[]): any; + layoutAbort(items: Item[]): any; + add(items: Item[]): any; + remove(items: Item[], indices: number[]): any; + showStart(items: Item[]): any; + showEnd(items: Item[]): any; + hideStart(items: Item[]): any; + hideEnd(items: Item[]): any; + filter(shownItems: Item[], hiddenItems: Item[]): any; + sort(currentOrder: Item[], previousOrder: Item[]): any; + move(data: { item: Item; fromIndex: number; toIndex: number; action: 'move' | 'swap' }): any; + send(data: { item: Item; fromGrid: Grid; fromIndex: number; toGrid: Grid; toIndex: number }): any; + beforeSend(data: { + item: Item; + fromGrid: Grid; + fromIndex: number; + toGrid: Grid; + toIndex: number; + }): any; + receive(data: { + item: Item; + fromGrid: Grid; + fromIndex: number; + toGrid: Grid; + toIndex: number; + }): any; + beforeReceive(data: { + item: Item; + fromGrid: Grid; + fromIndex: number; + toGrid: Grid; + toIndex: number; + }): any; + dragInit(item: Item, event: DraggerStartEvent | DraggerMoveEvent): any; + dragStart(item: Item, event: DraggerStartEvent | DraggerMoveEvent): any; + dragMove(item: Item, event: DraggerMoveEvent): any; + dragScroll(item: Item, event: ScrollEvent): any; + dragEnd(item: Item, event: DraggerEndEvent | DraggerCancelEvent): any; + dragReleaseStart(item: Item): any; + dragReleaseEnd(item: Item): any; + destroy(): any; +} + +export interface LayoutData { + id: number; + items: Item[]; + slots: number[]; + styles?: StyleDeclaration | null; + [key: string]: any; +} + +export interface LayoutOptions { + fillGaps?: boolean; + horizontal?: boolean; + alignRight?: boolean; + alignBottom?: boolean; + rounding?: boolean; +} + +export type LayoutOnFinish = (items: Item[], isAborted: boolean) => any; + +export type LayoutFunctionCallback = (layout: LayoutData) => any; + +export type LayoutFunctionCancel = (...args: any[]) => any; + +export type LayoutFunction = ( + grid: Grid, + id: number, + items: Item[], + gridWidth: number, + gridHeight: number, + callback: LayoutFunctionCallback +) => void | undefined | LayoutFunctionCancel; + +export type SortDataGetter = (item: Item, element: HTMLElement) => any; + +export type DragStartPredicate = ( + item: Item, + event: DraggerStartEvent | DraggerMoveEvent | DraggerEndEvent | DraggerCancelEvent +) => boolean | undefined; + +export interface DragStartPredicateOptions { + distance?: number; + delay?: number; +} + +export type DragSortGetter = (this: Grid, item: Item) => Grid[] | null | void | undefined; + +export interface DragSortHeuristics { + sortInterval?: number; + minDragDistance?: number; + minBounceBackAngle?: number; +} + +export type DragSortPredicateResult = { + grid: Grid; + index: number; + action: 'move' | 'swap'; +} | null; + +export type DragSortPredicate = (item: Item, event: DraggerMoveEvent) => DragSortPredicateResult; + +export interface DragSortPredicateOptions { + threshold?: number; + action?: 'move' | 'swap'; + migrateAction?: 'move' | 'swap'; +} + +export interface DragReleaseOptions { + duration?: number; + easing?: string; + useDragContainer?: boolean; +} + +export type DragPlaceholderCreateElement = (item: Item) => HTMLElement; + +export type DragPlaceholderOnCreate = (item: Item, placeholderElement: HTMLElement) => any; + +export type DragPlaceholderOnRemove = (item: Item, placeholderElement: HTMLElement) => any; + +export interface DragPlaceholderOptions { + enabled?: boolean; + createElement?: DragPlaceholderCreateElement | null; + onCreate?: DragPlaceholderOnCreate | null; + onRemove?: DragPlaceholderOnRemove | null; +} + +export interface DragAutoScrollTarget { + element: Window | HTMLElement; + axis?: number; + priority?: number; + threshold?: number; +} + +export type DragAutoScrollTargets = Array; + +export type DragAutoScrollTargetsGetter = (item: Item) => DragAutoScrollTargets; + +export type DragAutoScrollOnStart = ( + item: Item, + scrollElement: Window | HTMLElement, + scrollDirection: number +) => any; + +export type DragAutoScrollOnStop = ( + item: Item, + scrollElement: Window | HTMLElement, + scrollDirection: number +) => any; + +export type DragAutoScrollHandle = ( + item: Item, + itemClientX: number, + itemClientY: number, + itemWidth: number, + itemHeight: number, + pointerClientX: number, + pointerClientY: number +) => { + left: number; + top: number; + width: number; + height: number; +}; + +export type DragAutoScrollSpeed = ( + item: Item, + scrollElement: Window | HTMLElement, + scrollData: { + direction: number; + threshold: number; + distance: number; + value: number; + maxValue: number; + duration: number; + speed: number; + deltaTime: number; + isEnding: boolean; + } +) => number; + +export interface DragAutoScrollOptions { + targets?: DragAutoScrollTargets | DragAutoScrollTargetsGetter; + handle?: DragAutoScrollHandle | null; + threshold?: number; + safeZone?: number; + speed?: number | DragAutoScrollSpeed; + sortDuringScroll?: boolean; + smoothStop?: boolean; + onStart?: DragAutoScrollOnStart | null; + onStop?: DragAutoScrollOnStop | null; +} + +export interface GridOptions { + items?: HTMLElement[] | NodeList | HTMLCollection | string; + showDuration?: number; + showEasing?: string; + visibleStyles?: StyleDeclaration; + hideDuration?: number; + hideEasing?: string; + hiddenStyles?: StyleDeclaration; + layout?: LayoutOptions | LayoutFunction; + layoutOnResize?: boolean | number; + layoutOnInit?: boolean; + layoutDuration?: number; + layoutEasing?: string; + sortData?: { [key: string]: SortDataGetter } | null; + dragEnabled?: boolean; + dragHandle?: string | null; + dragContainer?: HTMLElement | null; + dragStartPredicate?: DragStartPredicateOptions | DragStartPredicate; + dragAxis?: 'x' | 'y' | 'xy'; + dragSort?: boolean | DragSortGetter; + dragSortHeuristics?: DragSortHeuristics; + dragSortPredicate?: DragSortPredicateOptions | DragSortPredicate; + dragRelease?: DragReleaseOptions; + dragCssProps?: DraggerCssProps; + dragPlaceholder?: DragPlaceholderOptions; + dragAutoScroll?: DragAutoScrollOptions; + containerClass?: string; + itemClass?: string; + itemVisibleClass?: string; + itemHiddenClass?: string; + itemPositioningClass?: string; + itemDraggingClass?: string; + itemReleasingClass?: string; + itemPlaceholderClass?: string; +} + +// +// CLASSES +// + +export class Item { + constructor(grid: Grid, element: HTMLElement, isActive?: boolean); + getGrid(): Grid | undefined; + getElement(): HTMLElement | undefined; + getWidth(): number; + getHeight(): number; + getMargin(): { left: number; right: number; top: number; bottom: number }; + getPosition(): { left: number; top: number }; + isActive(): boolean; + isVisible(): boolean; + isShowing(): boolean; + isHiding(): boolean; + isPositioning(): boolean; + isDragging(): boolean; + isReleasing(): boolean; + isDestroyed(): boolean; +} + +export class ItemLayout { + constructor(item: Item); + start(instant: boolean, onFinish?: (isInterrupted: boolean, item: Item) => any): void; + stop(processCallbackQueue: boolean, targetStyles?: StyleDeclaration): void; + destroy(): void; +} + +export class ItemVisibility { + constructor(item: Item); + show(instant: boolean, onFinish?: (isInterrupted: boolean, item: Item) => any): void; + hide(instant: boolean, onFinish?: (isInterrupted: boolean, item: Item) => any): void; + stop(processCallbackQueue: boolean, applyCurrentStyles?: boolean): void; + setStyles(styles: StyleDeclaration): void; + destroy(): void; +} + +export class ItemMigrate { + constructor(item: Item); + start(targetGrid: Grid, position: HTMLElement | number | Item, container?: HTMLElement): void; + stop(abort?: boolean, left?: number, top?: number): void; + destroy(): void; +} + +export class ItemDrag { + constructor(item: Item); + static autoScroller: AutoScroller; + static defaultStartPredicate( + item: Item, + event: DraggerEvent, + options?: DragStartPredicateOptions + ): boolean | undefined; + static defaultSortPredicate( + item: Item, + options?: DragSortPredicateOptions + ): DragSortPredicateResult; + stop(): void; + sort(force?: boolean): void; + destroy(): void; +} + +export class ItemDragRelease { + constructor(item: Item); + start(): void; + stop(abort?: boolean, left?: number, top?: number): void; + isJustReleased(): boolean; + destroy(): void; +} + +export class ItemDragPlaceholder { + constructor(item: Item); + create(): void; + reset(): void; + isActive(): boolean; + getElement(): HTMLElement | null; + updateDimensions(): void; + destroy(): void; +} + +export class Emitter { + constructor(); + on(event: string, listener: EventListener): this; + off(event: string, listener: EventListener): this; + clear(event: string): this; + emit(event: string, ...args: any[]): this; + burst(event: string, ...args: any[]): this; + countListeners(event: string): number; + destroy(): this; +} + +export class Animator { + constructor(element: HTMLElement); + start( + propsFrom: StyleDeclaration, + propsTo: StyleDeclaration, + options?: { + duration?: number; + easing?: string; + onFinish?: (...args: any[]) => any; + } + ): void; + stop(applyCurrentStyles?: boolean): void; + isAnimating(): boolean; + destroy(): void; +} + +export class Dragger { + constructor(element: HTMLElement, cssProps?: DraggerCssProps); + isActive(): boolean; + setTouchAction(touchAction: string): void; + setCssProps(props?: DraggerCssProps): void; + getDeltaX(): number; + getDeltaY(): number; + getDistance(): number; + getDeltaTime(): number; + on(event: T, listener: DraggerEvents[T]): void; + off(event: T, listener: DraggerEvents[T]): void; + destroy(): void; +} + +export class AutoScroller { + constructor(); + static AXIS_X: 1; + static AXIS_Y: 2; + static FORWARD: 4; + static BACKWARD: 8; + static LEFT: 9; + static RIGHT: 5; + static UP: 10; + static DOWN: 6; + static smoothSpeed( + maxSpeed: number, + acceleration: number, + deceleration: number + ): DragAutoScrollSpeed; + static pointerHandle(pointerSize: number): DragAutoScrollHandle; + addItem(item: Item): void; + updateItem(item: Item): void; + removeItem(item: Item): void; + isItemScrollingX(item: Item): boolean; + isItemScrollingY(item: Item): boolean; + isItemScrolling(item: Item): boolean; + destroy(): void; +} + +export class Packer { + constructor(numWorkers?: number, options?: LayoutOptions); + setOptions(options?: LayoutOptions): void; + createLayout( + grid: Grid, + id: number, + items: Item[], + width: number, + height: number, + callback: LayoutFunctionCallback + ): LayoutFunctionCancel | void; + cancelLayout(id: number): void; + destroy(): void; +} + +export default class Grid { + constructor(element: string | HTMLElement, options?: GridOptions); + + static Item: typeof Item; + + static ItemLayout: typeof ItemLayout; + + static ItemVisibility: typeof ItemVisibility; + + static ItemMigrate: typeof ItemMigrate; + + static ItemDrag: typeof ItemDrag; + + static ItemDragRelease: typeof ItemDragRelease; + + static ItemDragPlaceholder: typeof ItemDragPlaceholder; + + static Emitter: typeof Emitter; + + static Animator: typeof Animator; + + static Dragger: typeof Dragger; + + static Packer: typeof Packer; + + static AutoScroller: typeof AutoScroller; + + static defaultPacker: Packer; + + static defaultOptions: GridOptions; + + on(event: T, listener: GridEvents[T]): this; + + off(event: T, listener: GridEvents[T]): this; + + getElement(): HTMLElement; + + getItem(target: HTMLElement | number | Item): Item | null; + + getItems(targets?: HTMLElement | number | Item | Array): Item[]; + + refreshItems(items?: Item[], force?: boolean): this; + + refreshSortData(items?: Item[]): this; + + synchronize(): this; + + layout(instant?: boolean, onFinish?: LayoutOnFinish): this; + + add( + elements: HTMLElement | HTMLElement[] | NodeList | HTMLCollection, + options?: { + index?: number; + active?: boolean; + layout?: boolean | 'instant' | LayoutOnFinish; + } + ): Item[]; + + remove( + items: Item[], + options?: { + removeElements?: boolean; + layout?: boolean | 'instant' | LayoutOnFinish; + } + ): Item[]; + + show( + items: Item[], + options?: { + instant?: boolean; + syncWithLayout?: boolean; + onFinish?: (items: Item[]) => any; + layout?: boolean | 'instant' | LayoutOnFinish; + } + ): this; + + hide( + items: Item[], + options?: { + instant?: boolean; + syncWithLayout?: boolean; + onFinish?: (items: Item[]) => any; + layout?: boolean | 'instant' | LayoutOnFinish; + } + ): this; + + filter( + predicate: string | ((item: Item) => boolean), + options?: { + instant?: boolean; + syncWithLayout?: boolean; + onFinish?: (items: Item[]) => any; + layout?: boolean | 'instant' | LayoutOnFinish; + } + ): this; + + sort( + comparer: ((a: Item, b: Item) => number) | string | Item[], + options?: { + descending?: boolean; + layout?: boolean | 'instant' | LayoutOnFinish; + } + ): this; + + move( + item: HTMLElement | number | Item, + position: HTMLElement | number | Item, + options?: { + action?: 'move' | 'swap'; + layout?: boolean | 'instant' | LayoutOnFinish; + } + ): this; + + send( + item: HTMLElement | number | Item, + targetGrid: Grid, + position: HTMLElement | number | Item, + options?: { + appendTo?: HTMLElement; + layoutSender?: boolean | 'instant' | LayoutOnFinish; + layoutReceiver?: boolean | 'instant' | LayoutOnFinish; + } + ): this; + + destroy(removeElements?: boolean): this; +} + +export as namespace Muuri; diff --git a/src/index.js b/src/index.js index 24dda799..7eb50ac6 100644 --- a/src/index.js +++ b/src/index.js @@ -4,5 +4,4 @@ * https://github.com/haltu/muuri/blob/master/LICENSE.md */ -import Grid from './Grid/Grid'; -export default Grid; +export { default } from './Grid/Grid'; diff --git a/src/shared.js b/src/shared.js deleted file mode 100644 index e57943d4..00000000 --- a/src/shared.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2015-present, Haltu Oy - * Released under the MIT license - * https://github.com/haltu/muuri/blob/master/LICENSE.md - */ - -export var namespace = 'Muuri'; -export var gridInstances = {}; - -export var actionSwap = 'swap'; -export var actionMove = 'move'; - -export var eventSynchronize = 'synchronize'; -export var eventLayoutStart = 'layoutStart'; -export var eventLayoutEnd = 'layoutEnd'; -export var eventAdd = 'add'; -export var eventRemove = 'remove'; -export var eventShowStart = 'showStart'; -export var eventShowEnd = 'showEnd'; -export var eventHideStart = 'hideStart'; -export var eventHideEnd = 'hideEnd'; -export var eventFilter = 'filter'; -export var eventSort = 'sort'; -export var eventMove = 'move'; -export var eventSend = 'send'; -export var eventBeforeSend = 'beforeSend'; -export var eventReceive = 'receive'; -export var eventBeforeReceive = 'beforeReceive'; -export var eventDragInit = 'dragInit'; -export var eventDragStart = 'dragStart'; -export var eventDragMove = 'dragMove'; -export var eventDragScroll = 'dragScroll'; -export var eventDragEnd = 'dragEnd'; -export var eventDragReleaseStart = 'dragReleaseStart'; -export var eventDragReleaseEnd = 'dragReleaseEnd'; -export var eventDestroy = 'destroy'; diff --git a/src/ticker.js b/src/ticker.js index d4971a1d..6fbed900 100644 --- a/src/ticker.js +++ b/src/ticker.js @@ -6,52 +6,121 @@ import Ticker from './Ticker/Ticker'; -var ticker = new Ticker(); +var LAYOUT_READ = 'layoutRead'; +var LAYOUT_WRITE = 'layoutWrite'; +var VISIBILITY_READ = 'visibilityRead'; +var VISIBILITY_WRITE = 'visibilityWrite'; +var DRAG_START_READ = 'dragStartRead'; +var DRAG_START_WRITE = 'dragStartWrite'; +var DRAG_MOVE_READ = 'dragMoveRead'; +var DRAG_MOVE_WRITE = 'dragMoveWrite'; +var DRAG_SCROLL_READ = 'dragScrollRead'; +var DRAG_SCROLL_WRITE = 'dragScrollWrite'; +var DRAG_SORT_READ = 'dragSortRead'; +var PLACEHOLDER_LAYOUT_READ = 'placeholderLayoutRead'; +var PLACEHOLDER_LAYOUT_WRITE = 'placeholderLayoutWrite'; +var PLACEHOLDER_RESIZE_WRITE = 'placeholderResizeWrite'; +var AUTO_SCROLL_READ = 'autoScrollRead'; +var AUTO_SCROLL_WRITE = 'autoScrollWrite'; +var DEBOUNCE_READ = 'debounceRead'; -var layoutTick = 'layout'; -var visibilityTick = 'visibility'; -var moveTick = 'move'; -var scrollTick = 'scroll'; -var placeholderTick = 'placeholder'; +var LANE_READ = 0; +var LANE_READ_TAIL = 1; +var LANE_WRITE = 2; +var ticker = new Ticker(3); export default ticker; -export function addLayoutTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + layoutTick, readCallback, writeCallback); +export function addLayoutTick(itemId, read, write) { + ticker.add(LANE_READ, LAYOUT_READ + itemId, read); + ticker.add(LANE_WRITE, LAYOUT_WRITE + itemId, write); } export function cancelLayoutTick(itemId) { - return ticker.cancel(itemId + layoutTick); + ticker.remove(LANE_READ, LAYOUT_READ + itemId); + ticker.remove(LANE_WRITE, LAYOUT_WRITE + itemId); } -export function addVisibilityTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + visibilityTick, readCallback, writeCallback); +export function addVisibilityTick(itemId, read, write) { + ticker.add(LANE_READ, VISIBILITY_READ + itemId, read); + ticker.add(LANE_WRITE, VISIBILITY_WRITE + itemId, write); } export function cancelVisibilityTick(itemId) { - return ticker.cancel(itemId + visibilityTick); + ticker.remove(LANE_READ, VISIBILITY_READ + itemId); + ticker.remove(LANE_WRITE, VISIBILITY_WRITE + itemId); } -export function addMoveTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + moveTick, readCallback, writeCallback, true); +export function addDragStartTick(itemId, read, write) { + ticker.add(LANE_READ, DRAG_START_READ + itemId, read); + ticker.add(LANE_WRITE, DRAG_START_WRITE + itemId, write); } -export function cancelMoveTick(itemId) { - return ticker.cancel(itemId + moveTick); +export function cancelDragStartTick(itemId) { + ticker.remove(LANE_READ, DRAG_START_READ + itemId); + ticker.remove(LANE_WRITE, DRAG_START_WRITE + itemId); } -export function addScrollTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + scrollTick, readCallback, writeCallback, true); +export function addDragMoveTick(itemId, read, write) { + ticker.add(LANE_READ, DRAG_MOVE_READ + itemId, read); + ticker.add(LANE_WRITE, DRAG_MOVE_WRITE + itemId, write); } -export function cancelScrollTick(itemId) { - return ticker.cancel(itemId + scrollTick); +export function cancelDragMoveTick(itemId) { + ticker.remove(LANE_READ, DRAG_MOVE_READ + itemId); + ticker.remove(LANE_WRITE, DRAG_MOVE_WRITE + itemId); } -export function addPlaceholderTick(itemId, readCallback, writeCallback) { - return ticker.add(itemId + placeholderTick, readCallback, writeCallback); +export function addDragScrollTick(itemId, read, write) { + ticker.add(LANE_READ, DRAG_SCROLL_READ + itemId, read); + ticker.add(LANE_WRITE, DRAG_SCROLL_WRITE + itemId, write); } -export function cancelPlaceholderTick(itemId) { - return ticker.cancel(itemId + placeholderTick); +export function cancelDragScrollTick(itemId) { + ticker.remove(LANE_READ, DRAG_SCROLL_READ + itemId); + ticker.remove(LANE_WRITE, DRAG_SCROLL_WRITE + itemId); +} + +export function addDragSortTick(itemId, read) { + ticker.add(LANE_READ_TAIL, DRAG_SORT_READ + itemId, read); +} + +export function cancelDragSortTick(itemId) { + ticker.remove(LANE_READ_TAIL, DRAG_SORT_READ + itemId); +} + +export function addPlaceholderLayoutTick(itemId, read, write) { + ticker.add(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId, read); + ticker.add(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId, write); +} + +export function cancelPlaceholderLayoutTick(itemId) { + ticker.remove(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId); + ticker.remove(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId); +} + +export function addPlaceholderResizeTick(itemId, write) { + ticker.add(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId, write); +} + +export function cancelPlaceholderResizeTick(itemId) { + ticker.remove(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId); +} + +export function addAutoScrollTick(read, write) { + ticker.add(LANE_READ, AUTO_SCROLL_READ, read); + ticker.add(LANE_WRITE, AUTO_SCROLL_WRITE, write); +} + +export function cancelAutoScrollTick() { + ticker.remove(LANE_READ, AUTO_SCROLL_READ); + ticker.remove(LANE_WRITE, AUTO_SCROLL_WRITE); +} + +export function addDebounceTick(debounceId, read) { + ticker.add(LANE_READ, DEBOUNCE_READ + debounceId, read); +} + +export function cancelDebounceTick(debounceId) { + ticker.remove(LANE_READ, DEBOUNCE_READ + debounceId); } diff --git a/src/utils/addClass.js b/src/utils/addClass.js index 3f1f282f..ab3ade65 100644 --- a/src/utils/addClass.js +++ b/src/utils/addClass.js @@ -13,6 +13,8 @@ import elementMatches from './elementMatches'; * @param {String} className */ export default function addClass(element, className) { + if (!className) return; + if (element.classList) { element.classList.add(className); } else { diff --git a/src/utils/debounce.js b/src/utils/debounce.js index 166c1971..fad1a582 100644 --- a/src/utils/debounce.js +++ b/src/utils/debounce.js @@ -4,47 +4,61 @@ * https://github.com/haltu/muuri/blob/master/LICENSE.md */ -import ticker from '../ticker'; +import { addDebounceTick, cancelDebounceTick } from '../ticker'; -var actionCancel = 'cancel'; -var actionFinish = 'finish'; -var debounceTick = 'debounce'; var debounceId = 0; /** * Returns a function, that, as long as it continues to be invoked, will not * be triggered. The function will be called after it stops being called for * N milliseconds. The returned function accepts one argument which, when - * being "finish", calls the debounce function immediately if it is currently - * waiting to be called, and when being "cancel" cancels the currently queued - * function call. + * being `true`, cancels the debounce function immediately. When the debounce + * function is canceled it cannot be invoked again. * * @param {Function} fn - * @param {Number} wait + * @param {Number} durationMs * @returns {Function} */ -export default function debounce(fn, wait) { - var timeout; - var tickerId = ++debounceId + debounceTick; - - if (wait > 0) { - return function(action) { - if (timeout !== undefined) { - timeout = window.clearTimeout(timeout); - ticker.cancel(tickerId); - if (action === actionFinish) fn(); - } - - if (action !== actionCancel && action !== actionFinish) { - timeout = window.setTimeout(function() { - timeout = undefined; - ticker.add(tickerId, fn, null, true); - }, wait); - } - }; - } - - return function(action) { - if (action !== actionCancel) fn(); +export default function debounce(fn, durationMs) { + var id = ++debounceId; + var timer = 0; + var lastTime = 0; + var isCanceled = false; + var tick = function (time) { + if (isCanceled) return; + + if (lastTime) timer -= time - lastTime; + lastTime = time; + + if (timer > 0) { + addDebounceTick(id, tick); + } else { + timer = lastTime = 0; + fn(); + } + }; + + return function (cancel) { + if (isCanceled) return; + + if (durationMs <= 0) { + if (cancel !== true) fn(); + return; + } + + if (cancel === true) { + isCanceled = true; + timer = lastTime = 0; + tick = undefined; + cancelDebounceTick(id); + return; + } + + if (timer <= 0) { + timer = durationMs; + tick(0); + } else { + timer = durationMs; + } }; } diff --git a/src/utils/elementMatches.js b/src/utils/elementMatches.js index 6892fb17..a14446bf 100644 --- a/src/utils/elementMatches.js +++ b/src/utils/elementMatches.js @@ -12,7 +12,7 @@ var matchesFn = ElProto.mozMatchesSelector || ElProto.msMatchesSelector || ElProto.oMatchesSelector || - function() { + function () { return false; }; diff --git a/src/utils/getContainingBlock.js b/src/utils/getContainingBlock.js index c1073b68..b19ada99 100644 --- a/src/utils/getContainingBlock.js +++ b/src/utils/getContainingBlock.js @@ -14,20 +14,16 @@ import isTransformed from './isTransformed'; * absolute positioned elements. * * @param {HTMLElement} element - * @param {Boolean} [includeSelf=false] - * - When this is set to true the containing block checking is started from - * the provided element. Otherwise the checking is started from the - * provided element's parent element. * @returns {(Document|Element)} */ -export default function getContainingBlock(element, includeSelf) { +export default function getContainingBlock(element) { // As long as the containing block is an element, static and not // transformed, try to get the element's parent element and fallback to // document. https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L339 - var document = window.document; - var ret = (includeSelf ? element : element.parentElement) || document; - while (ret && ret !== document && getStyle(ret, 'position') === 'static' && !isTransformed(ret)) { - ret = ret.parentElement || document; + var doc = document; + var res = element || doc; + while (res && res !== doc && getStyle(res, 'position') === 'static' && !isTransformed(res)) { + res = res.parentElement || doc; } - return ret; + return res; } diff --git a/src/utils/getCurrentStyles.js b/src/utils/getCurrentStyles.js index 703b9a6d..ce037c03 100644 --- a/src/utils/getCurrentStyles.js +++ b/src/utils/getCurrentStyles.js @@ -8,16 +8,26 @@ import getStyle from './getStyle'; import getStyleName from './getStyleName'; /** - * Get current values of the provided styles definition object. + * Get current values of the provided styles definition object or array. * * @param {HTMLElement} element - * @param {Object} styles + * @param {(Object|Array} styles * @return {Object} */ export default function getCurrentStyles(element, styles) { - var current = {}; - for (var prop in styles) { - current[prop] = getStyle(element, getStyleName(prop)); + var result = {}; + var prop, i; + + if (Array.isArray(styles)) { + for (i = 0; i < styles.length; i++) { + prop = styles[i]; + result[prop] = getStyle(element, getStyleName(prop)); + } + } else { + for (prop in styles) { + result[prop] = getStyle(element, getStyleName(prop)); + } } - return current; + + return result; } diff --git a/src/utils/getIntersectionArea.js b/src/utils/getIntersectionArea.js new file mode 100644 index 00000000..f76a3a1d --- /dev/null +++ b/src/utils/getIntersectionArea.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +import isOverlapping from './isOverlapping'; + +/** + * Calculate intersection area between two rectangle. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ +export default function getIntersectionArea(a, b) { + if (!isOverlapping(a, b)) return 0; + var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left); + var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top); + return width * height; +} diff --git a/src/utils/getIntersectionScore.js b/src/utils/getIntersectionScore.js new file mode 100644 index 00000000..02d4d50c --- /dev/null +++ b/src/utils/getIntersectionScore.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +import getIntersectionArea from './getIntersectionArea'; + +/** + * Calculate how many percent the intersection area of two rectangles is from + * the maximum potential intersection area between the rectangles. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ +export default function getIntersectionScore(a, b) { + var area = getIntersectionArea(a, b); + if (!area) return 0; + var maxArea = Math.min(a.width, b.width) * Math.min(a.height, b.height); + return (area / maxArea) * 100; +} diff --git a/src/utils/getOffsetDiff.js b/src/utils/getOffsetDiff.js index 97fbd22f..48c51a0a 100644 --- a/src/utils/getOffsetDiff.js +++ b/src/utils/getOffsetDiff.js @@ -24,33 +24,33 @@ var offsetDiff = {}; * @returns {Object} */ function getOffset(element, offsetData) { - var ret = offsetData || {}; + var offset = offsetData || {}; var rect; // Set up return data. - ret.left = 0; - ret.top = 0; + offset.left = 0; + offset.top = 0; // Document's offsets are always 0. - if (element === document) return ret; + if (element === document) return offset; // Add viewport scroll left/top to the respective offsets. - ret.left = window.pageXOffset || 0; - ret.top = window.pageYOffset || 0; + offset.left = window.pageXOffset || 0; + offset.top = window.pageYOffset || 0; // Window's offsets are the viewport scroll left/top values. - if (element.self === window.self) return ret; + if (element.self === window.self) return offset; // Add element's client rects to the offsets. rect = element.getBoundingClientRect(); - ret.left += rect.left; - ret.top += rect.top; + offset.left += rect.left; + offset.top += rect.top; // Exclude element's borders from the offset. - ret.left += getStyleAsFloat(element, 'border-left-width'); - ret.top += getStyleAsFloat(element, 'border-top-width'); + offset.left += getStyleAsFloat(element, 'border-left-width'); + offset.top += getStyleAsFloat(element, 'border-top-width'); - return ret; + return offset; } /** @@ -73,8 +73,8 @@ export default function getOffsetDiff(elemA, elemB, compareContainingBlocks) { // Compare containing blocks if necessary. if (compareContainingBlocks) { - elemA = getContainingBlock(elemA, true); - elemB = getContainingBlock(elemB, true); + elemA = getContainingBlock(elemA); + elemB = getContainingBlock(elemB); // If containing blocks are identical, let's return early. if (elemA === elemB) return offsetDiff; diff --git a/src/utils/getPrefixedPropName.js b/src/utils/getPrefixedPropName.js index cce2bd09..5cb63db7 100644 --- a/src/utils/getPrefixedPropName.js +++ b/src/utils/getPrefixedPropName.js @@ -3,26 +3,32 @@ * https://github.com/hammerjs/hammer.js/blob/563b5b1e4bfbb5796798dd286cd57b7c56f1eb9e/src/utils/prefixed.js */ +// Playing it safe here, test all potential prefixes capitalized and lowercase. var vendorPrefixes = ['', 'webkit', 'moz', 'ms', 'o', 'Webkit', 'Moz', 'MS', 'O']; +var cache = {}; /** * Get prefixed CSS property name when given a non-prefixed CSS property name. - * @param {Object} elemStyle - * @param {String} propName - * @returns {!String} + * Returns null if the property is not supported at all. + * + * @param {CSSStyleDeclaration} style + * @param {String} prop + * @returns {String} */ -export default function getPrefixedPropName(elemStyle, propName) { - var camelPropName = propName[0].toUpperCase() + propName.slice(1); - var i = 0; - var prefix; - var prefixedPropName; +export default function getPrefixedPropName(style, prop) { + var prefixedProp = cache[prop] || ''; + if (prefixedProp) return prefixedProp; + var camelProp = prop[0].toUpperCase() + prop.slice(1); + var i = 0; while (i < vendorPrefixes.length) { - prefix = vendorPrefixes[i]; - prefixedPropName = prefix ? prefix + camelPropName : propName; - if (prefixedPropName in elemStyle) return prefixedPropName; + prefixedProp = vendorPrefixes[i] ? vendorPrefixes[i] + camelProp : prop; + if (prefixedProp in style) { + cache[prop] = prefixedProp; + return prefixedProp; + } ++i; } - return null; + return ''; } diff --git a/src/utils/getScrollableAncestors.js b/src/utils/getScrollableAncestors.js index 1576b2e2..db9dc5d8 100644 --- a/src/utils/getScrollableAncestors.js +++ b/src/utils/getScrollableAncestors.js @@ -7,36 +7,36 @@ import isScrollable from './isScrollable'; /** - * Collect element's ancestors that are potentially scrollable elements. + * Collect element's ancestors that are potentially scrollable elements. The + * provided element is also also included in the check, meaning that if it is + * scrollable it is added to the result array. * * @param {HTMLElement} element - * @param {Boolean} [includeSelf=false] - * @param {Array} [data] + * @param {Array} [result] * @returns {Array} */ -export default function getScrollableAncestors(element, includeSelf, data) { - var ret = data || []; - var parent = includeSelf ? element : element.parentNode; +export default function getScrollableAncestors(element, result) { + result = result || []; // Find scroll parents. - while (parent && parent !== document) { + while (element && element !== document) { // If element is inside ShadowDOM let's get it's host node from the real // DOM and continue looping. - if (parent.getRootNode && parent instanceof DocumentFragment) { - parent = parent.getRootNode().host; + if (element.getRootNode && element instanceof DocumentFragment) { + element = element.getRootNode().host; continue; } // If element is scrollable let's add it to the scrollable list. - if (isScrollable(parent)) { - ret.push(parent); + if (isScrollable(element)) { + result.push(element); } - parent = parent.parentNode; + element = element.parentNode; } // Always add window to the results. - ret.push(window); + result.push(window); - return ret; + return result; } diff --git a/src/utils/getStyle.js b/src/utils/getStyle.js index 54c658f9..5ca16069 100644 --- a/src/utils/getStyle.js +++ b/src/utils/getStyle.js @@ -3,10 +3,19 @@ * Released under the MIT license * https://github.com/haltu/muuri/blob/master/LICENSE.md */ - -import { transformStyle } from './supportedTransform'; - -var stylesCache = typeof WeakMap === 'function' ? new WeakMap() : null; +var isWeakMapSupported = typeof WeakMap === 'function'; +var cache = isWeakMapSupported ? new WeakMap() : null; +var cacheInterval = 3000; +var cacheTimer; +var canClearCache = true; +var clearCache = function () { + if (canClearCache) { + cacheTimer = window.clearInterval(cacheTimer); + cache = isWeakMapSupported ? new WeakMap() : null; + } else { + canClearCache = true; + } +}; /** * Returns the computed value of an element's style property as a string. @@ -16,10 +25,20 @@ var stylesCache = typeof WeakMap === 'function' ? new WeakMap() : null; * @returns {String} */ export default function getStyle(element, style) { - var styles = stylesCache && stylesCache.get(element); + var styles = cache && cache.get(element); + if (!styles) { styles = window.getComputedStyle(element, null); - if (stylesCache) stylesCache.set(element, styles); + if (cache) cache.set(element, styles); } - return styles.getPropertyValue(style === 'transform' ? transformStyle : style); + + if (cache) { + if (!cacheTimer) { + cacheTimer = window.setInterval(clearCache, cacheInterval); + } else { + canClearCache = false; + } + } + + return styles.getPropertyValue(style); } diff --git a/src/utils/getStyleName.js b/src/utils/getStyleName.js index a9fa5fa9..10636368 100644 --- a/src/utils/getStyleName.js +++ b/src/utils/getStyleName.js @@ -5,13 +5,28 @@ */ var styleNameRegEx = /([A-Z])/g; +var prefixRegex = /^(webkit-|moz-|ms-|o-)/; +var msPrefixRegex = /^(-m-s-)/; /** - * Transforms a camel case style property to kebab case style property. + * Transforms a camel case style property to kebab case style property. Handles + * vendor prefixed properties elegantly as well, e.g. "WebkitTransform" and + * "webkitTransform" are both transformed into "-webkit-transform". * - * @param {String} string + * @param {String} property * @returns {String} */ -export default function getStyleName(string) { - return string.replace(styleNameRegEx, '-$1').toLowerCase(); +export default function getStyleName(property) { + // Initial slicing, turns "fooBarProp" into "foo-bar-prop". + var styleName = property.replace(styleNameRegEx, '-$1').toLowerCase(); + + // Handle properties that start with "webkit", "moz", "ms" or "o" prefix (we + // need to add an extra '-' to the beginnig). + styleName = styleName.replace(prefixRegex, '-$1'); + + // Handle properties that start with "MS" prefix (we need to transform the + // "-m-s-" into "-ms-"). + styleName = styleName.replace(msPrefixRegex, '-ms-'); + + return styleName; } diff --git a/src/utils/getTranslate.js b/src/utils/getTranslate.js index d485eaa9..ea5c7967 100644 --- a/src/utils/getTranslate.js +++ b/src/utils/getTranslate.js @@ -5,9 +5,9 @@ */ import getStyle from './getStyle'; +import transformStyle from './transformStyle'; var translateValue = {}; -var transformStyle = 'transform'; var transformNone = 'none'; var rxMat3d = /^matrix3d/; var rxMatTx = /([^,]*,){4}/; diff --git a/src/utils/getUnprefixedPropName.js b/src/utils/getUnprefixedPropName.js new file mode 100644 index 00000000..2078b94d --- /dev/null +++ b/src/utils/getUnprefixedPropName.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +var unprefixRegEx = /^(webkit|moz|ms|o|Webkit|Moz|MS|O)(?=[A-Z])/; +var cache = {}; + +/** + * Remove any potential vendor prefixes from a property name. + * + * @param {String} prop + * @returns {String} + */ +export default function getUnprefixedPropName(prop) { + var result = cache[prop]; + if (result) return result; + + result = prop.replace(unprefixRegEx, ''); + + if (result !== prop) { + result = result[0].toLowerCase() + result.slice(1); + } + + cache[prop] = result; + + return result; +} diff --git a/src/utils/hasPassiveEvents.js b/src/utils/hasPassiveEvents.js new file mode 100644 index 00000000..9b4e3388 --- /dev/null +++ b/src/utils/hasPassiveEvents.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +/** + * Check if passive events are supported. + * https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection + * + * @returns {Boolean} + */ +export default function hasPassiveEvents() { + var isPassiveEventsSupported = false; + + try { + var passiveOpts = Object.defineProperty({}, 'passive', { + get: function () { + isPassiveEventsSupported = true; + }, + }); + window.addEventListener('testPassive', null, passiveOpts); + window.removeEventListener('testPassive', null, passiveOpts); + } catch (e) {} + + return isPassiveEventsSupported; +} diff --git a/src/utils/isFunction.js b/src/utils/isFunction.js index 7cbcdc61..c6566ac9 100644 --- a/src/utils/isFunction.js +++ b/src/utils/isFunction.js @@ -4,7 +4,7 @@ * https://github.com/haltu/muuri/blob/master/LICENSE.md */ -var strFunction = 'function'; +var functionType = 'function'; /** * Check if a value is a function. @@ -13,5 +13,5 @@ var strFunction = 'function'; * @returns {Boolean} */ export default function isFunction(val) { - return typeof val === strFunction; + return typeof val === functionType; } diff --git a/src/utils/isNative.js b/src/utils/isNative.js new file mode 100644 index 00000000..7fab4fa2 --- /dev/null +++ b/src/utils/isNative.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +import isFunction from './isFunction'; + +var nativeCode = '[native code]'; + +/** + * Check if a value (e.g. a method or constructor) is native code. Good for + * detecting when a polyfill is used and when not. + * + * @param {*} feat + * @returns {Boolean} + */ +export default function isNative(feat) { + var S = window.Symbol; + return !!( + feat && + isFunction(S) && + isFunction(S.toString) && + S(feat).toString().indexOf(nativeCode) > -1 + ); +} diff --git a/src/utils/isNodeList.js b/src/utils/isNodeList.js index ea99c5c8..20458a45 100644 --- a/src/utils/isNodeList.js +++ b/src/utils/isNodeList.js @@ -8,7 +8,7 @@ var htmlCollectionType = '[object HTMLCollection]'; var nodeListType = '[object NodeList]'; /** - * Check if a value is a node list + * Check if a value is a node list or a html collection. * * @param {*} val * @returns {Boolean} diff --git a/src/utils/isOverlapping.js b/src/utils/isOverlapping.js new file mode 100644 index 00000000..e09ee8a6 --- /dev/null +++ b/src/utils/isOverlapping.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +/** + * Check if two rectangles are overlapping. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ +export default function isOverlapping(a, b) { + return !( + a.left + a.width <= b.left || + b.left + b.width <= a.left || + a.top + a.height <= b.top || + b.top + b.height <= a.top + ); +} diff --git a/src/utils/isScrollable.js b/src/utils/isScrollable.js index 029002ab..b774a462 100644 --- a/src/utils/isScrollable.js +++ b/src/utils/isScrollable.js @@ -6,11 +6,15 @@ import getStyle from './getStyle'; -var styleOverflow = 'overflow'; -var styleOverflowX = 'overflow-x'; -var styleOverflowY = 'overflow-y'; -var overflowAuto = 'auto'; -var overflowScroll = 'scroll'; +/** + * Check if overflow style value is scrollable. + * + * @param {String} value + * @returns {Boolean} + */ +function isScrollableOverflow(value) { + return value === 'auto' || value === 'scroll' || value === 'overlay'; +} /** * Check if an element is scrollable. @@ -19,14 +23,9 @@ var overflowScroll = 'scroll'; * @returns {Boolean} */ export default function isScrollable(element) { - var overflow = getStyle(element, styleOverflow); - if (overflow === overflowAuto || overflow === overflowScroll) return true; - - overflow = getStyle(element, styleOverflowX); - if (overflow === overflowAuto || overflow === overflowScroll) return true; - - overflow = getStyle(element, styleOverflowY); - if (overflow === overflowAuto || overflow === overflowScroll) return true; - - return false; + return ( + isScrollableOverflow(getStyle(element, 'overflow')) || + isScrollableOverflow(getStyle(element, 'overflow-x')) || + isScrollableOverflow(getStyle(element, 'overflow-y')) + ); } diff --git a/src/utils/isTransformed.js b/src/utils/isTransformed.js index dafbc002..fea6c0fb 100644 --- a/src/utils/isTransformed.js +++ b/src/utils/isTransformed.js @@ -5,6 +5,12 @@ */ import getStyle from './getStyle'; +import transformStyle from './transformStyle'; + +var transformNone = 'none'; +var displayInline = 'inline'; +var displayNone = 'none'; +var displayStyle = 'display'; /** * Returns true if element is transformed, false if not. In practice the @@ -19,11 +25,11 @@ import getStyle from './getStyle'; * @returns {Boolean} */ export default function isTransformed(element) { - var transform = getStyle(element, 'transform'); - if (!transform || transform === 'none') return false; + var transform = getStyle(element, transformStyle); + if (!transform || transform === transformNone) return false; - var display = getStyle(element, 'display'); - if (display === 'inline' || display === 'none') return false; + var display = getStyle(element, displayStyle); + if (display === displayInline || display === displayNone) return false; return true; } diff --git a/src/utils/noop.js b/src/utils/noop.js new file mode 100644 index 00000000..66a36b16 --- /dev/null +++ b/src/utils/noop.js @@ -0,0 +1,7 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +export default function noop() {} diff --git a/src/utils/normalizeArrayIndex.js b/src/utils/normalizeArrayIndex.js index 150107e3..65d7ee44 100644 --- a/src/utils/normalizeArrayIndex.js +++ b/src/utils/normalizeArrayIndex.js @@ -7,14 +7,15 @@ /** * Normalize array index. Basically this function makes sure that the provided * array index is within the bounds of the provided array and also transforms - * negative index to the matching positive index. + * negative index to the matching positive index. The third (optional) argument + * allows you to define offset for array's length in case you are adding items + * to the array or removing items from the array. * * @param {Array} array * @param {Number} index - * @param {Boolean} isMigration + * @param {Number} [sizeOffset] */ -export default function normalizeArrayIndex(array, index, isMigration) { - var length = array.length; - var maxIndex = Math.max(0, isMigration ? length : length - 1); +export default function normalizeArrayIndex(array, index, sizeOffset) { + var maxIndex = Math.max(0, array.length - 1 + (sizeOffset || 0)); return index > maxIndex ? maxIndex : index < 0 ? Math.max(maxIndex + index + 1, 0) : index; } diff --git a/src/utils/raf.js b/src/utils/raf.js index cf9040d7..ed11fa04 100644 --- a/src/utils/raf.js +++ b/src/utils/raf.js @@ -11,9 +11,9 @@ var raf = ( window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || - function(callback) { - return this.setTimeout(function() { - callback(dt); + function (callback) { + return this.setTimeout(function () { + callback(Date.now()); }, dt); } ).bind(window); diff --git a/src/utils/removeClass.js b/src/utils/removeClass.js index 8eb314d2..120842dd 100644 --- a/src/utils/removeClass.js +++ b/src/utils/removeClass.js @@ -13,6 +13,8 @@ import elementMatches from './elementMatches'; * @param {String} className */ export default function removeClass(element, className) { + if (!className) return; + if (element.classList) { element.classList.remove(className); } else { diff --git a/src/utils/setStyles.js b/src/utils/setStyles.js index a54975fc..ec557292 100644 --- a/src/utils/setStyles.js +++ b/src/utils/setStyles.js @@ -4,10 +4,6 @@ * https://github.com/haltu/muuri/blob/master/LICENSE.md */ -import { transformProp } from './supportedTransform'; - -var transformStyle = 'transform'; - /** * Set inline styles to an element. * @@ -16,6 +12,6 @@ var transformStyle = 'transform'; */ export default function setStyles(element, styles) { for (var prop in styles) { - element.style[prop === transformStyle ? transformProp : prop] = styles[prop]; + element.style[prop] = styles[prop]; } } diff --git a/src/utils/supportedTransform.js b/src/utils/supportedTransform.js deleted file mode 100644 index 59b6fd84..00000000 --- a/src/utils/supportedTransform.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2015-present, Haltu Oy - * Released under the MIT license - * https://github.com/haltu/muuri/blob/master/LICENSE.md - */ - -// Set up the default export values. -export var transformStyle = 'transform'; -export var transformProp = 'transform'; - -// Find the supported transform prop and style names. -var docElemStyle = window.document.documentElement.style; -var style = 'transform'; -var styleCap = 'Transform'; -var found = false; -['', 'Webkit', 'Moz', 'O', 'ms'].forEach(function(prefix) { - if (found) return; - var propName = prefix ? prefix + styleCap : style; - if (docElemStyle[propName] !== undefined) { - prefix = prefix.toLowerCase(); - transformStyle = prefix ? '-' + prefix + '-' + style : style; - transformProp = propName; - found = true; - } -}); diff --git a/src/utils/toArray.js b/src/utils/toArray.js index 1dff800b..4a728774 100644 --- a/src/utils/toArray.js +++ b/src/utils/toArray.js @@ -9,9 +9,9 @@ import isNodeList from './isNodeList'; /** * Converts a value to an array or clones an array. * - * @param {*} target + * @param {*} val * @returns {Array} */ -export default function toArray(target) { - return isNodeList(target) ? Array.prototype.slice.call(target) : Array.prototype.concat(target); +export default function toArray(val) { + return isNodeList(val) ? Array.prototype.slice.call(val) : Array.prototype.concat(val); } diff --git a/src/utils/transformProp.js b/src/utils/transformProp.js new file mode 100644 index 00000000..1c4e42e6 --- /dev/null +++ b/src/utils/transformProp.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +import getPrefixedPropName from './getPrefixedPropName'; + +var transformProp = getPrefixedPropName(document.documentElement.style, 'transform') || 'transform'; + +export default transformProp; diff --git a/src/utils/transformStyle.js b/src/utils/transformStyle.js new file mode 100644 index 00000000..6082a331 --- /dev/null +++ b/src/utils/transformStyle.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2015-present, Haltu Oy + * Released under the MIT license + * https://github.com/haltu/muuri/blob/master/LICENSE.md + */ + +import transformProp from './transformProp'; +import getStyleName from './getStyleName'; + +var transformStyle = getStyleName(transformProp); + +export default transformStyle; diff --git a/tests/grid-constructor/container.js b/tests/grid-constructor/container.js index 9dd28910..adc310ee 100644 --- a/tests/grid-constructor/container.js +++ b/tests/grid-constructor/container.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid instance'); QUnit.test('Muuri constructor should not accept an invalid container element', function (assert) { - assert.expect(5); assert.throws(function () { @@ -27,48 +25,58 @@ assert.throws(function () { new Muuri('.does-not-exist'); }, 'Should throw an error when a valid element matching selector query string is not found'); - }); QUnit.test('Muuri constructor should accept document body as container', function (assert) { - - var muuri = new Muuri(document.body, {items: []}); - assert.strictEqual(muuri instanceof Muuri, true, 'Should initiate succesfully when body element is set as the container'); - muuri.destroy(); - - }); - - QUnit.test('Muuri constructor should accept any descendant of document body as container', function (assert) { - - var element = document.createElement('div'); - document.body.appendChild(element); - - var childElement = document.createElement('div'); - element.appendChild(childElement); - - var muuri = new Muuri(childElement); - assert.strictEqual(muuri instanceof Muuri, true, 'Should initiate succesfully when an element which is not a direct child but a descendant of document body is set as the container'); + var muuri = new Muuri(document.body, { items: [] }); + assert.strictEqual( + muuri instanceof Muuri, + true, + 'Should initiate succesfully when body element is set as the container' + ); muuri.destroy(); - - element.parentNode.removeChild(element); - - }); - - QUnit.test('Muuri constructor should accept a selector string as container and query it', function (assert) { - - var element = document.createElement('div'); - document.body.appendChild(element); - - var childElement = document.createElement('div'); - childElement.classList.add('muuri-grid'); - element.appendChild(childElement); - - var muuri = new Muuri('.muuri-grid'); - assert.strictEqual(muuri instanceof Muuri, true, 'Should initiate succesfully when a selector string is passed as the container parameter'); - muuri.destroy(); - - element.parentNode.removeChild(element); - }); -})(this); \ No newline at end of file + QUnit.test( + 'Muuri constructor should accept any descendant of document body as container', + function (assert) { + var element = document.createElement('div'); + document.body.appendChild(element); + + var childElement = document.createElement('div'); + element.appendChild(childElement); + + var muuri = new Muuri(childElement); + assert.strictEqual( + muuri instanceof Muuri, + true, + 'Should initiate succesfully when an element which is not a direct child but a descendant of document body is set as the container' + ); + muuri.destroy(); + + element.parentNode.removeChild(element); + } + ); + + QUnit.test( + 'Muuri constructor should accept a selector string as container and query it', + function (assert) { + var element = document.createElement('div'); + document.body.appendChild(element); + + var childElement = document.createElement('div'); + childElement.classList.add('muuri-grid'); + element.appendChild(childElement); + + var muuri = new Muuri('.muuri-grid'); + assert.strictEqual( + muuri instanceof Muuri, + true, + 'Should initiate succesfully when a selector string is passed as the container parameter' + ); + muuri.destroy(); + + element.parentNode.removeChild(element); + } + ); +})(this); diff --git a/tests/grid-constructor/instance.js b/tests/grid-constructor/instance.js index 61692b97..40739abd 100644 --- a/tests/grid-constructor/instance.js +++ b/tests/grid-constructor/instance.js @@ -1,5 +1,4 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid instance'); @@ -8,5 +7,4 @@ assert.expect(1); assert.strictEqual(typeof Muuri, 'function'); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/add.js b/tests/grid-events/add.js index 1b095b04..4d078426 100644 --- a/tests/grid-events/add.js +++ b/tests/grid-events/add.js @@ -1,18 +1,16 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('add: should be triggered after grid.add()', function (assert) { - assert.expect(2); var container = utils.createGridElements(); var grid = new Muuri(container); var newElems = [ document.createElement('div').appendChild(document.createElement('div')).parentNode, - document.createElement('div').appendChild(document.createElement('div')).parentNode + document.createElement('div').appendChild(document.createElement('div')).parentNode, ]; var teardown = function () { grid.destroy(); @@ -21,11 +19,13 @@ grid.on('add', function (items) { assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); - assert.deepEqual(utils.sortedIdList(items), utils.sortedIdList(grid.getItems(newElems)), 'callback: first argument should be an array of the added items'); + assert.deepEqual( + utils.sortedIdList(items), + utils.sortedIdList(grid.getItems(newElems)), + 'callback: first argument should be an array of the added items' + ); }); grid.add(newElems); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/beforeReceive.js b/tests/grid-events/beforeReceive.js index 1a4f6a91..30ca4fff 100644 --- a/tests/grid-events/beforeReceive.js +++ b/tests/grid-events/beforeReceive.js @@ -1,131 +1,216 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('beforeReceive: should be triggered in the beginning of the send procedure (for the receiving grid)', function (assert) { - - assert.expect(11); - - var containerA = utils.createGridElements(); - var containerB = utils.createGridElements(); - var gridA = new Muuri(containerA); - var gridB = new Muuri(containerB); - var item = gridA.getItems()[0]; - var teardown = function () { - gridA.destroy(); - gridB.destroy(); - containerA.parentNode.removeChild(containerA); - containerB.parentNode.removeChild(containerB); - }; - - gridB.on('beforeReceive', function (data) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: the argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 5, 'callback: the argument should have 5 properties'); - assert.strictEqual(data.item, item, 'callback: the argument item property should be the moved item'); - assert.strictEqual(data.fromGrid, gridA, 'callback: the argument fromGrid property should be the sending grid instance'); - assert.strictEqual(data.fromIndex, 0, 'callback: the argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toGrid, gridB, 'callback: the argument toGrid property should be the receiving grid instance'); - assert.strictEqual(data.toIndex, 1, 'callback: the argument toIndex property should be the index where the item was moved to'); - assert.strictEqual(data.toGrid.getItems().indexOf(data.item), -1, 'callback: the item should not be included in the target grid'); - assert.strictEqual(data.fromGrid.getItems().indexOf(data.item), data.fromIndex, 'callback: the item should be included in the source grid'); - assert.strictEqual(data.item.getElement().parentNode, containerA, 'callback: the item element should not be appended to the send container'); - }); - gridA.on('beforeReceive', function () { - assert.ok(false, 'should not be triggered for the sending grid'); - }); - gridA.send(item, gridB, 1, {layout: false}); - teardown(); - - }); - - QUnit.test('beforeSend: should be triggered when an item is dragged into another grid (for the receiving grid)', function (assert) { + QUnit.test( + 'beforeReceive: should be triggered in the beginning of the send procedure (for the receiving grid)', + function (assert) { + assert.expect(11); - assert.expect(11); + var containerA = utils.createGridElements(); + var containerB = utils.createGridElements(); + var gridA = new Muuri(containerA); + var gridB = new Muuri(containerB); + var item = gridA.getItems()[0]; + var teardown = function () { + gridA.destroy(); + gridB.destroy(); + containerA.parentNode.removeChild(containerA); + containerB.parentNode.removeChild(containerB); + }; - var done = assert.async(); - var containerA = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '0px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var containerB = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '70px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var grids = []; - var gridA = new Muuri(containerA, { - dragEnabled: true, - dragSort: function () { - return grids - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var gridB = new Muuri(containerB, { - dragEnabled: true, - dragSort: function () { - return grids - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var item = gridA.getItems()[0]; - var teardown = function () { - gridA.destroy(); - gridB.destroy(); - containerA.parentNode.removeChild(containerA); - containerB.parentNode.removeChild(containerB); - done(); - }; + gridB.on('beforeReceive', function (data) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: the argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 5, + 'callback: the argument should have 5 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: the argument item property should be the moved item' + ); + assert.strictEqual( + data.fromGrid, + gridA, + 'callback: the argument fromGrid property should be the sending grid instance' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: the argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toGrid, + gridB, + 'callback: the argument toGrid property should be the receiving grid instance' + ); + assert.strictEqual( + data.toIndex, + 1, + 'callback: the argument toIndex property should be the index where the item was moved to' + ); + assert.strictEqual( + data.toGrid.getItems().indexOf(data.item), + -1, + 'callback: the item should not be included in the target grid' + ); + assert.strictEqual( + data.fromGrid.getItems().indexOf(data.item), + data.fromIndex, + 'callback: the item should be included in the source grid' + ); + assert.strictEqual( + data.item.getElement().parentNode, + containerA, + 'callback: the item element should not be appended to the send container' + ); + }); + gridA.on('beforeReceive', function () { + assert.ok(false, 'should not be triggered for the sending grid'); + }); + gridA.send(item, gridB, 1, { layout: false }); + teardown(); + } + ); - grids.push(gridA, gridB); + QUnit.test( + 'beforeSend: should be triggered when an item is dragged into another grid (for the receiving grid)', + function (assert) { + assert.expect(11); - gridB.on('beforeReceive', function (data) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: the argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 5, 'callback: the argument should have 5 properties'); - assert.strictEqual(data.item, item, 'callback: the argument item property should be the dragged item'); - assert.strictEqual(data.fromGrid, gridA, 'callback: the argument fromGrid property should be the sending grid instance'); - assert.strictEqual(data.fromIndex, 0, 'callback: the argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toGrid, gridB, 'callback: the argument toGrid property should be the receiving grid instance'); - assert.strictEqual(data.toIndex, 0, 'callback: the argument toIndex property should be the index where the item was moved to'); - assert.strictEqual(data.toGrid.getItems().indexOf(data.item), -1, 'callback: the item should not be included in the target grid'); - assert.strictEqual(data.fromGrid.getItems().indexOf(data.item), data.fromIndex, 'callback: the item should be included in the source grid'); - assert.strictEqual(data.item.isDragging(), true, 'callback: the item should be in dragging state'); - }); + var done = assert.async(); + var containerA = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '0px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var containerB = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '70px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var grids = []; + var gridA = new Muuri(containerA, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var gridB = new Muuri(containerB, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var item = gridA.getItems()[0]; + var teardown = function () { + gridA.destroy(); + gridB.destroy(); + containerA.parentNode.removeChild(containerA); + containerB.parentNode.removeChild(containerB); + done(); + }; - utils.dragElement(item.getElement(), 70, 0, teardown); + grids.push(gridA, gridB); - }); + gridB.on('beforeReceive', function (data) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: the argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 5, + 'callback: the argument should have 5 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: the argument item property should be the dragged item' + ); + assert.strictEqual( + data.fromGrid, + gridA, + 'callback: the argument fromGrid property should be the sending grid instance' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: the argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toGrid, + gridB, + 'callback: the argument toGrid property should be the receiving grid instance' + ); + assert.strictEqual( + data.toIndex, + 0, + 'callback: the argument toIndex property should be the index where the item was moved to' + ); + assert.strictEqual( + data.toGrid.getItems().indexOf(data.item), + -1, + 'callback: the item should not be included in the target grid' + ); + assert.strictEqual( + data.fromGrid.getItems().indexOf(data.item), + data.fromIndex, + 'callback: the item should be included in the source grid' + ); + assert.strictEqual( + data.item.isDragging(), + true, + 'callback: the item should be in dragging state' + ); + }); -})(this); \ No newline at end of file + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 0, + onFinished: teardown, + }); + } + ); +})(this); diff --git a/tests/grid-events/beforeSend.js b/tests/grid-events/beforeSend.js index 27a1c15d..ba81fd27 100644 --- a/tests/grid-events/beforeSend.js +++ b/tests/grid-events/beforeSend.js @@ -1,131 +1,216 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('beforeSend: should be triggered in the beginning of the send procedure (for the sending grid)', function (assert) { - - assert.expect(11); - - var containerA = utils.createGridElements(); - var containerB = utils.createGridElements(); - var gridA = new Muuri(containerA); - var gridB = new Muuri(containerB); - var item = gridA.getItems()[0]; - var teardown = function () { - gridA.destroy(); - gridB.destroy(); - containerA.parentNode.removeChild(containerA); - containerB.parentNode.removeChild(containerB); - }; - - gridA.on('beforeSend', function (data) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: the argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 5, 'callback: the argument should have 5 properties'); - assert.strictEqual(data.item, item, 'callback: the argument item property should be the moved item'); - assert.strictEqual(data.fromGrid, gridA, 'callback: the argument fromGrid property should be the sending grid instance'); - assert.strictEqual(data.fromIndex, 0, 'callback: the argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toGrid, gridB, 'callback: the argument toGrid property should be the receiving grid instance'); - assert.strictEqual(data.toIndex, 1, 'callback: the argument toIndex property should be the index where the item was moved to'); - assert.strictEqual(data.toGrid.getItems().indexOf(data.item), -1, 'callback: the item should not be included in the target grid'); - assert.strictEqual(data.fromGrid.getItems().indexOf(data.item), data.fromIndex, 'callback: the item should be included in the source grid'); - assert.strictEqual(data.item.getElement().parentNode, containerA, 'callback: the item element should not be appended to the send container'); - }); - gridB.on('beforeSend', function () { - assert.ok(false, 'should not be triggered for the receiving grid'); - }); - gridA.send(item, gridB, 1, {layout: false}); - teardown(); - - }); - - QUnit.test('beforeSend: should be triggered when an item is dragged into another grid (for the sending grid)', function (assert) { + QUnit.test( + 'beforeSend: should be triggered in the beginning of the send procedure (for the sending grid)', + function (assert) { + assert.expect(11); - assert.expect(11); + var containerA = utils.createGridElements(); + var containerB = utils.createGridElements(); + var gridA = new Muuri(containerA); + var gridB = new Muuri(containerB); + var item = gridA.getItems()[0]; + var teardown = function () { + gridA.destroy(); + gridB.destroy(); + containerA.parentNode.removeChild(containerA); + containerB.parentNode.removeChild(containerB); + }; - var done = assert.async(); - var containerA = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '0px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var containerB = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '70px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var grids = []; - var gridA = new Muuri(containerA, { - dragEnabled: true, - dragSort: function () { - return grids - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var gridB = new Muuri(containerB, { - dragEnabled: true, - dragSort: function () { - return grids - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var item = gridA.getItems()[0]; - var teardown = function () { - gridA.destroy(); - gridB.destroy(); - containerA.parentNode.removeChild(containerA); - containerB.parentNode.removeChild(containerB); - done(); - }; + gridA.on('beforeSend', function (data) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: the argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 5, + 'callback: the argument should have 5 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: the argument item property should be the moved item' + ); + assert.strictEqual( + data.fromGrid, + gridA, + 'callback: the argument fromGrid property should be the sending grid instance' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: the argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toGrid, + gridB, + 'callback: the argument toGrid property should be the receiving grid instance' + ); + assert.strictEqual( + data.toIndex, + 1, + 'callback: the argument toIndex property should be the index where the item was moved to' + ); + assert.strictEqual( + data.toGrid.getItems().indexOf(data.item), + -1, + 'callback: the item should not be included in the target grid' + ); + assert.strictEqual( + data.fromGrid.getItems().indexOf(data.item), + data.fromIndex, + 'callback: the item should be included in the source grid' + ); + assert.strictEqual( + data.item.getElement().parentNode, + containerA, + 'callback: the item element should not be appended to the send container' + ); + }); + gridB.on('beforeSend', function () { + assert.ok(false, 'should not be triggered for the receiving grid'); + }); + gridA.send(item, gridB, 1, { layout: false }); + teardown(); + } + ); - grids.push(gridA, gridB); + QUnit.test( + 'beforeSend: should be triggered when an item is dragged into another grid (for the sending grid)', + function (assert) { + assert.expect(11); - gridA.on('beforeSend', function (data) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: the argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 5, 'callback: the argument should have 5 properties'); - assert.strictEqual(data.item, item, 'callback: the argument item property should be the dragged item'); - assert.strictEqual(data.fromGrid, gridA, 'callback: the argument fromGrid property should be the sending grid instance'); - assert.strictEqual(data.fromIndex, 0, 'callback: the argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toGrid, gridB, 'callback: the argument toGrid property should be the receiving grid instance'); - assert.strictEqual(data.toIndex, 0, 'callback: the argument toIndex property should be the index where the item was moved to'); - assert.strictEqual(data.toGrid.getItems().indexOf(data.item), -1, 'callback: the item should not be included in the target grid'); - assert.strictEqual(data.fromGrid.getItems().indexOf(data.item), data.fromIndex, 'callback: the item should be included in the source grid'); - assert.strictEqual(data.item.isDragging(), true, 'callback: the item should be in dragging state'); - }); + var done = assert.async(); + var containerA = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '0px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var containerB = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '70px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var grids = []; + var gridA = new Muuri(containerA, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var gridB = new Muuri(containerB, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var item = gridA.getItems()[0]; + var teardown = function () { + gridA.destroy(); + gridB.destroy(); + containerA.parentNode.removeChild(containerA); + containerB.parentNode.removeChild(containerB); + done(); + }; - utils.dragElement(item.getElement(), 70, 0, teardown); + grids.push(gridA, gridB); - }); + gridA.on('beforeSend', function (data) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: the argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 5, + 'callback: the argument should have 5 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: the argument item property should be the dragged item' + ); + assert.strictEqual( + data.fromGrid, + gridA, + 'callback: the argument fromGrid property should be the sending grid instance' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: the argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toGrid, + gridB, + 'callback: the argument toGrid property should be the receiving grid instance' + ); + assert.strictEqual( + data.toIndex, + 0, + 'callback: the argument toIndex property should be the index where the item was moved to' + ); + assert.strictEqual( + data.toGrid.getItems().indexOf(data.item), + -1, + 'callback: the item should not be included in the target grid' + ); + assert.strictEqual( + data.fromGrid.getItems().indexOf(data.item), + data.fromIndex, + 'callback: the item should be included in the source grid' + ); + assert.strictEqual( + data.item.isDragging(), + true, + 'callback: the item should be in dragging state' + ); + }); -})(this); \ No newline at end of file + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 0, + onFinished: teardown, + }); + } + ); +})(this); diff --git a/tests/grid-events/destroy.js b/tests/grid-events/destroy.js index e5dc7e92..b44bbaec 100644 --- a/tests/grid-events/destroy.js +++ b/tests/grid-events/destroy.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('destroy: should be triggered after grid.destroy()', function (assert) { - assert.expect(2); var container = utils.createGridElements(); @@ -21,9 +19,11 @@ ++calls; }); grid.destroy().destroy().destroy(); - assert.strictEqual(calls, 1, 'should be called only once no matter how many times grid.destroy() is called'); + assert.strictEqual( + calls, + 1, + 'should be called only once no matter how many times grid.destroy() is called' + ); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/dragEnd.js b/tests/grid-events/dragEnd.js index 0037c1d2..af21cadd 100644 --- a/tests/grid-events/dragEnd.js +++ b/tests/grid-events/dragEnd.js @@ -1,16 +1,14 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('dragEnd: should be triggered when item is dragged', function (assert) { - assert.expect(9); var done = assert.async(); var container = utils.createGridElements(); - var grid = new Muuri(container, {dragEnabled: true}); + var grid = new Muuri(container, { dragEnabled: true }); var item = grid.getItems()[0]; var calls = 0; var isStartCalled = false; @@ -35,7 +33,11 @@ assert.strictEqual(isStartCalled, true, 'callback: should be called after dragStart'); assert.strictEqual(isMoveCalled, true, 'callback: should be called after dragMove'); - assert.strictEqual(utils.isDraggerEvent(ev), true, 'callback: second argument should be a Dragger event object'); + assert.strictEqual( + utils.isDraggerEvent(ev), + true, + 'callback: second argument should be a Dragger event object' + ); assert.strictEqual(ev.isFirst, false, 'event.isFirst should be false'); assert.strictEqual(ev.isFinal, true, 'event.isFinal should be true'); assert.strictEqual(ev.type, 'end', 'event.type should be "end"'); @@ -48,8 +50,10 @@ teardown(); }); - utils.dragElement(item.getElement(), 100, 100); - + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/dragInit.js b/tests/grid-events/dragInit.js index e427a9b2..6945643d 100644 --- a/tests/grid-events/dragInit.js +++ b/tests/grid-events/dragInit.js @@ -1,46 +1,66 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('dragInit: should be triggered when dragging starts (in the beginning of the drag start process)', function (assert) { - - assert.expect(9); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container, { - dragEnabled: true, - dragContainer: document.body - }); - var item = grid.getItems()[0]; - var calls = 0; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.on('dragInit', function (draggedItem, ev) { - assert.strictEqual(arguments.length, 2, 'callback: should have receive two arguments'); - assert.strictEqual(draggedItem, item, 'callback: first argument should be the dragged item'); - assert.strictEqual(utils.matches(draggedItem.getElement(), '.muuri-item-dragging'), false, 'should be called before dragging classname is set'); - assert.strictEqual(draggedItem.getElement().parentNode, container, 'should be called before dragged element is appended to it`s drag container'); + QUnit.test( + 'dragInit: should be triggered when dragging starts (in the beginning of the drag start process)', + function (assert) { + assert.expect(9); - assert.strictEqual(utils.isDraggerEvent(ev), true, 'callback: second argument should be a Dragger event object'); - assert.strictEqual(ev.isFirst, true, 'event.isFirst should be true'); - assert.strictEqual(ev.isFinal, false, 'event.isFinal should be false'); - assert.strictEqual(ev.type, 'start', 'event.type should be "start"'); + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container, { + dragEnabled: true, + dragContainer: document.body, + }); + var item = grid.getItems()[0]; + var calls = 0; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; - ++calls; - }); + grid.on('dragInit', function (draggedItem, ev) { + assert.strictEqual(arguments.length, 2, 'callback: should have receive two arguments'); + assert.strictEqual( + draggedItem, + item, + 'callback: first argument should be the dragged item' + ); + assert.strictEqual( + utils.matches(draggedItem.getElement(), '.muuri-item-dragging'), + false, + 'should be called before dragging classname is set' + ); + assert.strictEqual( + draggedItem.getElement().parentNode, + container, + 'should be called before dragged element is appended to it`s drag container' + ); - utils.dragElement(item.getElement(), 100, 100, function () { - assert.strictEqual(calls, 1, 'should be called only once during drag process'); - teardown(); - }); + assert.strictEqual( + utils.isDraggerEvent(ev), + true, + 'callback: second argument should be a Dragger event object' + ); + assert.strictEqual(ev.isFirst, true, 'event.isFirst should be true'); + assert.strictEqual(ev.isFinal, false, 'event.isFinal should be false'); + assert.strictEqual(ev.type, 'start', 'event.type should be "start"'); - }); + ++calls; + }); -})(this); \ No newline at end of file + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + onFinished: function () { + assert.strictEqual(calls, 1, 'should be called only once during drag process'); + teardown(); + }, + }); + } + ); +})(this); diff --git a/tests/grid-events/dragMove.js b/tests/grid-events/dragMove.js index d9d943d8..4b3d48b2 100644 --- a/tests/grid-events/dragMove.js +++ b/tests/grid-events/dragMove.js @@ -1,16 +1,14 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('dragMove: should be triggered when item is dragged', function (assert) { - assert.expect(8); var done = assert.async(); var container = utils.createGridElements(); - var grid = new Muuri(container, {dragEnabled: true}); + var grid = new Muuri(container, { dragEnabled: true }); var item = grid.getItems()[0]; var calls = 0; var isStartCalled = false; @@ -27,10 +25,18 @@ grid.on('dragMove', function (draggedItem, ev) { if (!calls) { assert.strictEqual(arguments.length, 2, 'callback: should have receive two arguments'); - assert.strictEqual(draggedItem, item, 'callback: first argument should be the dragged item'); + assert.strictEqual( + draggedItem, + item, + 'callback: first argument should be the dragged item' + ); assert.strictEqual(isStartCalled, true, 'callback: should be called after dragStart'); - assert.strictEqual(utils.isDraggerEvent(ev), true, 'callback: second argument should be a Dragger event object'); + assert.strictEqual( + utils.isDraggerEvent(ev), + true, + 'callback: second argument should be a Dragger event object' + ); assert.strictEqual(ev.isFirst, false, 'event.isFirst should be false'); assert.strictEqual(ev.isFinal, false, 'event.isFinal should be false'); assert.strictEqual(ev.type, 'move', 'event.type should be "move"'); @@ -38,11 +44,14 @@ ++calls; }); - utils.dragElement(item.getElement(), 100, 100, function () { - assert.strictEqual(calls > 1, true, 'should be called many times during drag process'); - teardown(); + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + onFinished: function () { + assert.strictEqual(calls > 1, true, 'should be called many times during drag process'); + teardown(); + }, }); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/dragReleaseEnd.js b/tests/grid-events/dragReleaseEnd.js index c767946e..21e8e1f7 100644 --- a/tests/grid-events/dragReleaseEnd.js +++ b/tests/grid-events/dragReleaseEnd.js @@ -1,18 +1,17 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('dragReleaseEnd: should be triggered when item has positioned after drag', function (assert) { - + QUnit.test('dragReleaseEnd: should be triggered when item has positioned after drag', function ( + assert + ) { assert.expect(2); var done = assert.async(); var container = utils.createGridElements(); - var grid = new Muuri(container, {dragEnabled: true}); + var grid = new Muuri(container, { dragEnabled: true }); var item = grid.getItems()[0]; - var calls = 0; var teardown = function () { grid.destroy(); container.parentNode.removeChild(container); @@ -25,8 +24,10 @@ teardown(); }); - utils.dragElement(item.getElement(), 100, 100); - + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/dragReleaseStart.js b/tests/grid-events/dragReleaseStart.js index 41995fc3..30719a25 100644 --- a/tests/grid-events/dragReleaseStart.js +++ b/tests/grid-events/dragReleaseStart.js @@ -1,16 +1,16 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('dragReleaseStart: should be triggered when item is released after drag', function (assert) { - + QUnit.test('dragReleaseStart: should be triggered when item is released after drag', function ( + assert + ) { assert.expect(2); var done = assert.async(); var container = utils.createGridElements(); - var grid = new Muuri(container, {dragEnabled: true}); + var grid = new Muuri(container, { dragEnabled: true }); var item = grid.getItems()[0]; var teardown = function () { grid.destroy(); @@ -27,8 +27,10 @@ teardown(); }); - utils.dragElement(item.getElement(), 100, 100); - + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/dragScroll.js b/tests/grid-events/dragScroll.js index f0bd764f..e222b5b9 100644 --- a/tests/grid-events/dragScroll.js +++ b/tests/grid-events/dragScroll.js @@ -1,11 +1,11 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('dragScroll: should be triggered when scroll occurs during drag process', function (assert) { - + QUnit.test('dragScroll: should be triggered when scroll occurs during drag process', function ( + assert + ) { assert.expect(4); var done = assert.async(); @@ -17,22 +17,20 @@ dragSortInterval: 100, dragSortPredicate: { threshold: 50, - action: 'move' - } + action: 'move', + }, }); var item = grid.getItems()[0]; var calls = 0; - var isStartCalled = false; - var isMoveCalled = false; var teardown = function () { grid.destroy(); container.parentNode.removeChild(container); - utils.setStyles(docElem, {height: ''}); + utils.setStyles(docElem, { height: '' }); body.scrollTop = 0; done(); }; - utils.setStyles(docElem, {height: '1000%'}); + utils.setStyles(docElem, { height: '1000%' }); grid.on('dragStart', function () { body.scrollTop = 100; @@ -42,15 +40,22 @@ grid.on('dragScroll', function (draggedItem, ev) { assert.strictEqual(arguments.length, 2, 'callback: should have receive two arguments'); assert.strictEqual(draggedItem, item, 'callback: first argument should be the dragged item'); - assert.strictEqual(utils.isScrollEvent(ev), true, 'callback: second argument should be a scroll event object'); + assert.strictEqual( + utils.isScrollEvent(ev), + true, + 'callback: second argument should be a scroll event object' + ); ++calls; }); - utils.dragElement(item.getElement(), 0, 100, function () { - assert.strictEqual(calls, 1, 'should be called only once'); - teardown(); + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 100, + onFinished: function () { + assert.strictEqual(calls, 1, 'should be called only once'); + teardown(); + }, }); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/dragStart.js b/tests/grid-events/dragStart.js index 0c7b9f7f..2ce5866c 100644 --- a/tests/grid-events/dragStart.js +++ b/tests/grid-events/dragStart.js @@ -1,46 +1,66 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('dragStart: should be triggered when dragging starts (in the end of the drag start process)', function (assert) { - - assert.expect(9); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container, { - dragEnabled: true, - dragContainer: document.body - }); - var item = grid.getItems()[0]; - var calls = 0; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.on('dragStart', function (draggedItem, ev) { - assert.strictEqual(arguments.length, 2, 'callback: should have receive two arguments'); - assert.strictEqual(draggedItem, item, 'callback: first argument should be the dragged item'); - assert.strictEqual(utils.matches(draggedItem.getElement(), '.muuri-item-dragging'), true, 'should be called after dragging classname is set'); - assert.strictEqual(draggedItem.getElement().parentNode, document.body, 'should be called after dragged element is appended to it`s drag container'); + QUnit.test( + 'dragStart: should be triggered when dragging starts (in the end of the drag start process)', + function (assert) { + assert.expect(9); - assert.strictEqual(utils.isDraggerEvent(ev), true, 'callback: second argument should be a Dragger event object'); - assert.strictEqual(ev.isFirst, true, 'event.isFirst should be true'); - assert.strictEqual(ev.isFinal, false, 'event.isFinal should be false'); - assert.strictEqual(ev.type, 'start', 'event.type should be "start"'); + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container, { + dragEnabled: true, + dragContainer: document.body, + }); + var item = grid.getItems()[0]; + var calls = 0; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; - ++calls; - }); + grid.on('dragStart', function (draggedItem, ev) { + assert.strictEqual(arguments.length, 2, 'callback: should have receive two arguments'); + assert.strictEqual( + draggedItem, + item, + 'callback: first argument should be the dragged item' + ); + assert.strictEqual( + utils.matches(draggedItem.getElement(), '.muuri-item-dragging'), + true, + 'should be called after dragging classname is set' + ); + assert.strictEqual( + draggedItem.getElement().parentNode, + document.body, + 'should be called after dragged element is appended to it`s drag container' + ); - utils.dragElement(item.getElement(), 100, 100, function () { - assert.strictEqual(calls, 1, 'should be called only once during drag process'); - teardown(); - }); + assert.strictEqual( + utils.isDraggerEvent(ev), + true, + 'callback: second argument should be a Dragger event object' + ); + assert.strictEqual(ev.isFirst, true, 'event.isFirst should be true'); + assert.strictEqual(ev.isFinal, false, 'event.isFinal should be false'); + assert.strictEqual(ev.type, 'start', 'event.type should be "start"'); - }); + ++calls; + }); -})(this); \ No newline at end of file + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + onFinished: function () { + assert.strictEqual(calls, 1, 'should be called only once during drag process'); + teardown(); + }, + }); + } + ); +})(this); diff --git a/tests/grid-events/draggerEvent.js b/tests/grid-events/draggerEvent.js index 11ce001c..3a0c818c 100644 --- a/tests/grid-events/draggerEvent.js +++ b/tests/grid-events/draggerEvent.js @@ -1,16 +1,14 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('draggerEvent interface', function (assert) { - assert.expect(35); var done = assert.async(); var container = utils.createGridElements(); - var grid = new Muuri(container, {dragEnabled: true}); + var grid = new Muuri(container, { dragEnabled: true }); var item = grid.getItems()[0]; var evStart, evMove1, evMove2, evEnd; var startClientX, startClientY; @@ -35,7 +33,11 @@ } if (!srcInterface) { - assert.strictEqual(true, false, 'No matching touch/event interface found from source event'); + assert.strictEqual( + true, + false, + 'No matching touch/event interface found from source event' + ); } var dX = srcInterface.clientX - startClientX; @@ -81,26 +83,30 @@ }); grid.on('dragReleaseEnd', function () { - var hasUniqueEvents = evStart !== evMove1 && - evStart !== evMove2 && - evStart !== evEnd && - evMove1 !== evMove2 && - evMove1 !== evEnd && - evMove2 !== evEnd; + var hasUniqueEvents = + evStart !== evMove1 && + evStart !== evMove2 && + evStart !== evEnd && + evMove1 !== evMove2 && + evMove1 !== evEnd && + evMove2 !== evEnd; assert.strictEqual(hasUniqueEvents, true, 'event objects should not be pooled'); - var hasSameId = evStart.identifier === evMove1.identifier && - evStart.identifier === evMove2.identifier && - evStart.identifier === evEnd.identifier; + var hasSameId = + evStart.identifier === evMove1.identifier && + evStart.identifier === evMove2.identifier && + evStart.identifier === evEnd.identifier; assert.strictEqual(hasSameId, true, 'identifier should be same for all events'); teardown(); }); - utils.dragElement(item.getElement(), 100, 100); - + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/filter.js b/tests/grid-events/filter.js index fd72c2b3..bfb35155 100644 --- a/tests/grid-events/filter.js +++ b/tests/grid-events/filter.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('filter: should be triggered after grid.filter()', function (assert) { - assert.expect(3); var container = utils.createGridElements(); @@ -19,15 +17,20 @@ grid.on('filter', function (shownItems, hiddenItems) { assert.strictEqual(arguments.length, 2, 'callback: should have two arguments'); - assert.deepEqual(utils.sortedIdList(shownItems), utils.sortedIdList(itemsToShow), 'callback: array of shown items should be the first argument'); - assert.deepEqual(utils.sortedIdList(hiddenItems), utils.sortedIdList(itemsToHide), 'callback: array of hidden items should be the second argument'); + assert.deepEqual( + utils.sortedIdList(shownItems), + utils.sortedIdList(itemsToShow), + 'callback: array of shown items should be the first argument' + ); + assert.deepEqual( + utils.sortedIdList(hiddenItems), + utils.sortedIdList(itemsToHide), + 'callback: array of hidden items should be the second argument' + ); }); grid.filter(function (item) { return itemsToShow.indexOf(item) > -1; }); teardown(); - - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/hideEnd.js b/tests/grid-events/hideEnd.js index 8ced68e8..479fdd4a 100644 --- a/tests/grid-events/hideEnd.js +++ b/tests/grid-events/hideEnd.js @@ -1,30 +1,33 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('hideEnd: should be triggered after grid.hide() (after the hiding is finished)', function (assert) { - - assert.expect(2); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.hide(0, {layout: false, instant: true}); - grid.on('hideEnd', function (items) { - assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); - assert.deepEqual(utils.sortedIdList(items), utils.sortedIdList(grid.getItems([0, 1, 2])), 'callback: first argument should be an array of all the items that are were hidden'); - teardown(); - }); - grid.hide([0, 1, 2], {layout: false}); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'hideEnd: should be triggered after grid.hide() (after the hiding is finished)', + function (assert) { + assert.expect(2); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.hide(grid.getItems(0), { layout: false, instant: true, syncWithLayout: false }); + grid.on('hideEnd', function (items) { + assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); + assert.deepEqual( + utils.sortedIdList(items), + utils.sortedIdList(grid.getItems([0, 1, 2])), + 'callback: first argument should be an array of all the items that are were hidden' + ); + teardown(); + }); + grid.hide(grid.getItems([0, 1, 2]), { layout: false, syncWithLayout: false }); + } + ); +})(this); diff --git a/tests/grid-events/hideStart.js b/tests/grid-events/hideStart.js index 86daf3d0..e9a91508 100644 --- a/tests/grid-events/hideStart.js +++ b/tests/grid-events/hideStart.js @@ -1,28 +1,31 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('hideStart: should be triggered after grid.hide() (before the showing starts)', function (assert) { - - assert.expect(2); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - grid.hide(0, {layout: false, instant: true}); - grid.on('hideStart', function (items) { - assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); - assert.deepEqual(utils.sortedIdList(items), utils.sortedIdList(grid.getItems([0, 1, 2])), 'callback: first argument should be an array of all the items that are about to be hidden'); - }); - grid.hide([0, 1, 2], {layout: false}); - teardown(); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'hideStart: should be triggered after grid.hide() (before the showing starts)', + function (assert) { + assert.expect(2); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + grid.hide(grid.getItems(0), { layout: false, instant: true, syncWithLayout: false }); + grid.on('hideStart', function (items) { + assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); + assert.deepEqual( + utils.sortedIdList(items), + utils.sortedIdList(grid.getItems([0, 1, 2])), + 'callback: first argument should be an array of all the items that are about to be hidden' + ); + }); + grid.hide(grid.getItems([0, 1, 2]), { layout: false, syncWithLayout: false }); + teardown(); + } + ); +})(this); diff --git a/tests/grid-events/layoutAbort.js b/tests/grid-events/layoutAbort.js new file mode 100644 index 00000000..fc4cea7d --- /dev/null +++ b/tests/grid-events/layoutAbort.js @@ -0,0 +1,52 @@ +(function (window) { + var Muuri = window.Muuri; + + QUnit.module('Grid events'); + + QUnit.test( + 'layoutAbort: should be emitted before layoutStart if current layout process is aborted', + function (assert) { + assert.expect(8); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var expectedItems = []; + var firstItem = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.on('layoutAbort', function (items) { + assert.strictEqual(arguments.length, 1, 'callback: should have a single argument'); + assert.deepEqual( + utils.idList(items), + utils.idList(expectedItems), + 'callback: first argument should be an array of items that were active when the layout was triggered' + ); + }); + + grid.move(1, -1); + + // Abort #1 + expectedItems = utils.getActiveItems(grid); + grid.move(1, -1); + + // Abort #2 + expectedItems = utils.getActiveItems(grid); + grid.hide([firstItem]); + + // Abort #3 + expectedItems = utils.getActiveItems(grid); + grid.show([firstItem]); + + // Abort #4 + expectedItems = utils.getActiveItems(grid); + grid.hide([firstItem]); + + teardown(); + } + ); +})(this); diff --git a/tests/grid-events/layoutEnd.js b/tests/grid-events/layoutEnd.js index b57b1c1a..03259e8b 100644 --- a/tests/grid-events/layoutEnd.js +++ b/tests/grid-events/layoutEnd.js @@ -1,39 +1,46 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('layoutEnd: should be triggered after grid.layout() (after the items have positioned)', function (assert) { - - assert.expect(3); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var isAnyItemPositioning = false; - var expectedItems = []; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.on('layoutEnd', function (items) { - items.forEach(function (item) { - if (item.isPositioning()) { - isAnyItemPositioning = true; - } + QUnit.test( + 'layoutEnd: should be triggered after grid.layout() (after the items have positioned)', + function (assert) { + assert.expect(3); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var isAnyItemPositioning = false; + var expectedItems = []; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.on('layoutEnd', function (items) { + items.forEach(function (item) { + if (item.isPositioning()) { + isAnyItemPositioning = true; + } + }); + assert.strictEqual(arguments.length, 1, 'callback: should have a single argument'); + assert.deepEqual( + utils.sortedIdList(items), + utils.sortedIdList(expectedItems), + 'callback: first argument should be an array of items that were active when the layout was triggered' + ); + assert.strictEqual( + isAnyItemPositioning, + false, + 'callback: none of the items in the first argument should be in positioning state' + ); + teardown(); }); - assert.strictEqual(arguments.length, 1, 'callback: should have a single argument'); - assert.deepEqual(utils.sortedIdList(items), utils.sortedIdList(expectedItems), 'callback: first argument should be an array of items that were active when the layout was triggered'); - assert.strictEqual(isAnyItemPositioning, false, 'callback: none of the items in the first argument should be in positioning state'); - teardown(); - }); - grid.hide(0, {instant: true, layout: false}); - expectedItems = utils.getActiveItems(grid); - grid.move(1, -1); - - }); - -})(this); \ No newline at end of file + grid.hide(grid.getItems(0), { instant: true, layout: false, syncWithLayout: false }); + expectedItems = utils.getActiveItems(grid); + grid.move(1, -1); + } + ); +})(this); diff --git a/tests/grid-events/layoutStart.js b/tests/grid-events/layoutStart.js index 30715ebb..34d487fb 100644 --- a/tests/grid-events/layoutStart.js +++ b/tests/grid-events/layoutStart.js @@ -1,31 +1,64 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('layoutStart: should be triggered after grid.layout() (before the items are positioned)', function (assert) { - - assert.expect(4); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var layoutId = grid._layout.id; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - utils.setStyles(container, {height: ''}); - grid.on('layoutStart', function (items) { - assert.strictEqual(arguments.length, 1, 'should have one argument'); - assert.deepEqual(utils.sortedIdList(items), utils.sortedIdList(utils.getActiveItems(grid)), 'should be an array of the items that are about to be laid out'); - assert.notStrictEqual(grid._layout.id, layoutId, 'should be called after layout is created'); - assert.notStrictEqual(container.style.height, '', 'should be called after container dimensions are updated'); - }); - grid.move(0, -1); - teardown(); + QUnit.test( + 'layoutStart: should be triggered after grid.layout() (before the items are positioned)', + function (assert) { + assert.expect(6); - }); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var layoutId = grid._layout.id; + var numEvents = 0; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; -})(this); \ No newline at end of file + utils.setStyles(container, { height: '' }); + grid.on('layoutStart', function (items, isInstant) { + ++numEvents; + if (numEvents === 1) { + assert.strictEqual(arguments.length, 2, 'should have two arguments'); + assert.deepEqual( + utils.sortedIdList(items), + utils.sortedIdList(utils.getActiveItems(grid)), + 'first argument should be an array of the items that are about to be laid out' + ); + assert.strictEqual( + isInstant, + false, + 'second argument should be false when layout was not called with instant flag' + ); + assert.notStrictEqual( + grid._layout.id, + layoutId, + 'should be called after layout is created' + ); + assert.notStrictEqual( + container.style.height, + '', + 'should be called after container dimensions are updated' + ); + } else if (numEvents === 2) { + assert.strictEqual( + isInstant, + true, + 'second argument should be true when layout was called with instant flag' + ); + } else { + assert.strictEqual( + true, + false, + 'there should be one event per layout call, even if ongoing layout is aborted' + ); + } + }); + grid.move(0, -1); + grid.layout(true); + teardown(); + } + ); +})(this); diff --git a/tests/grid-events/move.js b/tests/grid-events/move.js index 633ff9c2..74691d4b 100644 --- a/tests/grid-events/move.js +++ b/tests/grid-events/move.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('move: should be triggered after grid.move()', function (assert) { - assert.expect(7); var container = utils.createGridElements(); @@ -18,36 +16,58 @@ grid.on('move', function (data) { assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: first argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 4, 'callback: first argument should have 4 properties'); - assert.strictEqual(data.item, item, 'callback: first argument item property should be the moved item'); - assert.strictEqual(data.action, 'move', 'callback: first argument action property should be the correct action'); - assert.strictEqual(data.fromIndex, 0, 'callback: first argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toIndex, 1, 'callback: first argument toIndex property should be the index where the item was moved to'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: first argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 4, + 'callback: first argument should have 4 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: first argument item property should be the moved item' + ); + assert.strictEqual( + data.action, + 'move', + 'callback: first argument action property should be the correct action' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: first argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toIndex, + 1, + 'callback: first argument toIndex property should be the index where the item was moved to' + ); }); - grid.move(item, 1, {layout: false}); + grid.move(item, 1, { layout: false }); teardown(); - }); QUnit.test('move: should be triggered when sorting occurs during drag', function (assert) { - assert.expect(7); var done = assert.async(); var container = utils.createGridElements({ containerStyles: { position: 'relative', - width: '70px' - } + width: '70px', + }, }); var grid = new Muuri(container, { dragEnabled: true, dragSortInterval: 100, dragSortPredicate: { threshold: 50, - action: 'move' - } + action: 'move', + }, }); var item = grid.getItems()[0]; var teardown = function () { @@ -58,16 +78,43 @@ grid.on('move', function (data) { assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: the argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 4, 'callback: the argument should have 4 properties'); - assert.strictEqual(data.item, item, 'callback: the argument item property should be the moved item'); - assert.strictEqual(data.action, 'move', 'callback: the argument action property should be the correct action'); - assert.strictEqual(data.fromIndex, 0, 'callback: the argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toIndex, 1, 'callback: the argument toIndex property should be the index where the item was moved to'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: the argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 4, + 'callback: the argument should have 4 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: the argument item property should be the moved item' + ); + assert.strictEqual( + data.action, + 'move', + 'callback: the argument action property should be the correct action' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: the argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toIndex, + 1, + 'callback: the argument toIndex property should be the index where the item was moved to' + ); }); - utils.dragElement(item.getElement(), 0, 70, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/receive.js b/tests/grid-events/receive.js index 18754a55..72580912 100644 --- a/tests/grid-events/receive.js +++ b/tests/grid-events/receive.js @@ -1,131 +1,216 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('receive: should be triggered in the end of the send procedure (for the receiving grid)', function (assert) { - - assert.expect(11); - - var containerA = utils.createGridElements(); - var containerB = utils.createGridElements(); - var gridA = new Muuri(containerA); - var gridB = new Muuri(containerB); - var item = gridA.getItems()[0]; - var teardown = function () { - gridA.destroy(); - gridB.destroy(); - containerA.parentNode.removeChild(containerA); - containerB.parentNode.removeChild(containerB); - }; - - gridB.on('receive', function (data) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: the argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 5, 'callback: the argument should have 5 properties'); - assert.strictEqual(data.item, item, 'callback: the argument item property should be the moved item'); - assert.strictEqual(data.fromGrid, gridA, 'callback: the argument fromGrid property should be the sending grid instance'); - assert.strictEqual(data.fromIndex, 0, 'callback: the argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toGrid, gridB, 'callback: the argument toGrid property should be the receiving grid instance'); - assert.strictEqual(data.toIndex, 1, 'callback: the argument toIndex property should be the index where the item was moved to'); - assert.strictEqual(data.toGrid.getItems().indexOf(data.item), data.toIndex, 'callback: the item should be included in the target grid in correct position'); - assert.strictEqual(data.fromGrid.getItems().indexOf(data.item), -1, 'callback: the item should not be included in the source grid'); - assert.strictEqual(data.item.getElement().parentNode, document.body, 'callback: the item element should be appended to the send container'); - }); - gridA.on('receive', function () { - assert.ok(false, 'should not be triggered for the sending grid'); - }); - gridA.send(item, gridB, 1, {layout: false}); - teardown(); - - }); - - QUnit.test('receive: should be triggered when an item is dragged into another grid (for the receiving grid)', function (assert) { + QUnit.test( + 'receive: should be triggered in the end of the send procedure (for the receiving grid)', + function (assert) { + assert.expect(11); - assert.expect(11); + var containerA = utils.createGridElements(); + var containerB = utils.createGridElements(); + var gridA = new Muuri(containerA); + var gridB = new Muuri(containerB); + var item = gridA.getItems()[0]; + var teardown = function () { + gridA.destroy(); + gridB.destroy(); + containerA.parentNode.removeChild(containerA); + containerB.parentNode.removeChild(containerB); + }; - var done = assert.async(); - var containerA = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '0px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var containerB = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '70px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var grids = []; - var gridA = new Muuri(containerA, { - dragEnabled: true, - dragSort: function () { - return grids - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var gridB = new Muuri(containerB, { - dragEnabled: true, - dragSort: function () { - return grids - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var item = gridA.getItems()[0]; - var teardown = function () { - gridA.destroy(); - gridB.destroy(); - containerA.parentNode.removeChild(containerA); - containerB.parentNode.removeChild(containerB); - done(); - }; + gridB.on('receive', function (data) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: the argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 5, + 'callback: the argument should have 5 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: the argument item property should be the moved item' + ); + assert.strictEqual( + data.fromGrid, + gridA, + 'callback: the argument fromGrid property should be the sending grid instance' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: the argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toGrid, + gridB, + 'callback: the argument toGrid property should be the receiving grid instance' + ); + assert.strictEqual( + data.toIndex, + 1, + 'callback: the argument toIndex property should be the index where the item was moved to' + ); + assert.strictEqual( + data.toGrid.getItems().indexOf(data.item), + data.toIndex, + 'callback: the item should be included in the target grid in correct position' + ); + assert.strictEqual( + data.fromGrid.getItems().indexOf(data.item), + -1, + 'callback: the item should not be included in the source grid' + ); + assert.strictEqual( + data.item.getElement().parentNode, + document.body, + 'callback: the item element should be appended to the send container' + ); + }); + gridA.on('receive', function () { + assert.ok(false, 'should not be triggered for the sending grid'); + }); + gridA.send(item, gridB, 1, { layout: false }); + teardown(); + } + ); - grids.push(gridA, gridB); + QUnit.test( + 'receive: should be triggered when an item is dragged into another grid (for the receiving grid)', + function (assert) { + assert.expect(11); - gridB.on('receive', function (data) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: the argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 5, 'callback: the argument should have 5 properties'); - assert.strictEqual(data.item, item, 'callback: the argument item property should be the dragged item'); - assert.strictEqual(data.fromGrid, gridA, 'callback: the argument fromGrid property should be the sending grid instance'); - assert.strictEqual(data.fromIndex, 0, 'callback: the argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toGrid, gridB, 'callback: the argument toGrid property should be the receiving grid instance'); - assert.strictEqual(data.toIndex, 0, 'callback: the argument toIndex property should be the index where the item was moved to'); - assert.strictEqual(data.toGrid.getItems().indexOf(data.item), data.toIndex, 'callback: the item should be included in the target grid in correct position'); - assert.strictEqual(data.fromGrid.getItems().indexOf(data.item), -1, 'callback: the item should not be included in the source grid'); - assert.strictEqual(data.item.isDragging(), true, 'callback: the item should be in dragging state'); - }); + var done = assert.async(); + var containerA = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '0px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var containerB = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '70px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var grids = []; + var gridA = new Muuri(containerA, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var gridB = new Muuri(containerB, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var item = gridA.getItems()[0]; + var teardown = function () { + gridA.destroy(); + gridB.destroy(); + containerA.parentNode.removeChild(containerA); + containerB.parentNode.removeChild(containerB); + done(); + }; - utils.dragElement(item.getElement(), 70, 0, teardown); + grids.push(gridA, gridB); - }); + gridB.on('receive', function (data) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: the argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 5, + 'callback: the argument should have 5 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: the argument item property should be the dragged item' + ); + assert.strictEqual( + data.fromGrid, + gridA, + 'callback: the argument fromGrid property should be the sending grid instance' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: the argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toGrid, + gridB, + 'callback: the argument toGrid property should be the receiving grid instance' + ); + assert.strictEqual( + data.toIndex, + 0, + 'callback: the argument toIndex property should be the index where the item was moved to' + ); + assert.strictEqual( + data.toGrid.getItems().indexOf(data.item), + data.toIndex, + 'callback: the item should be included in the target grid in correct position' + ); + assert.strictEqual( + data.fromGrid.getItems().indexOf(data.item), + -1, + 'callback: the item should not be included in the source grid' + ); + assert.strictEqual( + data.item.isDragging(), + true, + 'callback: the item should be in dragging state' + ); + }); -})(this); \ No newline at end of file + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 0, + onFinished: teardown, + }); + } + ); +})(this); diff --git a/tests/grid-events/remove.js b/tests/grid-events/remove.js index 3a9e0ad3..13304f93 100644 --- a/tests/grid-events/remove.js +++ b/tests/grid-events/remove.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('remove: should be triggered after grid.remove()', function (assert) { - assert.expect(3); var container = utils.createGridElements(); @@ -19,12 +17,18 @@ grid.on('remove', function (items, indices) { assert.strictEqual(arguments.length, 2, 'callback: should have two arguments'); - assert.deepEqual(utils.sortedIdList(items), utils.sortedIdList(removedItems), 'callback: first argument should be an array of the removed items'); - assert.deepEqual(indices, removedIndices, 'callback: second argument should be an array of the removed item indices'); + assert.deepEqual( + utils.sortedIdList(items), + utils.sortedIdList(removedItems), + 'callback: first argument should be an array of the removed items' + ); + assert.deepEqual( + indices, + removedIndices, + 'callback: second argument should be an array of the removed item indices' + ); }); grid.remove(removedItems); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/send.js b/tests/grid-events/send.js index 3488b25c..ba322668 100644 --- a/tests/grid-events/send.js +++ b/tests/grid-events/send.js @@ -1,131 +1,216 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('send: should be triggered in the end of the send procedure (for the sending grid)', function (assert) { - - assert.expect(11); - - var containerA = utils.createGridElements(); - var containerB = utils.createGridElements(); - var gridA = new Muuri(containerA); - var gridB = new Muuri(containerB); - var item = gridA.getItems()[0]; - var teardown = function () { - gridA.destroy(); - gridB.destroy(); - containerA.parentNode.removeChild(containerA); - containerB.parentNode.removeChild(containerB); - }; - - gridA.on('send', function (data) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: the argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 5, 'callback: the argument should have 5 properties'); - assert.strictEqual(data.item, item, 'callback: the argument item property should be the moved item'); - assert.strictEqual(data.fromGrid, gridA, 'callback: the argument fromGrid property should be the sending grid instance'); - assert.strictEqual(data.fromIndex, 0, 'callback: the argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toGrid, gridB, 'callback: the argument toGrid property should be the receiving grid instance'); - assert.strictEqual(data.toIndex, 1, 'callback: the argument toIndex property should be the index where the item was moved to'); - assert.strictEqual(data.toGrid.getItems().indexOf(data.item), data.toIndex, 'callback: the item should be included in the target grid in correct position'); - assert.strictEqual(data.fromGrid.getItems().indexOf(data.item), -1, 'callback: the item should not be included in the source grid'); - assert.strictEqual(data.item.getElement().parentNode, document.body, 'callback: the item element should be appended to the send container'); - }); - gridB.on('send', function () { - assert.ok(false, 'should not be triggered for the receiving grid'); - }); - gridA.send(item, gridB, 1, {layout: false}); - teardown(); - - }); - - QUnit.test('send: should be triggered when an item is dragged into another grid (for the sending grid)', function (assert) { + QUnit.test( + 'send: should be triggered in the end of the send procedure (for the sending grid)', + function (assert) { + assert.expect(11); - assert.expect(11); + var containerA = utils.createGridElements(); + var containerB = utils.createGridElements(); + var gridA = new Muuri(containerA); + var gridB = new Muuri(containerB); + var item = gridA.getItems()[0]; + var teardown = function () { + gridA.destroy(); + gridB.destroy(); + containerA.parentNode.removeChild(containerA); + containerB.parentNode.removeChild(containerB); + }; - var done = assert.async(); - var containerA = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '0px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var containerB = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '70px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var grids = []; - var gridA = new Muuri(containerA, { - dragEnabled: true, - dragSort: function () { - return grids - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var gridB = new Muuri(containerB, { - dragEnabled: true, - dragSort: function () { - return grids - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var item = gridA.getItems()[0]; - var teardown = function () { - gridA.destroy(); - gridB.destroy(); - containerA.parentNode.removeChild(containerA); - containerB.parentNode.removeChild(containerB); - done(); - }; + gridA.on('send', function (data) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: the argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 5, + 'callback: the argument should have 5 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: the argument item property should be the moved item' + ); + assert.strictEqual( + data.fromGrid, + gridA, + 'callback: the argument fromGrid property should be the sending grid instance' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: the argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toGrid, + gridB, + 'callback: the argument toGrid property should be the receiving grid instance' + ); + assert.strictEqual( + data.toIndex, + 1, + 'callback: the argument toIndex property should be the index where the item was moved to' + ); + assert.strictEqual( + data.toGrid.getItems().indexOf(data.item), + data.toIndex, + 'callback: the item should be included in the target grid in correct position' + ); + assert.strictEqual( + data.fromGrid.getItems().indexOf(data.item), + -1, + 'callback: the item should not be included in the source grid' + ); + assert.strictEqual( + data.item.getElement().parentNode, + document.body, + 'callback: the item element should be appended to the send container' + ); + }); + gridB.on('send', function () { + assert.ok(false, 'should not be triggered for the receiving grid'); + }); + gridA.send(item, gridB, 1, { layout: false }); + teardown(); + } + ); - grids.push(gridA, gridB); + QUnit.test( + 'send: should be triggered when an item is dragged into another grid (for the sending grid)', + function (assert) { + assert.expect(11); - gridA.on('send', function (data) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.strictEqual(Object.prototype.toString.call(data), '[object Object]', 'callback: the argument should be a plain object'); - assert.strictEqual(Object.keys(data).length, 5, 'callback: the argument should have 5 properties'); - assert.strictEqual(data.item, item, 'callback: the argument item property should be the dragged item'); - assert.strictEqual(data.fromGrid, gridA, 'callback: the argument fromGrid property should be the sending grid instance'); - assert.strictEqual(data.fromIndex, 0, 'callback: the argument fromIndex property should be the index where the item was moved from'); - assert.strictEqual(data.toGrid, gridB, 'callback: the argument toGrid property should be the receiving grid instance'); - assert.strictEqual(data.toIndex, 0, 'callback: the argument toIndex property should be the index where the item was moved to'); - assert.strictEqual(data.toGrid.getItems().indexOf(data.item), data.toIndex, 'callback: the item should be included in the target grid in correct position'); - assert.strictEqual(data.fromGrid.getItems().indexOf(data.item), -1, 'callback: the item should not be included in the source grid'); - assert.strictEqual(data.item.isDragging(), true, 'callback: the item should be in dragging state'); - }); + var done = assert.async(); + var containerA = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '0px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var containerB = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '70px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var grids = []; + var gridA = new Muuri(containerA, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var gridB = new Muuri(containerB, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var item = gridA.getItems()[0]; + var teardown = function () { + gridA.destroy(); + gridB.destroy(); + containerA.parentNode.removeChild(containerA); + containerB.parentNode.removeChild(containerB); + done(); + }; - utils.dragElement(item.getElement(), 70, 0, teardown); + grids.push(gridA, gridB); - }); + gridA.on('send', function (data) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.strictEqual( + Object.prototype.toString.call(data), + '[object Object]', + 'callback: the argument should be a plain object' + ); + assert.strictEqual( + Object.keys(data).length, + 5, + 'callback: the argument should have 5 properties' + ); + assert.strictEqual( + data.item, + item, + 'callback: the argument item property should be the dragged item' + ); + assert.strictEqual( + data.fromGrid, + gridA, + 'callback: the argument fromGrid property should be the sending grid instance' + ); + assert.strictEqual( + data.fromIndex, + 0, + 'callback: the argument fromIndex property should be the index where the item was moved from' + ); + assert.strictEqual( + data.toGrid, + gridB, + 'callback: the argument toGrid property should be the receiving grid instance' + ); + assert.strictEqual( + data.toIndex, + 0, + 'callback: the argument toIndex property should be the index where the item was moved to' + ); + assert.strictEqual( + data.toGrid.getItems().indexOf(data.item), + data.toIndex, + 'callback: the item should be included in the target grid in correct position' + ); + assert.strictEqual( + data.fromGrid.getItems().indexOf(data.item), + -1, + 'callback: the item should not be included in the source grid' + ); + assert.strictEqual( + data.item.isDragging(), + true, + 'callback: the item should be in dragging state' + ); + }); -})(this); \ No newline at end of file + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 0, + onFinished: teardown, + }); + } + ); +})(this); diff --git a/tests/grid-events/showEnd.js b/tests/grid-events/showEnd.js index 9a029ad0..b4e65bca 100644 --- a/tests/grid-events/showEnd.js +++ b/tests/grid-events/showEnd.js @@ -1,30 +1,33 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('showEnd: should be triggered after grid.show() (after the showing is finished)', function (assert) { - - assert.expect(2); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.on('showEnd', function (items) { - assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); - assert.deepEqual(utils.sortedIdList(items), utils.sortedIdList(grid.getItems([0, 1, 2])), 'callback: first argument should be an array of all the items that are were shown'); - teardown(); - }); - grid.hide([0, 1], {layout: false, instant: true}); - grid.show([0, 1, 2], {layout: false}); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'showEnd: should be triggered after grid.show() (after the showing is finished)', + function (assert) { + assert.expect(2); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.on('showEnd', function (items) { + assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); + assert.deepEqual( + utils.sortedIdList(items), + utils.sortedIdList(grid.getItems([0, 1, 2])), + 'callback: first argument should be an array of all the items that are were shown' + ); + teardown(); + }); + grid.hide(grid.getItems([0, 1]), { layout: false, instant: true, syncWithLayout: false }); + grid.show(grid.getItems([0, 1, 2]), { layout: false, syncWithLayout: false }); + } + ); +})(this); diff --git a/tests/grid-events/showStart.js b/tests/grid-events/showStart.js index bfb3b6e3..09584069 100644 --- a/tests/grid-events/showStart.js +++ b/tests/grid-events/showStart.js @@ -1,28 +1,31 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); - QUnit.test('showStart: should be triggered after grid.show() (just before the showing starts)', function (assert) { - - assert.expect(2); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - grid.on('showStart', function (items) { - assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); - assert.deepEqual(utils.sortedIdList(items), utils.sortedIdList(grid.getItems([0, 1, 2])), 'callback: first argument should be an array of all the items that are about to be shown'); - }); - grid.hide([0, 1], {layout: false, instant: true}); - grid.show([0, 1, 2], {layout: false}); - teardown(); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'showStart: should be triggered after grid.show() (just before the showing starts)', + function (assert) { + assert.expect(2); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + grid.on('showStart', function (items) { + assert.strictEqual(arguments.length, 1, 'callback: should have one argument'); + assert.deepEqual( + utils.sortedIdList(items), + utils.sortedIdList(grid.getItems([0, 1, 2])), + 'callback: first argument should be an array of all the items that are about to be shown' + ); + }); + grid.hide(grid.getItems([0, 1]), { layout: false, instant: true, syncWithLayout: false }); + grid.show(grid.getItems([0, 1, 2]), { layout: false, syncWithLayout: false }); + teardown(); + } + ); +})(this); diff --git a/tests/grid-events/sort.js b/tests/grid-events/sort.js index a57be5f4..22c9e8ad 100644 --- a/tests/grid-events/sort.js +++ b/tests/grid-events/sort.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('sort: should be triggered after grid.sort()', function (assert) { - assert.expect(3); var container = utils.createGridElements(); @@ -19,11 +17,18 @@ grid.on('sort', function (itemsNew, itemsPrev) { assert.strictEqual(arguments.length, 2, 'callback: should have two arguments'); - assert.deepEqual(utils.sortedIdList(itemsNew), utils.sortedIdList(newOrder), 'callback: first argument should be an array of all the items in their new order'); - assert.deepEqual(utils.sortedIdList(itemsPrev), utils.sortedIdList(currentOrder), 'callback: second argument should be an array of all the items in their previous order'); + assert.deepEqual( + utils.sortedIdList(itemsNew), + utils.sortedIdList(newOrder), + 'callback: first argument should be an array of all the items in their new order' + ); + assert.deepEqual( + utils.sortedIdList(itemsPrev), + utils.sortedIdList(currentOrder), + 'callback: second argument should be an array of all the items in their previous order' + ); }); grid.sort(newOrder); - + teardown(); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-events/synchronize.js b/tests/grid-events/synchronize.js index 32488941..00985cb4 100644 --- a/tests/grid-events/synchronize.js +++ b/tests/grid-events/synchronize.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid events'); QUnit.test('synchronize: should be triggered after grid.synchronize()', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -20,7 +18,5 @@ }); grid.synchronize(); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/add.js b/tests/grid-methods/add.js index b671091e..3cfcacc1 100644 --- a/tests/grid-methods/add.js +++ b/tests/grid-methods/add.js @@ -1,12 +1,10 @@ (function (window) { - var Muuri = window.Muuri; var idList = utils.idList; QUnit.module('Grid methods'); QUnit.test('add: should return the added items', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -19,31 +17,33 @@ assert.deepEqual(idList(grid.add(elem)), idList(grid.getItems(elem))); teardown(); - }); - QUnit.test('add: should accept an element or an array of elements as the first argument', function (assert) { - - assert.expect(2); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var elemA = document.createElement('div').appendChild(document.createElement('div')).parentNode; - var elemB = document.createElement('div').appendChild(document.createElement('div')).parentNode; - var elemC = document.createElement('div').appendChild(document.createElement('div')).parentNode; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - assert.deepEqual(idList(grid.add(elemA)), idList(grid.getItems(elemA))); - assert.deepEqual(idList(grid.add([elemB, elemC])), idList(grid.getItems([elemB, elemC]))); - teardown(); - - }); + QUnit.test( + 'add: should accept an element or an array of elements as the first argument', + function (assert) { + assert.expect(2); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var elemA = document.createElement('div').appendChild(document.createElement('div')) + .parentNode; + var elemB = document.createElement('div').appendChild(document.createElement('div')) + .parentNode; + var elemC = document.createElement('div').appendChild(document.createElement('div')) + .parentNode; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + assert.deepEqual(idList(grid.add(elemA)), idList(grid.getItems(elemA))); + assert.deepEqual(idList(grid.add([elemB, elemC])), idList(grid.getItems([elemB, elemC]))); + teardown(); + } + ); QUnit.test('add: should add the item to the last index by default', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -57,17 +57,17 @@ assert.strictEqual(grid.getItems().indexOf(item), 10); teardown(); - }); - QUnit.test('add: should allow defining the index where the items are inserted to', function (assert) { - + QUnit.test('add: should allow defining the index where the items are inserted to', function ( + assert + ) { assert.expect(1); var container = utils.createGridElements(); var grid = new Muuri(container); var elem = document.createElement('div').appendChild(document.createElement('div')).parentNode; - var item = grid.add(elem, {index: 1})[0]; + var item = grid.add(elem, { index: 1 })[0]; var teardown = function () { grid.destroy(); container.parentNode.removeChild(container); @@ -75,11 +75,9 @@ assert.strictEqual(grid.getItems().indexOf(item), 1); teardown(); - }); QUnit.test('add: should automatically layout the grid after add', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -95,74 +93,88 @@ teardown(); }); grid.add(elem); - - }); - - QUnit.test('add: should not trigger layout after add when layout option is set to false', function (assert) { - - assert.expect(0); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var elem = document.createElement('div').appendChild(document.createElement('div')).parentNode; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - grid.on('layoutStart', function () { - assert.strictEqual(true, false); - }); - grid.add(elem, {layout: false}); - teardown(); - }); - QUnit.test('add: should trigger unanimated layout after add when layout option is set to "instant"', function (assert) { - - assert.expect(1); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var elem = document.createElement('div').appendChild(document.createElement('div')).parentNode; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - grid.on('layoutEnd', function () { - assert.strictEqual(true, true); + QUnit.test( + 'add: should not trigger layout after add when layout option is set to false', + function (assert) { + assert.expect(0); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var elem = document.createElement('div').appendChild(document.createElement('div')) + .parentNode; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + grid.on('layoutStart', function () { + assert.strictEqual(true, false); + }); + grid.add(elem, { layout: false }); teardown(); - }); - grid.add(elem, {layout: 'instant'}); - - }); - - QUnit.test('add: should trigger layout and call callback function after add when a callback function is provided to the layout option', function (assert) { - - assert.expect(2); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var elem = document.createElement('div').appendChild(document.createElement('div')).parentNode; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - var args; - - grid.on('layoutEnd', function (items) { - assert.notStrictEqual(args, items, 'layout callback items argument should not the same object as the layoutEnd event callback`s argument'); - assert.deepEqual(idList(args), idList(items), 'layout callback should receive the same items as the layoutEnd event callback'); - teardown(); - }); - - grid.add(elem, {layout: function (isInterrupted, items) { - args = items; - }}); - - }); - -})(this); \ No newline at end of file + } + ); + + QUnit.test( + 'add: should trigger unanimated layout after add when layout option is set to "instant"', + function (assert) { + assert.expect(1); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var elem = document.createElement('div').appendChild(document.createElement('div')) + .parentNode; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + grid.on('layoutEnd', function () { + assert.strictEqual(true, true); + teardown(); + }); + grid.add(elem, { layout: 'instant' }); + } + ); + + QUnit.test( + 'add: should trigger layout and call callback function after add when a callback function is provided to the layout option', + function (assert) { + assert.expect(2); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var elem = document.createElement('div').appendChild(document.createElement('div')) + .parentNode; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + var args; + + grid.on('layoutEnd', function (items) { + assert.notStrictEqual( + args, + items, + 'layout callback items argument should not the same object as the layoutEnd event callback`s argument' + ); + assert.deepEqual( + idList(args), + idList(items), + 'layout callback should receive the same items as the layoutEnd event callback' + ); + teardown(); + }); + + grid.add(elem, { + layout: function (items) { + args = items; + }, + }); + } + ); +})(this); diff --git a/tests/grid-methods/destroy.js b/tests/grid-methods/destroy.js index 8fcf6b3c..3d37dec9 100644 --- a/tests/grid-methods/destroy.js +++ b/tests/grid-methods/destroy.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid methods'); QUnit.test('destroy: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -17,7 +15,5 @@ assert.strictEqual(grid.destroy(), grid); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/filter.js b/tests/grid-methods/filter.js index cb5a806b..4fdcae10 100644 --- a/tests/grid-methods/filter.js +++ b/tests/grid-methods/filter.js @@ -1,12 +1,10 @@ (function (window) { - var Muuri = window.Muuri; var idList = utils.idList; QUnit.module('Grid methods'); QUnit.test('filter: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -16,14 +14,17 @@ container.parentNode.removeChild(container); }; - assert.strictEqual(grid.filter(function () {}), grid); + assert.strictEqual( + grid.filter(function () { + return 0; + }), + grid + ); teardown(); - }); QUnit.test('filter: should accept a function as the first argument', function (assert) { - assert.expect(12); var container = utils.createGridElements(); @@ -37,20 +38,26 @@ }; grid.filter(function (item) { - assert.strictEqual(item._id, items[i]._id, 'predicate function should be called in ascending order for each item'); + assert.strictEqual( + item._id, + items[i]._id, + 'predicate function should be called in ascending order for each item' + ); ++i; return item === firstItem; }); assert.strictEqual(i, 10, 'predicate function should be called for each item'); - assert.deepEqual(idList(utils.getVisibleItems(grid)), idList([firstItem]), 'the items for which true were returned should be shown and others hidden'); + assert.deepEqual( + idList(utils.getVisibleItems(grid)), + idList([firstItem]), + 'the items for which true were returned should be shown and others hidden' + ); teardown(); - }); QUnit.test('filter: should accept a selector as the first argument', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -68,11 +75,9 @@ assert.deepEqual(idList(utils.getVisibleItems(grid)), idList([firstItem])); teardown(); - }); QUnit.test('filter: should not show/hide items instantly by default', function (assert) { - assert.expect(2); var container = utils.createGridElements(); @@ -83,9 +88,7 @@ container.parentNode.removeChild(container); }; - grid - .hide(0, {instant: true}) - .filter(function (item) { + grid.hide(grid.getItems(0), { instant: true }).filter(function (item) { return item === items[0]; }); @@ -93,11 +96,11 @@ assert.deepEqual(idList(utils.getHidingItems(grid)), idList(items.slice(1))); teardown(); - }); - QUnit.test('filter: should show/hide items instantly if instant option is true', function (assert) { - + QUnit.test('filter: should show/hide items instantly if instant option is true', function ( + assert + ) { assert.expect(4); var container = utils.createGridElements(); @@ -108,11 +111,12 @@ container.parentNode.removeChild(container); }; - grid - .hide(0, {instant: true}) - .filter(function (item) { - return item === items[0]; - }, {instant: true}); + grid.hide(grid.getItems(0), { instant: true }).filter( + function (item) { + return item === items[0]; + }, + { instant: true } + ); assert.strictEqual(utils.getShowingItems(grid).length, 0); assert.strictEqual(utils.getHidingItems(grid).length, 0); @@ -120,36 +124,54 @@ assert.deepEqual(idList(utils.getHiddenItems(grid)), idList(items.slice(1))); teardown(); - - }); - - QUnit.test('filter: should call the onFinish callback once the animations are finished', function (assert) { - - assert.expect(5); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var items = grid.getItems(); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid - .hide(0, {instant: true}) - .filter(function (item) { - return item === items[0]; - }, {onFinish: function (itemsToShow, itemsToHide) { - assert.strictEqual(arguments.length, 2, 'callback: should receive two arguments'); - assert.deepEqual(idList(itemsToShow), idList(items.slice(0, 1)), 'callback: should receive the shown items as it`s first argument'); - assert.deepEqual(idList(itemsToHide), idList(items.slice(1)), 'callback: should receive the hidden items as it`s second argument'); - assert.strictEqual(items[0].isVisible(), true, 'callback: the first argument items should be visible'); - assert.strictEqual(items[1].isVisible(), false, 'callback: the second argument items should be hidden'); - teardown(); - }}); - }); -})(this); \ No newline at end of file + QUnit.test( + 'filter: should call the onFinish callback once the animations are finished', + function (assert) { + assert.expect(5); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var items = grid.getItems(); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.hide(grid.getItems(0), { instant: true }).filter( + function (item) { + return item === items[0]; + }, + { + onFinish: function (itemsToShow, itemsToHide) { + assert.strictEqual(arguments.length, 2, 'callback: should receive two arguments'); + assert.deepEqual( + idList(itemsToShow), + idList(items.slice(0, 1)), + 'callback: should receive the shown items as it`s first argument' + ); + assert.deepEqual( + idList(itemsToHide), + idList(items.slice(1)), + 'callback: should receive the hidden items as it`s second argument' + ); + assert.strictEqual( + items[0].isVisible(), + true, + 'callback: the first argument items should be visible' + ); + assert.strictEqual( + items[1].isVisible(), + false, + 'callback: the second argument items should be hidden' + ); + teardown(); + }, + } + ); + } + ); +})(this); diff --git a/tests/grid-methods/getElement.js b/tests/grid-methods/getElement.js index 575a342e..57262274 100644 --- a/tests/grid-methods/getElement.js +++ b/tests/grid-methods/getElement.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid methods'); QUnit.test('getElement: should return the container element', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -17,7 +15,5 @@ assert.strictEqual(grid.getElement(), container); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/getItems.js b/tests/grid-methods/getItems.js index bd4cac93..70dd9083 100644 --- a/tests/grid-methods/getItems.js +++ b/tests/grid-methods/getItems.js @@ -1,12 +1,10 @@ (function (window) { - var Muuri = window.Muuri; var idList = utils.idList; QUnit.module('Grid methods'); QUnit.test('getItems: should return the instance`s items', function (assert) { - assert.expect(6); var done = assert.async(); @@ -19,14 +17,37 @@ done(); }; - assert.notStrictEqual(idList(grid.getItems()), idList(grid._items), 'should return a new array and not a reference to the internal array'); - assert.deepEqual(idList(grid.getItems()), idList(grid._items), 'should return all items in correct order if no arguments are provided'); - assert.deepEqual(idList(grid.getItems(0)), idList([items[0]]), 'should allow providing an index as the first argument'); - assert.deepEqual(idList(grid.getItems(items[0].getElement())), idList([items[0]]), 'should allow providing an element as the first argument'); - assert.deepEqual(idList(grid.getItems(items[0])), idList([items[0]]), 'should allow providing an item as the first argument'); - assert.deepEqual(idList(grid.getItems([0, items[1].getElement(), items[2]])), idList([items[0], items[1], items[2]]), 'should allow providing an array of indices, elements and items as the first argument'); + assert.notStrictEqual( + idList(grid.getItems()), + idList(grid._items), + 'should return a new array and not a reference to the internal array' + ); + assert.deepEqual( + idList(grid.getItems()), + idList(grid._items), + 'should return all items in correct order if no arguments are provided' + ); + assert.deepEqual( + idList(grid.getItems(0)), + idList([items[0]]), + 'should allow providing an index as the first argument' + ); + assert.deepEqual( + idList(grid.getItems(items[0].getElement())), + idList([items[0]]), + 'should allow providing an element as the first argument' + ); + assert.deepEqual( + idList(grid.getItems(items[0])), + idList([items[0]]), + 'should allow providing an item as the first argument' + ); + assert.deepEqual( + idList(grid.getItems([0, items[1].getElement(), items[2]])), + idList([items[0], items[1], items[2]]), + 'should allow providing an array of indices, elements and items as the first argument' + ); teardown(); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/hide.js b/tests/grid-methods/hide.js index c65edfeb..a2282ab1 100644 --- a/tests/grid-methods/hide.js +++ b/tests/grid-methods/hide.js @@ -1,12 +1,10 @@ (function (window) { - var Muuri = window.Muuri; var idList = utils.idList; QUnit.module('Grid methods'); QUnit.test('hide: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -16,14 +14,12 @@ container.parentNode.removeChild(container); }; - assert.strictEqual(grid.hide(0), grid); + assert.strictEqual(grid.hide(grid.getItems(0)), grid); teardown(); - }); - QUnit.test('hide: should accept an item, element or an index (or an array of them) as the first argument', function (assert) { - - assert.expect(5); + QUnit.test('hide: should accept an array of items as the first argument', function (assert) { + assert.expect(2); var container = utils.createGridElements(); var grid = new Muuri(container); @@ -33,26 +29,22 @@ container.parentNode.removeChild(container); }; - assert.strictEqual(utils.getHiddenItems(grid).length, 0, 'there should be no hidden items before the tests commence'); - - grid.hide(0); - assert.deepEqual(idList(utils.getHiddenItems(grid)), idList(items.slice(0, 1)), 'should accept an index as the first argument'); - - grid.hide(items[1]); - assert.deepEqual(idList(utils.getHiddenItems(grid)), idList(items.slice(0, 2)), 'should accept an item as the first argument'); - - grid.hide(items[2].getElement()); - assert.deepEqual(idList(utils.getHiddenItems(grid)), idList(items.slice(0, 3)), 'should accept an element as the first argument'); - - grid.hide([3, items[4].getElement(), items[5]]); - assert.deepEqual(idList(utils.getHiddenItems(grid)), idList(items.slice(0, 6)), 'should accept an array of items, elements and indices as the first argument'); + assert.strictEqual( + utils.getHiddenItems(grid).length, + 0, + 'there should be no hidden items before the tests start' + ); + grid.hide(items.slice(0, 3)); + assert.deepEqual( + idList(utils.getHiddenItems(grid)), + idList(items.slice(0, 3)), + 'should accept an array of items as the first argument' + ); teardown(); - }); QUnit.test('hide: should not hide instantly by default', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -63,15 +55,13 @@ container.parentNode.removeChild(container); }; - grid.hide(0); + grid.hide(items.slice(0, 1)); assert.deepEqual(idList(utils.getHidingItems(grid)), idList(items.slice(0, 1))); teardown(); - }); QUnit.test('hide: should hide instantly if instant option is true', function (assert) { - assert.expect(2); var container = utils.createGridElements(); @@ -82,16 +72,16 @@ container.parentNode.removeChild(container); }; - grid.hide(0, {instant: true}); + grid.hide(items.slice(0, 1), { instant: true }); assert.strictEqual(items[0].isHiding(), false); assert.strictEqual(items[0].isVisible(), false); teardown(); - }); - QUnit.test('hide: should call the onFinish callback once the animation is finished', function (assert) { - + QUnit.test('hide: should call the onFinish callback once the animation is finished', function ( + assert + ) { assert.expect(5); var done = assert.async(); @@ -106,18 +96,34 @@ }; grid - .on('hideEnd', function (completedItems) { - assert.deepEqual(idList(completedItems), idList(argItems), 'callback: the received items should match the items of show event callback'); - teardown(); - }) - .hide(0, {onFinish: function (completedItems) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.deepEqual(idList(completedItems), idList(items.slice(0, 1)), 'callback: should receive the hidden items as it`s first argument'); - assert.strictEqual(completedItems[0].isVisible(), false, 'callback: the received items should not be in "visible" state'); - assert.strictEqual(completedItems[0].isHiding(), false, 'callback: the received items should not be in "hiding" state'); - argItems = completedItems; - }}); - + .on('hideEnd', function (completedItems) { + assert.deepEqual( + idList(completedItems), + idList(argItems), + 'callback: the received items should match the items of show event callback' + ); + teardown(); + }) + .hide(items.slice(0, 1), { + onFinish: function (completedItems) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.deepEqual( + idList(completedItems), + idList(items.slice(0, 1)), + 'callback: should receive the hidden items as it`s first argument' + ); + assert.strictEqual( + completedItems[0].isVisible(), + false, + 'callback: the received items should not be in "visible" state' + ); + assert.strictEqual( + completedItems[0].isHiding(), + false, + 'callback: the received items should not be in "hiding" state' + ); + argItems = completedItems; + }, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/layout.js b/tests/grid-methods/layout.js index c1b00a1c..a6ecd582 100644 --- a/tests/grid-methods/layout.js +++ b/tests/grid-methods/layout.js @@ -1,12 +1,10 @@ (function (window) { - var Muuri = window.Muuri; var idList = utils.idList; QUnit.module('Grid methods'); QUnit.test('layout: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -18,11 +16,9 @@ assert.strictEqual(grid.layout(), grid); teardown(); - }); QUnit.test('layout: should not layout the items instantly by default', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -33,15 +29,15 @@ container.parentNode.removeChild(container); }; - grid.move(0, -1, {layout: false}); + grid.move(0, -1, { layout: false }); grid.layout(); assert.strictEqual(items[0].isPositioning(), true); teardown(); - }); - QUnit.test('layout: should layout the items instantly if the first argument is true', function (assert) { - + QUnit.test('layout: should layout the items instantly if the first argument is true', function ( + assert + ) { assert.expect(1); var container = utils.createGridElements(); @@ -52,37 +48,45 @@ container.parentNode.removeChild(container); }; - grid.move(0, -1, {layout: false}); + grid.move(0, -1, { layout: false }); grid.layout(true); assert.strictEqual(items[0].isPositioning(), false); teardown(); - - }); - - QUnit.test('layout: should call the provided callback function after layout is finished', function (assert) { - - assert.expect(4); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var items = grid.getItems(); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid - .move(0, -1, {layout: false}) - .layout(function (isInterrupted, items) { - assert.strictEqual(arguments.length, 2, 'callback: should have two arguments'); - assert.strictEqual(isInterrupted, false, 'callback: first argument should be a boolean that is true if the layout process was interrupted'); - assert.deepEqual(idList(items), idList(utils.getActiveItems(grid)), 'callback: second argument should be an array of the positioned items (all active items)'); - assert.strictEqual(items[0].isPositioning(), false, 'callback: items should not be in positioning state'); - teardown(); - }); - }); -})(this); \ No newline at end of file + QUnit.test( + 'layout: should call the provided callback function after layout is finished', + function (assert) { + assert.expect(4); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.move(0, -1, { layout: false }).layout(function (items, isInterrupted) { + assert.strictEqual(arguments.length, 2, 'callback: should have two arguments'); + assert.deepEqual( + idList(items), + idList(utils.getActiveItems(grid)), + 'callback: first argument should be an array of the positioned items (all active items)' + ); + assert.strictEqual( + isInterrupted, + false, + 'callback: second argument should be a boolean that is true if the layout process was interrupted' + ); + assert.strictEqual( + items[0].isPositioning(), + false, + 'callback: items should not be in positioning state' + ); + teardown(); + }); + } + ); +})(this); diff --git a/tests/grid-methods/move.js b/tests/grid-methods/move.js index cf7475d2..be8c5b95 100644 --- a/tests/grid-methods/move.js +++ b/tests/grid-methods/move.js @@ -1,12 +1,10 @@ (function (window) { - var Muuri = window.Muuri; var idList = utils.idList; QUnit.module('Grid methods'); QUnit.test('move: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -18,42 +16,41 @@ assert.strictEqual(grid.move(0, -1), grid); teardown(); - }); - QUnit.test('move: should accept elements, items and indices as the first and second arguments', function (assert) { - - assert.expect(3); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var items = grid.getItems(); - var move = function (array, fromIndex, toIndex) { - array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); - }; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - grid.move(0, 1); - move(items, 0, 1); - assert.deepEqual(idList(grid.getItems()), idList(items), 'should accept indices'); - - grid.move(items[0].getElement(), items[1].getElement()); - move(items, 0, 1); - assert.deepEqual(idList(grid.getItems()), idList(items), 'should accept elements'); - - grid.move(items[0], items[1]); - move(items, 0, 1); - assert.deepEqual(idList(grid.getItems()), idList(items), 'should accept items'); - - teardown(); - - }); + QUnit.test( + 'move: should accept elements, items and indices as the first and second arguments', + function (assert) { + assert.expect(3); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var items = grid.getItems(); + var move = function (array, fromIndex, toIndex) { + array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); + }; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + grid.move(0, 1); + move(items, 0, 1); + assert.deepEqual(idList(grid.getItems()), idList(items), 'should accept indices'); + + grid.move(items[0].getElement(), items[1].getElement()); + move(items, 0, 1); + assert.deepEqual(idList(grid.getItems()), idList(items), 'should accept elements'); + + grid.move(items[0], items[1]); + move(items, 0, 1); + assert.deepEqual(idList(grid.getItems()), idList(items), 'should accept items'); + + teardown(); + } + ); QUnit.test('move: should normalize negative indices to positive indices', function (assert) { - assert.expect(3); var container = utils.createGridElements(); @@ -73,17 +70,23 @@ grid.move(0, -2); move(items, 0, items.length - 2); - assert.deepEqual(idList(grid.getItems()), idList(items), 'should normalize -2 to second last index'); + assert.deepEqual( + idList(grid.getItems()), + idList(items), + 'should normalize -2 to second last index' + ); grid.move(0, -1000); - assert.deepEqual(idList(grid.getItems()), idList(items), 'should normalize too large negative index to 0'); + assert.deepEqual( + idList(grid.getItems()), + idList(items), + 'should normalize too large negative index to 0' + ); teardown(); - }); QUnit.test('move: should not swap items by default', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -102,11 +105,9 @@ assert.deepEqual(idList(grid.getItems()), idList(items)); teardown(); - }); QUnit.test('move: should swap items when action option is set to "swap"', function (assert) { - assert.expect(2); var container = utils.createGridElements(); @@ -117,12 +118,10 @@ container.parentNode.removeChild(container); }; - grid.move(0, 2, {action: 'swap'}); + grid.move(0, 2, { action: 'swap' }); assert.strictEqual(grid.getItems().indexOf(items[0]), 2); assert.strictEqual(grid.getItems().indexOf(items[2]), 0); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/off.js b/tests/grid-methods/off.js index fc51106e..5ed9ad9a 100644 --- a/tests/grid-methods/off.js +++ b/tests/grid-methods/off.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid methods'); QUnit.test('off: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -15,13 +13,14 @@ container.parentNode.removeChild(container); }; - assert.strictEqual(grid.off('foo', function () {}), grid); + assert.strictEqual( + grid.off('foo', function () {}), + grid + ); teardown(); - }); QUnit.test('off: should unbind an event listener', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -43,9 +42,11 @@ grid.on('synchronize', callback2); grid.off('synchronize', callback1); grid.synchronize(); - assert.strictEqual(calls, 1, 'should unbind all the listeners from the event that match the provided callback'); + assert.strictEqual( + calls, + 1, + 'should unbind all the listeners from the event that match the provided callback' + ); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/on.js b/tests/grid-methods/on.js index 84559ec5..6e0b2794 100644 --- a/tests/grid-methods/on.js +++ b/tests/grid-methods/on.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid methods'); QUnit.test('on: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -15,13 +13,14 @@ container.parentNode.removeChild(container); }; - assert.strictEqual(grid.on('foo', function () {}), grid); + assert.strictEqual( + grid.on('foo', function () {}), + grid + ); teardown(); - }); QUnit.test('on: should bind an event listener', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -39,7 +38,5 @@ grid.synchronize().synchronize().synchronize(); assert.strictEqual(calls, 3, 'should execute the listeners when event is emitted'); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/refreshItems.js b/tests/grid-methods/refreshItems.js index 1353c57c..771540cb 100644 --- a/tests/grid-methods/refreshItems.js +++ b/tests/grid-methods/refreshItems.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid methods'); QUnit.test('refreshItems: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -17,15 +15,15 @@ assert.strictEqual(grid.refreshItems(), grid); teardown(); - }); - QUnit.test('refreshItems: should update the cached dimensions of instance`s items', function (assert) { - + QUnit.test('refreshItems: should update the cached dimensions of instance`s items', function ( + assert + ) { assert.expect(7); var container = utils.createGridElements({ - itemCount: 7, + itemCount: 5, itemStyles: { position: 'absolute', width: '50px', @@ -34,8 +32,8 @@ border: '0px', margin: '10px', boxSizing: 'border-box', - background: '#000' - } + background: '#000', + }, }); var grid = new Muuri(container); var items = grid.getItems(); @@ -44,7 +42,7 @@ utils.setStyles(item.getElement(), { width: '10px', height: '20px', - margin: '30px' + margin: '30px', }); }); }; @@ -53,18 +51,22 @@ var result = { margin: item.getMargin(), width: item.getWidth(), - height: item.getHeight() + height: item.getHeight(), }; - assert.deepEqual(result, { - width: 10, - height: 20, - margin: { - left: 30, - right: 30, - top: 30, - bottom: 30 - } - }, msg); + assert.deepEqual( + result, + { + width: 10, + height: 20, + margin: { + left: 30, + right: 30, + top: 30, + bottom: 30, + }, + }, + msg + ); }); }; var teardown = function () { @@ -72,28 +74,14 @@ container.parentNode.removeChild(container); }; - updateItemDimensions(items[0]); - grid.refreshItems(items[0]); - assertItemChange(items[0], 'should accept an item as the first argument'); - - updateItemDimensions(items[1]); - grid.refreshItems(items[1].getElement()); - assertItemChange(items[1], 'should accept an element as the first argument'); + updateItemDimensions(items.slice(0, 2)); + grid.refreshItems(items.slice(0, 2)); + assertItemChange(items.slice(0, 2), 'should accept an array of items'); - updateItemDimensions(items[2]); - grid.refreshItems(2); - assertItemChange(items[2], 'should accept an index (number) as the first argument'); - - updateItemDimensions([items[3], items[4]]); - grid.refreshItems([3, items[4].getElement()]); - assertItemChange([items[3], items[4]], 'should accept an array of items, elements and indices as the first argument'); - - updateItemDimensions([items[5], items[6]]); + updateItemDimensions(items); grid.refreshItems(); - assertItemChange([items[5], items[6]], 'should refresh all items if no aguments are provided'); + assertItemChange(items, 'should refresh all items if no aguments are provided'); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/refreshSortData.js b/tests/grid-methods/refreshSortData.js index 7dfd175c..55d285af 100644 --- a/tests/grid-methods/refreshSortData.js +++ b/tests/grid-methods/refreshSortData.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid methods'); QUnit.test('refreshSortData: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -17,7 +15,5 @@ assert.strictEqual(grid.refreshSortData(), grid); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/remove.js b/tests/grid-methods/remove.js index fb2be468..2743019b 100644 --- a/tests/grid-methods/remove.js +++ b/tests/grid-methods/remove.js @@ -1,12 +1,10 @@ (function (window) { - var Muuri = window.Muuri; var idList = utils.idList; QUnit.module('Grid methods'); QUnit.test('remove: should return the removed items', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -19,12 +17,10 @@ assert.deepEqual(grid.remove(removedItems), removedItems); teardown(); - }); - QUnit.test('remove: should accept an item, element or an index (or an array of them) as the first argument', function (assert) { - - assert.expect(4); + QUnit.test('remove: should accept an array of items as the first argument', function (assert) { + assert.expect(1); var container = utils.createGridElements(); var grid = new Muuri(container); @@ -33,24 +29,17 @@ container.parentNode.removeChild(container); }; - grid.remove(grid.getItems()[0]); - assert.strictEqual(grid.getItems().length, 9, 'should accept an item as the first argument'); - - grid.remove(grid.getItems()[0].getElement()); - assert.strictEqual(grid.getItems().length, 8, 'should accept an element as the first argument'); - - grid.remove(0); - assert.strictEqual(grid.getItems().length, 7, 'should accept an index as the first argument'); - - grid.remove([0, 1, grid.getItems()[2], grid.getItems()[3].getElement()]); - assert.strictEqual(grid.getItems().length, 3, 'should accept an array of items, elements and indices as the first argument'); + grid.remove(grid.getItems([0, 1])); + assert.strictEqual( + grid.getItems().length, + 8, + 'should accept an array of items as the first argument' + ); teardown(); - }); QUnit.test('remove: should not remove the item elements by default', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -60,31 +49,30 @@ container.parentNode.removeChild(container); }; - grid.remove([0, 1]); + grid.remove(grid.getItems([0, 1])); assert.strictEqual(container.children.length, 10); teardown(); - }); - QUnit.test('remove: should remove the item elements when removeElements option is set to true', function (assert) { + QUnit.test( + 'remove: should remove the item elements when removeElements option is set to true', + function (assert) { + assert.expect(1); - assert.expect(1); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; - var container = utils.createGridElements(); - var grid = new Muuri(container); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - grid.remove([0, 1], {removeElements: true}); - assert.strictEqual(container.children.length, 8); - teardown(); - - }); + grid.remove(grid.getItems([0, 1]), { removeElements: true }); + assert.strictEqual(container.children.length, 8); + teardown(); + } + ); QUnit.test('remove: should automatically layout the grid after remove', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -98,72 +86,83 @@ assert.strictEqual(true, true); teardown(); }); - grid.remove(0); - - }); - - QUnit.test('remove: should not trigger layout after remove when layout option is set to false', function (assert) { - - assert.expect(0); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - grid.on('layoutStart', function () { - assert.strictEqual(true, false); - }); - grid.remove(0, {layout: false}); - teardown(); - - }); - - QUnit.test('remove: should trigger unanimated layout after add when layout option is set to "instant"', function (assert) { - - assert.expect(1); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - grid.on('layoutEnd', function () { - assert.strictEqual(true, true); - teardown(); - }); - grid.remove(0, {layout: 'instant'}); - + grid.remove(grid.getItems(0)); }); - QUnit.test('remove: should trigger layout and call callback function after add when a callback function is provided to the layout option', function (assert) { - - assert.expect(2); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - var args; - - grid.on('layoutEnd', function (items) { - assert.notStrictEqual(args, items, 'layout callback items argument should not the same object as the layoutEnd event callback`s argument'); - assert.deepEqual(idList(args), idList(items), 'layout callback should receive the same items as the layoutEnd event callback'); + QUnit.test( + 'remove: should not trigger layout after remove when layout option is set to false', + function (assert) { + assert.expect(0); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + grid.on('layoutStart', function () { + assert.strictEqual(true, false); + }); + grid.remove(grid.getItems(0), { layout: false }); teardown(); - }); - - grid.remove(0, {layout: function (isInterrupted, items) { - args = items; - }}); - - }); - -})(this); \ No newline at end of file + } + ); + + QUnit.test( + 'remove: should trigger unanimated layout after add when layout option is set to "instant"', + function (assert) { + assert.expect(1); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + grid.on('layoutEnd', function () { + assert.strictEqual(true, true); + teardown(); + }); + grid.remove(grid.getItems(0), { layout: 'instant' }); + } + ); + + QUnit.test( + 'remove: should trigger layout and call callback function after add when a callback function is provided to the layout option', + function (assert) { + assert.expect(2); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + var args; + + grid.on('layoutEnd', function (items) { + assert.notStrictEqual( + args, + items, + 'layout callback items argument should not the same object as the layoutEnd event callback`s argument' + ); + assert.deepEqual( + idList(args), + idList(items), + 'layout callback should receive the same items as the layoutEnd event callback' + ); + teardown(); + }); + + grid.remove(grid.getItems(0), { + layout: function (items) { + args = items; + }, + }); + } + ); +})(this); diff --git a/tests/grid-methods/send.js b/tests/grid-methods/send.js index 6b9f8718..b6f2bf40 100644 --- a/tests/grid-methods/send.js +++ b/tests/grid-methods/send.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid methods'); QUnit.test('send: should return the instance', function (assert) { - assert.expect(1); var containerA = utils.createGridElements(); @@ -21,11 +19,11 @@ assert.strictEqual(gridA.send(0, gridB, 0), gridA); teardown(); - }); - QUnit.test('send: should move an item from a grid to another to the specified index', function (assert) { - + QUnit.test('send: should move an item from a grid to another to the specified index', function ( + assert + ) { assert.expect(3); var containerA = utils.createGridElements(); @@ -50,11 +48,9 @@ assert.strictEqual(gridB.getItems(2)[0], item); teardown(); - }); QUnit.test('send: appendTo option', function (assert) { - assert.expect(2); var containerA = utils.createGridElements(); @@ -70,17 +66,23 @@ }; gridA.send(item, gridB, 0); - assert.strictEqual(item.getElement().parentNode, document.body, 'appendTo should be by default document.body'); - - gridB.send(item, gridA, 0, {appendTo: containerA}); - assert.strictEqual(item.getElement().parentNode, containerA, 'item element is appended to the element that is provided for the appendTo option'); + assert.strictEqual( + item.getElement().parentNode, + document.body, + 'appendTo should be by default document.body' + ); + + gridB.send(item, gridA, 0, { appendTo: containerA }); + assert.strictEqual( + item.getElement().parentNode, + containerA, + 'item element is appended to the element that is provided for the appendTo option' + ); teardown(); - }); QUnit.test('send: layoutSender/layoutReceiver options', function (assert) { - assert.expect(8); var containerA = utils.createGridElements(); @@ -99,40 +101,70 @@ gridALayoutId = gridA._layout.id; gridBLayoutId = gridB._layout.id; gridA.send(0, gridB, 0); - assert.notStrictEqual(gridA._layout.id, gridALayoutId, 'The sender grid should be laid out by default.'); - assert.notStrictEqual(gridB._layout.id, gridBLayoutId, 'The receiver grid should be laid out by default.'); + assert.notStrictEqual( + gridA._layout.id, + gridALayoutId, + 'The sender grid should be laid out by default.' + ); + assert.notStrictEqual( + gridB._layout.id, + gridBLayoutId, + 'The receiver grid should be laid out by default.' + ); gridALayoutId = gridA._layout.id; gridBLayoutId = gridB._layout.id; gridA.send(0, gridB, 0, { layoutSender: false, - layoutReceiver: false + layoutReceiver: false, }); - assert.strictEqual(gridA._layout.id, gridALayoutId, 'The sender grid should not be laid out when layoutSender is `false`.'); - assert.strictEqual(gridB._layout.id, gridBLayoutId, 'The receiver grid should not be laid out when layoutReceiver is `false`.'); + assert.strictEqual( + gridA._layout.id, + gridALayoutId, + 'The sender grid should not be laid out when layoutSender is `false`.' + ); + assert.strictEqual( + gridB._layout.id, + gridBLayoutId, + 'The receiver grid should not be laid out when layoutReceiver is `false`.' + ); gridALayoutId = gridA._layout.id; gridBLayoutId = gridB._layout.id; gridA.send(0, gridB, 0, { layoutSender: 'instant', - layoutReceiver: 'instant' + layoutReceiver: 'instant', }); - assert.notStrictEqual(gridA._layout.id, gridALayoutId, 'The sender grid should be laid out when layoutSender is `"instant"`.'); - assert.notStrictEqual(gridB._layout.id, gridBLayoutId, 'The receiver grid should be laid out when layoutReceiver is `"instant"`.'); + assert.notStrictEqual( + gridA._layout.id, + gridALayoutId, + 'The sender grid should be laid out when layoutSender is `"instant"`.' + ); + assert.notStrictEqual( + gridB._layout.id, + gridBLayoutId, + 'The receiver grid should be laid out when layoutReceiver is `"instant"`.' + ); gridALayoutId = gridA._layout.id; gridBLayoutId = gridB._layout.id; gridA.send(0, gridB, 0, { layoutSender: function () {}, - layoutReceiver: function () {} + layoutReceiver: function () {}, }); - assert.notStrictEqual(gridA._layout.id, gridALayoutId, 'The sender grid should be laid out when layoutSender is a function.'); - assert.notStrictEqual(gridB._layout.id, gridBLayoutId, 'The receiver grid should be laid out when layoutReceiver is a function.'); + assert.notStrictEqual( + gridA._layout.id, + gridALayoutId, + 'The sender grid should be laid out when layoutSender is a function.' + ); + assert.notStrictEqual( + gridB._layout.id, + gridBLayoutId, + 'The receiver grid should be laid out when layoutReceiver is a function.' + ); // TODO: Test that animations work as supposed to also with all the different options. teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/show.js b/tests/grid-methods/show.js index 923de120..05437c95 100644 --- a/tests/grid-methods/show.js +++ b/tests/grid-methods/show.js @@ -1,12 +1,10 @@ (function (window) { - var Muuri = window.Muuri; var idList = utils.idList; QUnit.module('Grid methods'); QUnit.test('show: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -16,14 +14,12 @@ container.parentNode.removeChild(container); }; - assert.strictEqual(grid.show(0), grid); + assert.strictEqual(grid.show(grid.getItems(0)), grid); teardown(); - }); - QUnit.test('show: should accept an item, element or an index (or an array of them) as the first argument', function (assert) { - - assert.expect(5); + QUnit.test('show: should accept an array of items as the first argument', function (assert) { + assert.expect(2); var container = utils.createGridElements(); var grid = new Muuri(container); @@ -33,27 +29,24 @@ container.parentNode.removeChild(container); }; - grid.hide(items, {instant: true}); - assert.strictEqual(utils.getVisibleItems(grid).length, 0, 'there should be no visible items before the tests commence'); - - grid.show(0); - assert.deepEqual(idList(utils.getVisibleItems(grid)), idList(items.slice(0, 1)), 'should accept an index as the first argument'); - - grid.show(items[1]); - assert.deepEqual(idList(utils.getVisibleItems(grid)), idList(items.slice(0, 2)), 'should accept an item as the first argument'); - - grid.show(items[2].getElement()); - assert.deepEqual(idList(utils.getVisibleItems(grid)), idList(items.slice(0, 3)), 'should accept an element as the first argument'); + grid.hide(items, { instant: true }); + assert.strictEqual( + utils.getVisibleItems(grid).length, + 0, + 'there should be no visible items before the tests start' + ); - grid.show([3, items[4].getElement(), items[5]]); - assert.deepEqual(idList(utils.getVisibleItems(grid)), idList(items.slice(0, 6)), 'should accept an array of items, elements and indices as the first argument'); + grid.show(items.slice(0, 3)); + assert.deepEqual( + idList(utils.getVisibleItems(grid)), + idList(items.slice(0, 3)), + 'should accept an array of items as the first argument' + ); teardown(); - }); QUnit.test('show: should not show instantly by default', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -64,15 +57,13 @@ container.parentNode.removeChild(container); }; - grid.hide(items, {instant: true}).show(0); + grid.hide(items, { instant: true }).show(items.slice(0, 1)); assert.deepEqual(idList(utils.getShowingItems(grid)), idList(items.slice(0, 1))); teardown(); - }); QUnit.test('show: should show instantly if instant option is true', function (assert) { - assert.expect(2); var container = utils.createGridElements(); @@ -83,16 +74,16 @@ container.parentNode.removeChild(container); }; - grid.hide(items, {instant: true}).show(0, {instant: true}); + grid.hide(items, { instant: true }).show(items.slice(0, 1), { instant: true }); assert.deepEqual(items[0].isShowing(), false); assert.deepEqual(items[0].isVisible(), true); teardown(); - }); - QUnit.test('show: should call the onFinish callback once the animation is finished', function (assert) { - + QUnit.test('show: should call the onFinish callback once the animation is finished', function ( + assert + ) { assert.expect(5); var done = assert.async(); @@ -107,19 +98,35 @@ }; grid - .on('showEnd', function (completedItems) { - assert.deepEqual(idList(completedItems), idList(argItems), 'callback: the received items should match the items of show event callback'); - teardown(); - }) - .hide(items, {instant: true}) - .show(0, {onFinish: function (completedItems) { - assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); - assert.deepEqual(idList(completedItems), idList(items.slice(0, 1)), 'callback: should receive the shown items as it`s first argument'); - assert.strictEqual(completedItems[0].isVisible(), true, 'callback: the received items should be in "visible" state'); - assert.strictEqual(completedItems[0].isShowing(), false, 'callback: the received items should not be in "showing" state'); - argItems = completedItems; - }}); - + .on('showEnd', function (completedItems) { + assert.deepEqual( + idList(completedItems), + idList(argItems), + 'callback: the received items should match the items of show event callback' + ); + teardown(); + }) + .hide(items, { instant: true }) + .show(grid.getItems(0), { + onFinish: function (completedItems) { + assert.strictEqual(arguments.length, 1, 'callback: should receive one argument'); + assert.deepEqual( + idList(completedItems), + idList(items.slice(0, 1)), + 'callback: should receive the shown items as it`s first argument' + ); + assert.strictEqual( + completedItems[0].isVisible(), + true, + 'callback: the received items should be in "visible" state' + ); + assert.strictEqual( + completedItems[0].isShowing(), + false, + 'callback: the received items should not be in "showing" state' + ); + argItems = completedItems; + }, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/sort.js b/tests/grid-methods/sort.js index fbe2cc6a..608fc78f 100644 --- a/tests/grid-methods/sort.js +++ b/tests/grid-methods/sort.js @@ -1,12 +1,10 @@ (function (window) { - var Muuri = window.Muuri; var idList = utils.idList; QUnit.module('Grid methods'); QUnit.test('sort: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -16,13 +14,16 @@ container.parentNode.removeChild(container); }; - assert.strictEqual(grid.sort(function () {}), grid); + assert.strictEqual( + grid.sort(function () { + return 0; + }), + grid + ); teardown(); - }); QUnit.test('sort: should accept a function as the first argument', function (assert) { - assert.expect(2); var container = utils.createGridElements(); @@ -48,18 +49,26 @@ // Test the default ascending order. grid.sort(sortByFoo); - assert.deepEqual(idList(grid.getItems()), idList(newItems), 'the items should be in ascending order by default'); + assert.deepEqual( + idList(grid.getItems()), + idList(newItems), + 'the items should be in ascending order by default' + ); // Test descending flag. - grid.sort(sortByFoo, {descending: true}); - assert.deepEqual(idList(grid.getItems()), idList(newItems.reverse()), 'the items should be in descending order when descending option is true'); + grid.sort(sortByFoo, { descending: true }); + assert.deepEqual( + idList(grid.getItems()), + idList(newItems.reverse()), + 'the items should be in descending order when descending option is true' + ); teardown(); - }); - QUnit.test('sort: should accept a single sort property (string) as the first argument', function (assert) { - + QUnit.test('sort: should accept a single sort property (string) as the first argument', function ( + assert + ) { assert.expect(3); var container = utils.createGridElements(); @@ -67,8 +76,8 @@ sortData: { foo: function (item, element) { return parseFloat(element.getAttribute('data-foo')); - } - } + }, + }, }); var items = grid.getItems(); var newIndices = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8]; @@ -87,87 +96,98 @@ // Test the default ascending order. grid.sort('foo'); - assert.deepEqual(idList(grid.getItems()), idList(newItems.concat()), 'the items should be in ascending order by default'); + assert.deepEqual( + idList(grid.getItems()), + idList(newItems.concat()), + 'the items should be in ascending order by default' + ); // Test property's descending flag. grid.sort('foo:desc'); - assert.deepEqual(idList(grid.getItems()), idList(newItems.concat().reverse()), 'the items should be in descending order when "desc" flag is added to the property'); + assert.deepEqual( + idList(grid.getItems()), + idList(newItems.concat().reverse()), + 'the items should be in descending order when "desc" flag is added to the property' + ); // Test property's descending flag. - grid.sort('foo', {descending: true}); - assert.deepEqual(idList(grid.getItems()), idList(newItems.concat().reverse()), 'the items should be in descending order when descending option is true'); + grid.sort('foo', { descending: true }); + assert.deepEqual( + idList(grid.getItems()), + idList(newItems.concat().reverse()), + 'the items should be in descending order when descending option is true' + ); teardown(); - }); - QUnit.test('sort: should accept multiple sort properties (string) as the first argument', function (assert) { - - assert.expect(5); - - var container = utils.createGridElements(); - var grid = new Muuri(container, { - sortData: { - foo: function (item, element) { - return parseFloat(element.getAttribute('data-foo')); + QUnit.test( + 'sort: should accept multiple sort properties (string) as the first argument', + function (assert) { + assert.expect(5); + + var container = utils.createGridElements(); + var grid = new Muuri(container, { + sortData: { + foo: function (item, element) { + return parseFloat(element.getAttribute('data-foo')); + }, + bar: function (item, element) { + return parseFloat(element.getAttribute('data-bar')); + }, }, - bar: function (item, element) { - return parseFloat(element.getAttribute('data-bar')); - } - } - }); - var items = grid.getItems(); - var fooData = [2, 1, 4, 3, 6, 5, 8, 7, 10, 9]; - var barData = [2, 2, 2, 2, 2, 1, 1, 1, 1, 1]; - var orderFooBar = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8]; - var orderFooBarDesc = [8, 9, 6, 7, 4, 5, 2, 3, 0, 1]; - var orderBarFoo = [6, 5, 8, 7, 9, 0, 2, 1, 4, 3]; - var orderBarFooDesc = [3, 4, 1, 2, 0, 9, 7, 8, 5, 6]; - var orderBarFooSpecial = [1, 0, 3, 2, 4, 5, 7, 6, 9, 8]; - var itemsFooBar = []; - var itemsFooBarDesc = []; - var itemsBarFoo = []; - var itemsBarFooDesc = []; - var itemsBarFooSpecial = []; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - // Add foo and bar data to elements and refresh sort data. - items.forEach(function (item, i) { - item.getElement().setAttribute('data-foo', fooData[i]); - item.getElement().setAttribute('data-bar', barData[i]); - itemsFooBar[orderFooBar[i]] = item; - itemsFooBarDesc[orderFooBarDesc[i]] = item; - itemsBarFoo[orderBarFoo[i]] = item; - itemsBarFooDesc[orderBarFooDesc[i]] = item; - itemsBarFooSpecial[orderBarFooSpecial[i]] = item; - }); - grid.refreshSortData(); - - grid.sort('foo bar'); - assert.deepEqual(idList(grid.getItems()), idList(itemsFooBar), 'foo bar'); - - grid.sort('bar foo'); - assert.deepEqual(idList(grid.getItems()), idList(itemsBarFoo), 'bar foo'); - - grid.sort('foo bar', {descending: true}); - assert.deepEqual(idList(grid.getItems()), idList(itemsFooBarDesc), 'foo bar (descending)'); - - grid.sort('bar foo', {descending: true}); - assert.deepEqual(idList(grid.getItems()), idList(itemsBarFooDesc), 'bar foo (descending)'); - - grid.sort('bar:desc foo'); - assert.deepEqual(idList(grid.getItems()), idList(itemsBarFooSpecial), 'bar:desc foo'); - - teardown(); - - }); + }); + var items = grid.getItems(); + var fooData = [2, 1, 4, 3, 6, 5, 8, 7, 10, 9]; + var barData = [2, 2, 2, 2, 2, 1, 1, 1, 1, 1]; + var orderFooBar = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8]; + var orderFooBarDesc = [8, 9, 6, 7, 4, 5, 2, 3, 0, 1]; + var orderBarFoo = [6, 5, 8, 7, 9, 0, 2, 1, 4, 3]; + var orderBarFooDesc = [3, 4, 1, 2, 0, 9, 7, 8, 5, 6]; + var orderBarFooSpecial = [1, 0, 3, 2, 4, 5, 7, 6, 9, 8]; + var itemsFooBar = []; + var itemsFooBarDesc = []; + var itemsBarFoo = []; + var itemsBarFooDesc = []; + var itemsBarFooSpecial = []; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + // Add foo and bar data to elements and refresh sort data. + items.forEach(function (item, i) { + item.getElement().setAttribute('data-foo', fooData[i]); + item.getElement().setAttribute('data-bar', barData[i]); + itemsFooBar[orderFooBar[i]] = item; + itemsFooBarDesc[orderFooBarDesc[i]] = item; + itemsBarFoo[orderBarFoo[i]] = item; + itemsBarFooDesc[orderBarFooDesc[i]] = item; + itemsBarFooSpecial[orderBarFooSpecial[i]] = item; + }); + grid.refreshSortData(); + + grid.sort('foo bar'); + assert.deepEqual(idList(grid.getItems()), idList(itemsFooBar), 'foo bar'); + + grid.sort('bar foo'); + assert.deepEqual(idList(grid.getItems()), idList(itemsBarFoo), 'bar foo'); + + grid.sort('foo bar', { descending: true }); + assert.deepEqual(idList(grid.getItems()), idList(itemsFooBarDesc), 'foo bar (descending)'); + + grid.sort('bar foo', { descending: true }); + assert.deepEqual(idList(grid.getItems()), idList(itemsBarFooDesc), 'bar foo (descending)'); + + grid.sort('bar:desc foo'); + assert.deepEqual(idList(grid.getItems()), idList(itemsBarFooSpecial), 'bar:desc foo'); + + teardown(); + } + ); QUnit.test('sort: should accept an array of items as the first argument', function (assert) { - - assert.expect(4); + assert.expect(1); var container = utils.createGridElements(); var grid = new Muuri(container); @@ -181,26 +201,6 @@ grid.sort(newItems); assert.deepEqual(idList(grid.getItems()), idList(newItems)); - newItems.push(newItems[0]); - assert.throws( - function () { grid.sort(newItems); }, - 'Should throw error if the number of provided items is more than the amount of current items.' - ); - - newItems.length = newItems.length - 2; - assert.throws( - function () { grid.sort(newItems); }, - 'Should throw error if the number of provided items is less than the amount of current items.' - ); - - newItems.push(1); - assert.throws( - function () { grid.sort(newItems); }, - 'Should throw if there one or more of the provided items does not exists within the current items.' - ); - teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-methods/synchronize.js b/tests/grid-methods/synchronize.js index d08ff4d1..fb8a06ea 100644 --- a/tests/grid-methods/synchronize.js +++ b/tests/grid-methods/synchronize.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid methods'); QUnit.test('synchronize: should return the instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -17,11 +15,11 @@ assert.strictEqual(grid.synchronize(), grid); teardown(); - }); - QUnit.test('synchronize: should order the dom elements to match the order of items', function (assert) { - + QUnit.test('synchronize: should order the dom elements to match the order of items', function ( + assert + ) { assert.expect(2); var container = utils.createGridElements(); @@ -32,15 +30,21 @@ container.parentNode.removeChild(container); }; - grid.move(0, -1, {layout: false}); + grid.move(0, -1, { layout: false }); elements = grid.getItems().map(function (item) { return item.getElement(); }); - assert.notDeepEqual([].slice.call(container.children), elements, 'elements should be out of sync after an item is moved'); + assert.notDeepEqual( + [].slice.call(container.children), + elements, + 'elements should be out of sync after an item is moved' + ); grid.synchronize(); - assert.deepEqual([].slice.call(container.children), elements, 'elements should be in sync after grid.synchronize() is called'); + assert.deepEqual( + [].slice.call(container.children), + elements, + 'elements should be in sync after grid.synchronize() is called' + ); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/containerClass.js b/tests/grid-options/containerClass.js index 061481cb..5db29d8b 100644 --- a/tests/grid-options/containerClass.js +++ b/tests/grid-options/containerClass.js @@ -1,16 +1,16 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); - QUnit.test('containerClass: should define the classname for the container element', function (assert) { - + QUnit.test('containerClass: should define the classname for the container element', function ( + assert + ) { assert.expect(1); var container = utils.createGridElements(); var grid = new Muuri(container, { - containerClass: 'foo' + containerClass: 'foo', }); var teardown = function () { grid.destroy(); @@ -19,7 +19,5 @@ assert.strictEqual(utils.matches(container, '.foo'), true); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/dragAutoScroll.js b/tests/grid-options/dragAutoScroll.js new file mode 100644 index 00000000..c6644196 --- /dev/null +++ b/tests/grid-options/dragAutoScroll.js @@ -0,0 +1,141 @@ +(function (window) { + var Muuri = window.Muuri; + + QUnit.module('Grid options'); + + QUnit.test('dragAutoScroll: should scroll window vertically and horizontally', function (assert) { + assert.expect(4 + 4 * 6); + + var done = assert.async(); + var container = utils.createGridElements({ + itemCount: 2, + containerStyles: { + position: 'absolute', + left: '0px', + top: '0px', + width: '140px', + }, + }); + + // Create fixed drag container. + var dragContainer = document.createElement('div'); + dragContainer.style.position = 'fixed'; + document.body.appendChild(dragContainer); + + // Make document body large so that window can scroll. + document.body.style.position = 'relative'; + document.body.style.height = '10000px'; + document.body.style.width = '10000px'; + + // Init grid. + var grid = new Muuri(container, { + dragEnabled: true, + dragSort: false, + dragContainer: dragContainer, + dragAutoScroll: { + targets: [window], + onStart: function (item, element, direction) { + assert.ok(grid.getItems().indexOf(item) > -1, 'onStart item argument is grid item'); + assert.strictEqual(element, window, 'onStart element argument is window'); + assert.ok( + [ + Muuri.AutoScroller.LEFT, + Muuri.AutoScroller.RIGHT, + Muuri.AutoScroller.UP, + Muuri.AutoScroller.DOWN, + ].indexOf(direction) > -1, + 'onStart direction argument is valid direction' + ); + }, + onStop: function (item, element, direction) { + assert.ok(grid.getItems().indexOf(item) > -1, 'onStop item argument is grid item'); + assert.strictEqual(element, window, 'onStop element argument is window'); + assert.ok( + [ + Muuri.AutoScroller.LEFT, + Muuri.AutoScroller.RIGHT, + Muuri.AutoScroller.UP, + Muuri.AutoScroller.DOWN, + ].indexOf(direction) > -1, + 'onStop direction argument is valid direction' + ); + }, + }, + }); + + // Make sure window is not scrolled on init. + var scrollX = 0; + var scrollY = 0; + window.scrollTo(scrollX, scrollY); + assert.ok( + window.pageXOffset === scrollX && window.pageYOffset === scrollY, + 'window should not be scrolled on init' + ); + + // Compute how much we need to drag the item and make sure that it is + // possible to trigger auto-scroll. + var item = grid.getItems()[0]; + var itemRect = item.getElement().getBoundingClientRect(); + var leftOffset = window.innerWidth - itemRect.right; + var topOffset = window.innerHeight - itemRect.bottom; + assert.ok(leftOffset > 0 && topOffset > 0, 'item can scroll the window'); + + // Define teardown procedure. + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + dragContainer.parentNode.removeChild(dragContainer); + document.body.style.height = ''; + document.body.style.width = ''; + document.body.style.position = ''; + window.scrollTo(0, 0); + done(); + }; + + // Drag down right. + utils.dragElement({ + element: item.getElement(), + x: leftOffset, + y: topOffset, + holdDuration: 300, + onFinished: function () { + assert.ok( + window.pageXOffset > scrollX && window.pageYOffset > scrollY, + 'window should be scrolled down and right' + ); + + // Place container to the bottom-right corner of the body and scroll + // window to the max. + container.style.left = 'auto'; + container.style.top = 'auto'; + container.style.right = '0px'; + container.style.bottom = '0px'; + window.scrollTo(100000, 100000); + scrollX = window.pageXOffset; + scrollY = window.pageYOffset; + + // Compute how much we need to drag the item to left and top so that + // auto-scroll will be triggered. + item = grid.getItems()[1]; + itemRect = item.getElement().getBoundingClientRect(); + leftOffset = -itemRect.left; + topOffset = -itemRect.top; + + // Drag up left. + utils.dragElement({ + element: item.getElement(), + x: leftOffset, + y: topOffset, + holdDuration: 300, + onFinished: function () { + assert.ok( + window.pageXOffset < scrollX && window.pageYOffset < scrollY, + 'window should be scrolled up and left' + ); + teardown(); + }, + }); + }, + }); + }); +})(this); diff --git a/tests/grid-options/dragAxis.js b/tests/grid-options/dragAxis.js index 9e339d2c..6ab2870c 100644 --- a/tests/grid-options/dragAxis.js +++ b/tests/grid-options/dragAxis.js @@ -1,57 +1,20 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('dragAxis: should allow dragging items on x and y axis by default', function (assert) { - assert.expect(2); var done = assert.async(); var container = utils.createGridElements({ containerStyles: { position: 'relative', - width: '70px' - } - }); - var grid = new Muuri(container, { - dragEnabled: true - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - var left = parseInt(item.getElement().getBoundingClientRect().left); - var top = parseInt(item.getElement().getBoundingClientRect().top); - - grid.on('dragEnd', function () { - var newLeft = parseInt(item.getElement().getBoundingClientRect().left); - var newTop = parseInt(item.getElement().getBoundingClientRect().top); - assert.strictEqual(newLeft, left + 70, 'left'); - assert.strictEqual(newTop, top + 70, 'top'); - }); - - utils.dragElement(item.getElement(), 70, 70, teardown); - - }); - - QUnit.test('dragAxis: when set to "xy" items should be only moved on x-axis and y-axis', function (assert) { - - assert.expect(2); - - var done = assert.async(); - var container = utils.createGridElements({ - containerStyles: { - position: 'relative', - width: '70px' - } + width: '70px', + }, }); var grid = new Muuri(container, { dragEnabled: true, - dragAxis: 'xy' }); var item = grid.getItems()[0]; var teardown = function () { @@ -69,24 +32,68 @@ assert.strictEqual(newTop, top + 70, 'top'); }); - utils.dragElement(item.getElement(), 70, 70, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 70, + onFinished: teardown, + }); }); - QUnit.test('dragAxis: when set to "x" items should be only moved on x-axis', function (assert) { + QUnit.test( + 'dragAxis: when set to "xy" items should be moved on x-axis and y-axis', + function (assert) { + assert.expect(2); + + var done = assert.async(); + var container = utils.createGridElements({ + containerStyles: { + position: 'relative', + width: '70px', + }, + }); + var grid = new Muuri(container, { + dragEnabled: true, + dragAxis: 'xy', + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + var left = parseInt(item.getElement().getBoundingClientRect().left); + var top = parseInt(item.getElement().getBoundingClientRect().top); + + grid.on('dragEnd', function () { + var newLeft = parseInt(item.getElement().getBoundingClientRect().left); + var newTop = parseInt(item.getElement().getBoundingClientRect().top); + assert.strictEqual(newLeft, left + 70, 'left'); + assert.strictEqual(newTop, top + 70, 'top'); + }); + + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 70, + onFinished: teardown, + }); + } + ); + QUnit.test('dragAxis: when set to "x" items should be only moved on x-axis', function (assert) { assert.expect(2); var done = assert.async(); var container = utils.createGridElements({ containerStyles: { position: 'relative', - width: '70px' - } + width: '70px', + }, }); var grid = new Muuri(container, { dragEnabled: true, - dragAxis: 'x' + dragAxis: 'x', }); var item = grid.getItems()[0]; var teardown = function () { @@ -104,24 +111,27 @@ assert.strictEqual(newTop, top, 'top'); }); - utils.dragElement(item.getElement(), 70, 70, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 70, + onFinished: teardown, + }); }); QUnit.test('dragAxis: when set to "y" items should be only moved on y-axis', function (assert) { - assert.expect(2); var done = assert.async(); var container = utils.createGridElements({ containerStyles: { position: 'relative', - width: '70px' - } + width: '70px', + }, }); var grid = new Muuri(container, { dragEnabled: true, - dragAxis: 'y' + dragAxis: 'y', }); var item = grid.getItems()[0]; var teardown = function () { @@ -139,8 +149,11 @@ assert.strictEqual(newTop, top + 70, 'top'); }); - utils.dragElement(item.getElement(), 70, 70, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 70, + onFinished: teardown, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/dragContainer.js b/tests/grid-options/dragContainer.js index 0a316437..82518f29 100644 --- a/tests/grid-options/dragContainer.js +++ b/tests/grid-options/dragContainer.js @@ -1,17 +1,15 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('dragContainer: should be the grid container by default', function (assert) { - assert.expect(1); var done = assert.async(); var container = utils.createGridElements(); var grid = new Muuri(container, { - dragEnabled: true + dragEnabled: true, }); var item = grid.getItems()[0]; var teardown = function () { @@ -24,33 +22,42 @@ assert.strictEqual(item.getElement().parentNode, container); }); - utils.dragElement(item.getElement(), 100, 100, teardown); - - }); - - QUnit.test('dragContainer: should define the element the dragged item is appended to during drag', function (assert) { - - assert.expect(1); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container, { - dragEnabled: true, - dragContainer: document.body - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.on('dragStart', function () { - assert.strictEqual(item.getElement().parentNode, document.body); + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + onFinished: teardown, }); - - utils.dragElement(item.getElement(), 100, 100, teardown); - }); -})(this); \ No newline at end of file + QUnit.test( + 'dragContainer: should define the element the dragged item is appended to during drag', + function (assert) { + assert.expect(1); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container, { + dragEnabled: true, + dragContainer: document.body, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.on('dragStart', function () { + assert.strictEqual(item.getElement().parentNode, document.body); + }); + + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + onFinished: teardown, + }); + } + ); +})(this); diff --git a/tests/grid-options/dragEnabled.js b/tests/grid-options/dragEnabled.js index 3cf20f19..99a909da 100644 --- a/tests/grid-options/dragEnabled.js +++ b/tests/grid-options/dragEnabled.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('dragEnabled: drag should be disabled by default', function (assert) { - assert.expect(0); var done = assert.async(); @@ -22,18 +20,21 @@ assert.strictEqual(true, false, 'drag should not be started'); }); - utils.dragElement(item.getElement(), 100, 100, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + onFinished: teardown, + }); }); QUnit.test('dragEnabled: drag is enabled when provided true is provided', function (assert) { - assert.expect(1); var done = assert.async(); var container = utils.createGridElements(); var grid = new Muuri(container, { - dragEnabled: true + dragEnabled: true, }); var item = grid.getItems()[0]; var teardown = function () { @@ -46,8 +47,11 @@ assert.strictEqual(true, true, 'drag should be started'); }); - utils.dragElement(item.getElement(), 100, 100, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + onFinished: teardown, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/dragPlaceholder.js b/tests/grid-options/dragPlaceholder.js new file mode 100644 index 00000000..e8ba7ab1 --- /dev/null +++ b/tests/grid-options/dragPlaceholder.js @@ -0,0 +1,178 @@ +(function (window) { + var Muuri = window.Muuri; + + QUnit.module('Grid options'); + + QUnit.test('dragPlaceholder: should not be enabled by default', function (assert) { + assert.expect(1); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container, { dragEnabled: true }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.on('dragStart', function () { + assert.strictEqual(item._dragPlaceholder.isActive(), false, ''); + }); + + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 70, + onFinished: teardown, + }); + }); + + QUnit.test('dragPlaceholder: should be enabled when `enabled` is set to true', function (assert) { + assert.expect(12); + + var done = assert.async(); + var placeholderClassName = 'i-am-placeholder'; + var container = utils.createGridElements(); + var phElem = document.createElement('div'); + var grid = new Muuri(container, { + itemPlaceholderClass: placeholderClassName, + dragEnabled: true, + dragPlaceholder: { + enabled: true, + createElement: function (draggedItem) { + assert.strictEqual(arguments.length, 1, 'createElement should receive one argument'); + assert.strictEqual( + draggedItem, + item, + 'createElement first argument should be the dragged item' + ); + return phElem; + }, + onCreate: function (draggedItem, placeholderElem) { + assert.strictEqual(arguments.length, 2, 'onCreate: should receive two arguments'); + assert.strictEqual( + draggedItem, + item, + 'onCreate: first argument should be the dragged item' + ); + assert.strictEqual( + placeholderElem, + phElem, + 'onCreate: second argument should be the placeholder element' + ); + assert.strictEqual( + placeholderElem.classList.contains(placeholderClassName), + true, + 'onCreate: placeholder element should have itemPlaceholderClass applied' + ); + }, + onRemove: function (draggedItem, placeholderElem) { + assert.strictEqual(arguments.length, 2, 'onRemove: should receive two arguments'); + assert.strictEqual( + draggedItem, + item, + 'onRemove: first argument should be the dragged item' + ); + assert.strictEqual( + placeholderElem, + phElem, + 'onRemove: second argument should be the placeholder element' + ); + assert.strictEqual( + placeholderElem.classList.contains(placeholderClassName), + false, + 'onRemove: placeholder element should have itemPlaceholderClass removed' + ); + teardown(); + }, + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.on('dragStart', function () { + var ph = item._dragPlaceholder; + assert.strictEqual(ph.isActive(), true, 'placeholder should be active'); + assert.strictEqual( + ph.getElement(), + phElem, + 'placeholder element should be the element returned from createElement method' + ); + }); + + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + }); + }); + + QUnit.test( + 'dragPlaceholder: placeholder element dimensions should be kept in sync with item element dimensions', + function (assert) { + assert.expect(4); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container, { dragEnabled: true, dragPlaceholder: { enabled: true } }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.on('dragStart', function () { + var phElem = item._dragPlaceholder.getElement(); + var itemElem = item.getElement(); + var phRect = phElem.getBoundingClientRect(); + var itemRect = itemElem.getBoundingClientRect(); + + assert.strictEqual( + phRect.width, + itemRect.width, + 'placeholder width should match item width' + ); + assert.strictEqual( + phRect.height, + itemRect.height, + 'placeholder height should match item height' + ); + + itemElem.style.width = '200px'; + itemElem.style.height = '10px'; + grid.refreshItems(); + }); + + grid.on('dragEnd', function () { + var phElem = item._dragPlaceholder.getElement(); + var itemElem = item.getElement(); + var phRect = phElem.getBoundingClientRect(); + var itemRect = itemElem.getBoundingClientRect(); + + assert.strictEqual( + phRect.width, + itemRect.width, + 'placeholder width should match item width after item is resized and refreshed' + ); + assert.strictEqual( + phRect.height, + itemRect.height, + 'placeholder height should match item height after item is resized and refreshed' + ); + }); + + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); + } + ); +})(this); diff --git a/tests/grid-options/dragSort.js b/tests/grid-options/dragSort.js index ba3c29fc..79e4ffe1 100644 --- a/tests/grid-options/dragSort.js +++ b/tests/grid-options/dragSort.js @@ -1,22 +1,20 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('dragSort: should be enabled by default', function (assert) { - assert.expect(1); var done = assert.async(); var container = utils.createGridElements({ containerStyles: { position: 'relative', - width: '70px' - } + width: '70px', + }, }); var grid = new Muuri(container, { - dragEnabled: true + dragEnabled: true, }); var item = grid.getItems()[0]; var teardown = function () { @@ -29,24 +27,27 @@ assert.strictEqual(true, true); }); - utils.dragElement(item.getElement(), 0, 70, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); }); QUnit.test('dragSort: should be disabled if false is provided', function (assert) { - assert.expect(0); var done = assert.async(); var container = utils.createGridElements({ containerStyles: { position: 'relative', - width: '70px' - } + width: '70px', + }, }); var grid = new Muuri(container, { dragEnabled: true, - dragSort: false + dragSort: false, }); var item = grid.getItems()[0]; var teardown = function () { @@ -59,12 +60,15 @@ assert.strictEqual(true, false, 'items should not be moved'); }); - utils.dragElement(item.getElement(), 0, 70, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); }); QUnit.test('dragSort: by default items should not be draggable between grids', function (assert) { - assert.expect(0); var done = assert.async(); @@ -73,46 +77,46 @@ position: 'absolute', left: '0px', top: '0px', - width: '50px' + width: '50px', }, itemStyles: { position: 'absolute', height: '50px', width: '100%', margin: '10px', - background: '#000' - } + background: '#000', + }, }); var containerB = utils.createGridElements({ containerStyles: { position: 'absolute', left: '70px', top: '0px', - width: '50px' + width: '50px', }, itemStyles: { position: 'absolute', height: '50px', width: '100%', margin: '10px', - background: '#000' - } + background: '#000', + }, }); var gridA = new Muuri(containerA, { dragEnabled: true, dragSortInterval: 100, dragSortPredicate: { threshold: 50, - action: 'move' - } + action: 'move', + }, }); var gridB = new Muuri(containerB, { dragEnabled: true, dragSortInterval: 100, dragSortPredicate: { threshold: 50, - action: 'move' - } + action: 'move', + }, }); var item = gridA.getItems()[0]; var teardown = function () { @@ -123,89 +127,98 @@ done(); }; - gridB.on('receive', function (data) { + gridB.on('receive', function () { assert.strictEqual(true, false); }); - utils.dragElement(item.getElement(), 70, 0, teardown); - - }); - - QUnit.test('dragSort: should accept a function that returns an array of grid instances', function (assert) { - - assert.expect(1); - - var done = assert.async(); - var containerA = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '0px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var containerB = utils.createGridElements({ - containerStyles: { - position: 'absolute', - left: '70px', - top: '0px', - width: '50px' - }, - itemStyles: { - position: 'absolute', - height: '50px', - width: '100%', - margin: '10px', - background: '#000' - } - }); - var grids = []; - var gridA = new Muuri(containerA, { - dragEnabled: true, - dragSort: function () { - return grids; - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var gridB = new Muuri(containerB, { - dragEnabled: true, - dragSort: function () { - return grids; - }, - dragSortInterval: 100, - dragSortPredicate: { - threshold: 50, - action: 'move' - } - }); - var item = gridA.getItems()[0]; - var teardown = function () { - gridA.destroy(); - gridB.destroy(); - containerA.parentNode.removeChild(containerA); - containerB.parentNode.removeChild(containerB); - done(); - }; - - grids.push(gridA, gridB); - - gridB.on('receive', function (data) { - assert.strictEqual(true, true); + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 0, + onFinished: teardown, }); - - utils.dragElement(item.getElement(), 70, 0, teardown); - }); -})(this); \ No newline at end of file + QUnit.test( + 'dragSort: should accept a function that returns an array of grid instances', + function (assert) { + assert.expect(1); + + var done = assert.async(); + var containerA = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '0px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var containerB = utils.createGridElements({ + containerStyles: { + position: 'absolute', + left: '70px', + top: '0px', + width: '50px', + }, + itemStyles: { + position: 'absolute', + height: '50px', + width: '100%', + margin: '10px', + background: '#000', + }, + }); + var grids = []; + var gridA = new Muuri(containerA, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var gridB = new Muuri(containerB, { + dragEnabled: true, + dragSort: function () { + return grids; + }, + dragSortInterval: 100, + dragSortPredicate: { + threshold: 50, + action: 'move', + }, + }); + var item = gridA.getItems()[0]; + var teardown = function () { + gridA.destroy(); + gridB.destroy(); + containerA.parentNode.removeChild(containerA); + containerB.parentNode.removeChild(containerB); + done(); + }; + + grids.push(gridA, gridB); + + gridB.on('receive', function () { + assert.strictEqual(true, true); + }); + + utils.dragElement({ + element: item.getElement(), + x: 70, + y: 0, + onFinished: teardown, + }); + } + ); +})(this); diff --git a/tests/grid-options/dragSortPredicate.js b/tests/grid-options/dragSortPredicate.js index 182c1e4e..f5f4910d 100644 --- a/tests/grid-options/dragSortPredicate.js +++ b/tests/grid-options/dragSortPredicate.js @@ -1,22 +1,20 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('dragSortPredicate: the default action should be "move"', function (assert) { - assert.expect(1); var done = assert.async(); var container = utils.createGridElements({ containerStyles: { position: 'relative', - width: '140px' - } + width: '140px', + }, }); var grid = new Muuri(container, { - dragEnabled: true + dragEnabled: true, }); var item = grid.getItems()[0]; var teardown = function () { @@ -26,32 +24,35 @@ }; function onMove(data) { - grid.off('move', onMove) + grid.off('move', onMove); assert.strictEqual(data.action, 'move', 'the movement action should be "move"'); } grid.on('move', onMove); - utils.dragElement(item.getElement(), 0, 70, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); }); QUnit.test('dragSortPredicate: should allow "swap" as the sort action', function (assert) { - assert.expect(1); var done = assert.async(); var container = utils.createGridElements({ containerStyles: { position: 'relative', - width: '140px' - } + width: '140px', + }, }); var grid = new Muuri(container, { dragEnabled: true, dragSortPredicate: { - action: 'swap' - } + action: 'swap', + }, }); var item = grid.getItems()[0]; var teardown = function () { @@ -67,189 +68,234 @@ grid.on('move', onMove); - utils.dragElement(item.getElement(), 0, 70, teardown); - - }); - - QUnit.test('dragSortPredicate: should receive the dragged item and current Dragger event as it`s arguments', function (assert) { - - assert.expect(3); - - var done = assert.async(); - var container = utils.createGridElements({ - containerStyles: { - position: 'relative', - width: '140px' - } + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, }); - var isChecked = false; - var grid = new Muuri(container, { - dragEnabled: true, - dragSortPredicate: function (draggedItem, ev) { - if (!isChecked) { - assert.strictEqual(arguments.length, 2, 'predicate should receive two aguments'); - assert.strictEqual(draggedItem, item, 'predicate first argument should be the dragged item'); - assert.strictEqual(utils.isDraggerEvent(ev), true, 'predicate second argument should be a Dragger event'); - isChecked = true; - } - } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - utils.dragElement(item.getElement(), 0, 70, teardown); - }); - QUnit.test('dragSortPredicate: should not trigger sorting if a falsy value is returned', function (assert) { - - assert.expect(0); - - var done = assert.async(); - var container = utils.createGridElements({ - containerStyles: { - position: 'relative', - width: '140px' - } - }); - var grid = new Muuri(container, { - dragEnabled: true, - dragSortPredicate: function (draggedItem, ev) { - return false; - } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - function onMove() { - grid.off('move', onMove); - assert.strictEqual(true, false, 'move should not be triggered'); + QUnit.test( + 'dragSortPredicate: should receive the dragged item and current Dragger event as it`s arguments', + function (assert) { + assert.expect(3); + + var done = assert.async(); + var container = utils.createGridElements({ + containerStyles: { + position: 'relative', + width: '140px', + }, + }); + var isChecked = false; + var grid = new Muuri(container, { + dragEnabled: true, + dragSortPredicate: function (draggedItem, ev) { + if (!isChecked) { + assert.strictEqual(arguments.length, 2, 'predicate should receive two aguments'); + assert.strictEqual( + draggedItem, + item, + 'predicate first argument should be the dragged item' + ); + assert.strictEqual( + utils.isDraggerEvent(ev), + true, + 'predicate second argument should be a Dragger event' + ); + isChecked = true; + } + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); } - - grid.on('move', onMove); - - utils.dragElement(item.getElement(), 0, 70, teardown); - - }); - - QUnit.test('dragSortPredicate: should trigger sorting if an object with index is returned', function (assert) { - - assert.expect(2); - - var done = assert.async(); - var container = utils.createGridElements({ - containerStyles: { - position: 'relative', - width: '140px' - } - }); - var grid = new Muuri(container, { - dragEnabled: true, - dragSortPredicate: function () { - return { - index: -1, - action: 'swap' - }; + ); + + QUnit.test( + 'dragSortPredicate: should not trigger sorting if a falsy value is returned', + function (assert) { + assert.expect(0); + + var done = assert.async(); + var container = utils.createGridElements({ + containerStyles: { + position: 'relative', + width: '140px', + }, + }); + var grid = new Muuri(container, { + dragEnabled: true, + dragSortPredicate: function () { + return false; + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + function onMove() { + grid.off('move', onMove); + assert.strictEqual(true, false, 'move should not be triggered'); } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - function onMove(data) { - grid.off('move', onMove); - assert.strictEqual(data.action, 'swap', 'sort action should be "swap"'); - assert.strictEqual(data.toIndex, grid.getItems().length - 1, 'target index should be the last index'); - } - - grid.on('move', onMove); - utils.dragElement(item.getElement(), 0, 70, teardown); - - }); + grid.on('move', onMove); - QUnit.test('dragSortPredicate: should allow using Muuri.ItemDrag.defaultSortPredicate manually without options', function (assert) { - - assert.expect(1); - - var done = assert.async(); - var container = utils.createGridElements({ - containerStyles: { - position: 'relative', - width: '140px' - } - }); - var grid = new Muuri(container, { - dragEnabled: true, - dragSortPredicate: function (item) { - return Muuri.ItemDrag.defaultSortPredicate(item); - } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - function onMove(data) { - grid.off('move', onMove); - assert.strictEqual(data.action, 'move', 'sort action should be "move"'); + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); } - - grid.on('move', onMove); - - utils.dragElement(item.getElement(), 0, 70, teardown); - - }); - - QUnit.test('dragSortPredicate: should allow using Muuri.ItemDrag.defaultSortPredicate manually with options', function (assert) { - - assert.expect(1); - - var done = assert.async(); - var container = utils.createGridElements({ - containerStyles: { - position: 'relative', - width: '140px' - } - }); - var grid = new Muuri(container, { - dragEnabled: true, - dragSortPredicate: function (item) { - return Muuri.ItemDrag.defaultSortPredicate(item, { - threshold: 30, - action: 'swap' - }); + ); + + QUnit.test( + 'dragSortPredicate: should trigger sorting if an object with index is returned', + function (assert) { + assert.expect(2); + + var done = assert.async(); + var container = utils.createGridElements({ + containerStyles: { + position: 'relative', + width: '140px', + }, + }); + var grid = new Muuri(container, { + dragEnabled: true, + dragSortPredicate: function () { + return { + index: -1, + action: 'swap', + }; + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + function onMove(data) { + grid.off('move', onMove); + assert.strictEqual(data.action, 'swap', 'sort action should be "swap"'); + assert.strictEqual( + data.toIndex, + grid.getItems().length - 1, + 'target index should be the last index' + ); } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - function onMove(data) { - grid.off('move', onMove); - assert.strictEqual(data.action, 'swap', 'sort action should be "swap"'); + grid.on('move', onMove); + + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); } + ); + + QUnit.test( + 'dragSortPredicate: should allow using Muuri.ItemDrag.defaultSortPredicate manually without options', + function (assert) { + assert.expect(1); + + var done = assert.async(); + var container = utils.createGridElements({ + containerStyles: { + position: 'relative', + width: '140px', + }, + }); + var grid = new Muuri(container, { + dragEnabled: true, + dragSortPredicate: function (item) { + return Muuri.ItemDrag.defaultSortPredicate(item); + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + function onMove(data) { + grid.off('move', onMove); + assert.strictEqual(data.action, 'move', 'sort action should be "move"'); + } - grid.on('move', onMove); + grid.on('move', onMove); - utils.dragElement(item.getElement(), 0, 70, teardown); + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); + } + ); + + QUnit.test( + 'dragSortPredicate: should allow using Muuri.ItemDrag.defaultSortPredicate manually with options', + function (assert) { + assert.expect(1); + + var done = assert.async(); + var container = utils.createGridElements({ + containerStyles: { + position: 'relative', + width: '140px', + }, + }); + var grid = new Muuri(container, { + dragEnabled: true, + dragSortPredicate: function (item) { + return Muuri.ItemDrag.defaultSortPredicate(item, { + threshold: 30, + action: 'swap', + }); + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + function onMove(data) { + grid.off('move', onMove); + assert.strictEqual(data.action, 'swap', 'sort action should be "swap"'); + } - }); + grid.on('move', onMove); -})(this); \ No newline at end of file + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); + } + ); +})(this); diff --git a/tests/grid-options/dragStartPredicate.js b/tests/grid-options/dragStartPredicate.js index 1f235e1c..74d48419 100644 --- a/tests/grid-options/dragStartPredicate.js +++ b/tests/grid-options/dragStartPredicate.js @@ -1,41 +1,53 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); - QUnit.test('dragStartPredicate: should receive the dragged item and current Dragger event as it`s arguments', function (assert) { - - assert.expect(3); - - var done = assert.async(); - var container = utils.createGridElements(); - var isChecked = false; - var grid = new Muuri(container, { - dragEnabled: true, - dragStartPredicate: function (draggedItem, ev) { - if (!isChecked) { - assert.strictEqual(arguments.length, 2, 'predicate should receive two aguments'); - assert.strictEqual(draggedItem, item, 'predicate first argument should be the dragged item'); - assert.strictEqual(utils.isDraggerEvent(ev), true, 'predicate second argument should be a Dragger event'); - isChecked = true; - } - return true; - } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - utils.dragElement(item.getElement(), 0, 70, teardown); - - }); + QUnit.test( + 'dragStartPredicate: should receive the dragged item and current Dragger event as it`s arguments', + function (assert) { + assert.expect(3); + + var done = assert.async(); + var container = utils.createGridElements(); + var isChecked = false; + var grid = new Muuri(container, { + dragEnabled: true, + dragStartPredicate: function (draggedItem, ev) { + if (!isChecked) { + assert.strictEqual(arguments.length, 2, 'predicate should receive two aguments'); + assert.strictEqual( + draggedItem, + item, + 'predicate first argument should be the dragged item' + ); + assert.strictEqual( + utils.isDraggerEvent(ev), + true, + 'predicate second argument should be a Dragger event' + ); + isChecked = true; + } + return true; + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); + } + ); QUnit.test('dragStartPredicate: returning true should resolve the predicate', function (assert) { - assert.expect(4); var done = assert.async(); @@ -46,7 +58,7 @@ dragStartPredicate: function () { ++counter; return true; - } + }, }); var item = grid.getItems()[0]; var teardown = function () { @@ -57,17 +69,29 @@ function onDragStart() { grid.off('dragStart', onDragStart); - assert.strictEqual(true, true, 'a resolved predicate should start the dragging procedure and trigger dragStart event'); + assert.strictEqual( + true, + true, + 'a resolved predicate should start the dragging procedure and trigger dragStart event' + ); } function onDragMove() { grid.off('dragMove', onDragMove); - assert.strictEqual(true, true, 'a resolved predicate should start the dragging procedure and trigger dragMove event'); + assert.strictEqual( + true, + true, + 'a resolved predicate should start the dragging procedure and trigger dragMove event' + ); } function onDragEnd() { grid.off('dragEnd', onDragEnd); - assert.strictEqual(true, true, 'a resolved predicate should start the dragging procedure and trigger dragEnd event'); + assert.strictEqual( + true, + true, + 'a resolved predicate should start the dragging procedure and trigger dragEnd event' + ); } function onDragReleaseStart() { @@ -76,17 +100,20 @@ } grid - .on('dragStart', onDragStart) - .on('dragMove', onDragMove) - .on('dragEnd', onDragEnd) - .on('dragReleaseStart', onDragReleaseStart); - - utils.dragElement(item.getElement(), 0, 70, teardown); - + .on('dragStart', onDragStart) + .on('dragMove', onDragMove) + .on('dragEnd', onDragEnd) + .on('dragReleaseStart', onDragReleaseStart); + + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: teardown, + }); }); QUnit.test('dragStartPredicate: returning false should reject the predicate', function (assert) { - assert.expect(1); var done = assert.async(); @@ -97,42 +124,7 @@ dragStartPredicate: function () { ++counter; return false; - } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid - .on('dragStart', function () { - assert.strictEqual(true, false, 'a rejected predicate should not start the dragging procedure'); - }) - .on('dragMove', function () { - assert.strictEqual(true, false, 'a rejected predicate should not start the dragging procedure'); - }); - - utils.dragElement(item.getElement(), 0, 70, function () { - assert.strictEqual(counter, 2, 'predicate should be called twice'); - teardown(); - }); - - }); - - QUnit.test('dragStartPredicate: returning nothing (undefined) should keep calling the predicate and not start the drag procedure', function (assert) { - - assert.expect(1); - - var done = assert.async(); - var container = utils.createGridElements(); - var counter = 0; - var grid = new Muuri(container, { - dragEnabled: true, - dragStartPredicate: function () { - ++counter; - } + }, }); var item = grid.getItems()[0]; var teardown = function () { @@ -142,150 +134,165 @@ }; grid - .on('dragStart', function () { - assert.strictEqual(true, false, 'a rejected predicate should not start the dragging procedure'); - }) - .on('dragMove', function () { - assert.strictEqual(true, false, 'a rejected predicate should not start the dragging procedure'); - }); - - utils.dragElement(item.getElement(), 0, 70, function () { - assert.strictEqual(counter > 2, true, 'predicate should be called more than twice'); - teardown(); + .on('dragStart', function () { + assert.strictEqual( + true, + false, + 'a rejected predicate should not start the dragging procedure' + ); + }) + .on('dragMove', function () { + assert.strictEqual( + true, + false, + 'a rejected predicate should not start the dragging procedure' + ); + }); + + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: function () { + assert.strictEqual(counter, 1, 'predicate should be called once'); + teardown(); + }, }); - }); - QUnit.test('dragStartPredicate: delay - drag should start after a delay if delay is defined', function (assert) { - - assert.expect(3); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container, { - dragEnabled: true, - dragStartPredicate: { - delay: 100 - } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid - .on('dragStart', function () { - assert.ok(true, 'dragStart event should be emitted after the delay even if there was no movement'); - }) - .on('dragMove', function () { - assert.ok(false, 'dragMove event should not be emitted if there was no movement'); - }) - .on('dragEnd', function () { - assert.ok(true, 'dragEnd event should be emitted even if there was no movement'); - }); - - window.setTimeout(function () { - assert.strictEqual(item.isDragging(), false, 'the item should not be in dragged state before the delay is finished'); - }, 90); - - utils.dragElement(item.getElement(), 0, 0, teardown); - - }); - - QUnit.test('dragStartPredicate: distance - drag should start after a distance if distance is defined', function (assert) { - - assert.expect(1); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container, { - dragEnabled: true, - dragStartPredicate: { - distance: 10 - } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.on('dragStart', function (item, e) { - assert.ok(e.distance >= 10, 'dragStart event should be emitted after the specified distance is dragged'); - }); - - utils.dragElement(item.getElement(), 15, 15, teardown); - - }); - - QUnit.test('dragStartPredicate: handle - if a handle is specified the drag should start when the handle is dragged', function (assert) { - - assert.expect(1); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container, { - dragEnabled: true, - dragStartPredicate: { - handle: '.handle' - } - }); - var item = grid.getItems()[0]; - var handle = document.createElement('div'); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - item.getElement().appendChild(handle); - handle.classList.add('handle'); - utils.setStyles(handle, { - position: 'absolute', - width: '30px', - height: '30px', - left: '-30px', - top: '-30px' - }); - - grid.on('dragStart', function (item, e) { - assert.ok(true); - }); - - utils.dragElement(item.getElement(), 15, 15, function () { - utils.dragElement(handle, 15, 15, teardown); - }); - - }); - - QUnit.test('dragStartPredicate: handle - drag should not start if the drag pointer is outside the handle when dragging should start', function (assert) { - - assert.expect(0); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container, { - dragEnabled: true, - dragStartPredicate: { - distance: 500 - } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.on('dragStart', function (item, e) { - assert.ok(false); - }); - - utils.dragElement(item.getElement(), 600, 600, teardown); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'dragStartPredicate: returning nothing (undefined) should keep calling the predicate and not start the drag procedure', + function (assert) { + assert.expect(1); + + var done = assert.async(); + var container = utils.createGridElements(); + var counter = 0; + var grid = new Muuri(container, { + dragEnabled: true, + dragStartPredicate: function () { + ++counter; + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid + .on('dragStart', function () { + assert.strictEqual( + true, + false, + 'a rejected predicate should not start the dragging procedure' + ); + }) + .on('dragMove', function () { + assert.strictEqual( + true, + false, + 'a rejected predicate should not start the dragging procedure' + ); + }); + + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 70, + onFinished: function () { + assert.strictEqual(counter > 2, true, 'predicate should be called more than twice'); + teardown(); + }, + }); + } + ); + + QUnit.test( + 'dragStartPredicate: delay - drag should start after a delay if delay is defined', + function (assert) { + assert.expect(3); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container, { + dragEnabled: true, + dragStartPredicate: { + delay: 100, + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid + .on('dragStart', function () { + assert.ok( + true, + 'dragStart event should be emitted after the delay even if there was no movement' + ); + }) + .on('dragMove', function () { + assert.ok(false, 'dragMove event should not be emitted if there was no movement'); + }) + .on('dragEnd', function () { + assert.ok(true, 'dragEnd event should be emitted even if there was no movement'); + }); + + window.setTimeout(function () { + assert.strictEqual( + item.isDragging(), + false, + 'the item should not be in dragged state before the delay is finished' + ); + }, 90); + + utils.dragElement({ + element: item.getElement(), + x: 0, + y: 0, + onFinished: teardown, + }); + } + ); + + QUnit.test( + 'dragStartPredicate: distance - drag should start after a distance if distance is defined', + function (assert) { + assert.expect(1); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container, { + dragEnabled: true, + dragStartPredicate: { + distance: 10, + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.on('dragStart', function (item, e) { + assert.ok( + e.distance >= 10, + 'dragStart event should be emitted after the specified distance is dragged' + ); + }); + + utils.dragElement({ + element: item.getElement(), + x: 15, + y: 15, + onFinished: teardown, + }); + } + ); +})(this); diff --git a/tests/grid-options/hideDuration.js b/tests/grid-options/hideDuration.js index 27419f29..99214167 100644 --- a/tests/grid-options/hideDuration.js +++ b/tests/grid-options/hideDuration.js @@ -1,16 +1,14 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('hideDuration: should disable hide animation when set to 0', function (assert) { - assert.expect(2); var container = utils.createGridElements(); var grid = new Muuri(container, { - hideDuration: 0 + hideDuration: 0, }); var item = grid.getItems()[0]; var teardown = function () { @@ -18,11 +16,9 @@ container.parentNode.removeChild(container); }; - grid.hide(item); + grid.hide([item]); assert.strictEqual(item.isVisible(), false, 'item should be hidden'); assert.strictEqual(item.isHiding(), false, 'item should not be hiding'); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/itemClass.js b/tests/grid-options/itemClass.js index f04dc747..ce3087b6 100644 --- a/tests/grid-options/itemClass.js +++ b/tests/grid-options/itemClass.js @@ -1,16 +1,14 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('itemClass: should define the classname for the item elements', function (assert) { - assert.expect(1); var container = utils.createGridElements(); var grid = new Muuri(container, { - itemClass: 'foo' + itemClass: 'foo', }); var teardown = function () { grid.destroy(); @@ -19,7 +17,5 @@ assert.strictEqual(utils.matches(grid.getItems()[0].getElement(), '.foo'), true); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/itemDraggingClass.js b/tests/grid-options/itemDraggingClass.js index c30e4f32..517fcb92 100644 --- a/tests/grid-options/itemDraggingClass.js +++ b/tests/grid-options/itemDraggingClass.js @@ -1,18 +1,18 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); - QUnit.test('itemDraggingClass: should define the classname for dragged item elements', function (assert) { - + QUnit.test('itemDraggingClass: should define the classname for dragged item elements', function ( + assert + ) { assert.expect(3); var done = assert.async(); - var container = utils.createGridElements({itemCount: 3}); + var container = utils.createGridElements({ itemCount: 3 }); var grid = new Muuri(container, { itemDraggingClass: 'foo', - dragEnabled: true + dragEnabled: true, }); var item = grid.getItems()[0]; var teardown = function () { @@ -21,18 +21,33 @@ done(); }; - assert.strictEqual(utils.matches(item.getElement(), '.foo'), false, 'the classname should not be applied before dragging starts'); + assert.strictEqual( + utils.matches(item.getElement(), '.foo'), + false, + 'the classname should not be applied before dragging starts' + ); grid.on('dragStart', function () { - assert.strictEqual(utils.matches(item.getElement(), '.foo'), true, 'the classname should be applied when dragging starts'); + assert.strictEqual( + utils.matches(item.getElement(), '.foo'), + true, + 'the classname should be applied when dragging starts' + ); }); grid.on('dragEnd', function () { - assert.strictEqual(utils.matches(item.getElement(), '.foo'), false, 'the classname should be removed when dragging ends'); + assert.strictEqual( + utils.matches(item.getElement(), '.foo'), + false, + 'the classname should be removed when dragging ends' + ); }); - utils.dragElement(item.getElement(), 100, 100, teardown); - + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + onFinished: teardown, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/itemHiddenClass.js b/tests/grid-options/itemHiddenClass.js index f391889c..d42d3b85 100644 --- a/tests/grid-options/itemHiddenClass.js +++ b/tests/grid-options/itemHiddenClass.js @@ -1,27 +1,33 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); - QUnit.test('itemHiddenClass: should define the classname for hidden item elements', function (assert) { - + QUnit.test('itemHiddenClass: should define the classname for hidden item elements', function ( + assert + ) { assert.expect(2); var container = utils.createGridElements(); var grid = new Muuri(container, { - itemHiddenClass: 'foo' + itemHiddenClass: 'foo', }); var teardown = function () { grid.destroy(); container.parentNode.removeChild(container); }; - grid.hide(0); - assert.strictEqual(utils.matches(grid.getItems()[0].getElement(), '.foo'), true, 'hidden items should have the classname'); - assert.strictEqual(utils.matches(grid.getItems()[1].getElement(), '.foo'), false, 'visible items should not have the classname'); + grid.hide(grid.getItems(0)); + assert.strictEqual( + utils.matches(grid.getItems()[0].getElement(), '.foo'), + true, + 'hidden items should have the classname' + ); + assert.strictEqual( + utils.matches(grid.getItems()[1].getElement(), '.foo'), + false, + 'visible items should not have the classname' + ); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/itemPositioningClass.js b/tests/grid-options/itemPositioningClass.js index 805603b8..414a7750 100644 --- a/tests/grid-options/itemPositioningClass.js +++ b/tests/grid-options/itemPositioningClass.js @@ -1,32 +1,43 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); - QUnit.test('itemPositioningClass: should define the classname for positioning item elements', function (assert) { - - assert.expect(3); - - var done = assert.async(); - var container = utils.createGridElements({itemCount: 3}); - var grid = new Muuri(container, { - itemPositioningClass: 'foo' - }); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.move(0, -1, {action: 'swap'}); - utils.raf(function () { - assert.strictEqual(utils.matches(grid.getItems()[0].getElement(), '.foo'), true, 'first item should be positioning'); - assert.strictEqual(utils.matches(grid.getItems()[2].getElement(), '.foo'), true, 'last item should be positioning'); - assert.strictEqual(utils.matches(grid.getItems()[1].getElement(), '.foo'), false, 'second item should not be positioning'); - teardown(); - }); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'itemPositioningClass: should define the classname for positioning item elements', + function (assert) { + assert.expect(3); + + var done = assert.async(); + var container = utils.createGridElements({ itemCount: 3 }); + var grid = new Muuri(container, { + itemPositioningClass: 'foo', + }); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.move(0, -1, { action: 'swap' }); + utils.raf(function () { + assert.strictEqual( + utils.matches(grid.getItems()[0].getElement(), '.foo'), + true, + 'first item should be positioning' + ); + assert.strictEqual( + utils.matches(grid.getItems()[2].getElement(), '.foo'), + true, + 'last item should be positioning' + ); + assert.strictEqual( + utils.matches(grid.getItems()[1].getElement(), '.foo'), + false, + 'second item should not be positioning' + ); + teardown(); + }); + } + ); +})(this); diff --git a/tests/grid-options/itemReleasingClass.js b/tests/grid-options/itemReleasingClass.js index 9c26e003..139a980c 100644 --- a/tests/grid-options/itemReleasingClass.js +++ b/tests/grid-options/itemReleasingClass.js @@ -1,39 +1,54 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); - QUnit.test('itemReleasingClass: should define the classname for released item elements', function (assert) { - - assert.expect(3); - - var done = assert.async(); - var container = utils.createGridElements({itemCount: 3}); - var grid = new Muuri(container, { - itemReleasingClass: 'foo', - dragEnabled: true - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - assert.strictEqual(utils.matches(item.getElement(), '.foo'), false, 'the classname should not be applied before release starts'); - - grid.on('dragReleaseStart', function () { - assert.strictEqual(utils.matches(item.getElement(), '.foo'), true, 'the classname should be applied when release starts'); - }); - - grid.on('dragReleaseEnd', function () { - assert.strictEqual(utils.matches(item.getElement(), '.foo'), false, 'the classname should be removed when release ends'); - teardown(); - }); - - utils.dragElement(item.getElement(), 100, 100); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'itemReleasingClass: should define the classname for released item elements', + function (assert) { + assert.expect(3); + + var done = assert.async(); + var container = utils.createGridElements({ itemCount: 3 }); + var grid = new Muuri(container, { + itemReleasingClass: 'foo', + dragEnabled: true, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + assert.strictEqual( + utils.matches(item.getElement(), '.foo'), + false, + 'the classname should not be applied before release starts' + ); + + grid.on('dragReleaseStart', function () { + assert.strictEqual( + utils.matches(item.getElement(), '.foo'), + true, + 'the classname should be applied when release starts' + ); + }); + + grid.on('dragReleaseEnd', function () { + assert.strictEqual( + utils.matches(item.getElement(), '.foo'), + false, + 'the classname should be removed when release ends' + ); + teardown(); + }); + + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + }); + } + ); +})(this); diff --git a/tests/grid-options/itemVisibleClass.js b/tests/grid-options/itemVisibleClass.js index 38f8bb07..21277c15 100644 --- a/tests/grid-options/itemVisibleClass.js +++ b/tests/grid-options/itemVisibleClass.js @@ -1,27 +1,33 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); - QUnit.test('itemVisibleClass: should define the classname for visible item elements', function (assert) { - + QUnit.test('itemVisibleClass: should define the classname for visible item elements', function ( + assert + ) { assert.expect(2); var container = utils.createGridElements(); var grid = new Muuri(container, { - itemVisibleClass: 'foo' + itemVisibleClass: 'foo', }); var teardown = function () { grid.destroy(); container.parentNode.removeChild(container); }; - grid.hide(0); - assert.strictEqual(utils.matches(grid.getItems()[0].getElement(), '.foo'), false, 'hidden items should not have the classname'); - assert.strictEqual(utils.matches(grid.getItems()[1].getElement(), '.foo'), true, 'visible items should have the classname'); + grid.hide(grid.getItems(0)); + assert.strictEqual( + utils.matches(grid.getItems()[0].getElement(), '.foo'), + false, + 'hidden items should not have the classname' + ); + assert.strictEqual( + utils.matches(grid.getItems()[1].getElement(), '.foo'), + true, + 'visible items should have the classname' + ); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/items.js b/tests/grid-options/items.js index 3c2bdfcb..9531febc 100644 --- a/tests/grid-options/items.js +++ b/tests/grid-options/items.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('items: should fetch all container`s child elements by default', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -15,13 +13,46 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(grid.getItems().map(function (item) { return item.getElement(); }), [].slice.call(container.children)); + assert.deepEqual( + grid.getItems().map(function (item) { + return item.getElement(); + }), + [].slice.call(container.children) + ); teardown(); - }); - QUnit.test('items: should fetch all container`s child elements that match the provided selector', function (assert) { + QUnit.test( + 'items: should fetch all container`s child elements that match the provided selector', + function (assert) { + assert.expect(1); + + var container = utils.createGridElements(); + var children = [].slice.call(container.children); + var targets = [0, 1, 2].map(function (i) { + children[i].classList.add('foo'); + return children[i]; + }); + container.classList.add('foo'); + var grid = new Muuri(container, { + items: '.foo', + }); + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + assert.deepEqual( + grid.getItems().map(function (item) { + return item.getElement(); + }), + targets + ); + teardown(); + } + ); + QUnit.test('items: should accept a node list', function (assert) { assert.expect(1); var container = utils.createGridElements(); @@ -30,61 +61,45 @@ children[i].classList.add('foo'); return children[i]; }); - container.classList.add('foo'); var grid = new Muuri(container, { - items: '.foo' + items: document.querySelectorAll('.foo'), }); var teardown = function () { grid.destroy(); container.parentNode.removeChild(container); }; - assert.deepEqual(grid.getItems().map(function (item) { return item.getElement(); }), targets); + assert.deepEqual( + grid.getItems().map(function (item) { + return item.getElement(); + }), + targets + ); teardown(); - }); - QUnit.test('items: should accept a node list', function (assert) { - + QUnit.test('items: should accept an array of elements', function (assert) { assert.expect(1); var container = utils.createGridElements(); var children = [].slice.call(container.children); var targets = [0, 1, 2].map(function (i) { - children[i].classList.add('foo'); return children[i]; }); var grid = new Muuri(container, { - items: document.querySelectorAll('.foo') - }); - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - assert.deepEqual(grid.getItems().map(function (item) { return item.getElement(); }), targets); - teardown(); - - }); - - QUnit.test('items: should accept an array of elements', function (assert) { - - assert.expect(1); - - var container = utils.createGridElements(); - var children = [].slice.call(container.children); - var targets = [0, 1, 2].map(function (i) { return children[i]; }); - var grid = new Muuri(container, { - items: targets + items: targets, }); var teardown = function () { grid.destroy(); container.parentNode.removeChild(container); }; - assert.deepEqual(grid.getItems().map(function (item) { return item.getElement(); }), targets); + assert.deepEqual( + grid.getItems().map(function (item) { + return item.getElement(); + }), + targets + ); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/layout.js b/tests/grid-options/layout.js index aab63288..104a1f0e 100644 --- a/tests/grid-options/layout.js +++ b/tests/grid-options/layout.js @@ -1,27 +1,25 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('layout: vertical - left/top', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {width: '140px'}); - utils.setStyles(children[0], {width: '70px'}); - utils.setStyles(children[3], {width: '30px'}); + utils.setStyles(container, { width: '140px' }); + utils.setStyles(children[0], { width: '70px' }); + utils.setStyles(children[3], { width: '30px' }); var grid = new Muuri(container, { layout: { horizontal: false, fillGaps: false, alignRight: false, - alignBottom: false - } + alignBottom: false, + }, }); var items = grid.getItems(); var teardown = function () { @@ -29,33 +27,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 0, top: 0}); - assert.deepEqual(items[1].getPosition(), {left: 0, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 0, top: 140}); + assert.deepEqual(items[0].getPosition(), { left: 0, top: 0 }); + assert.deepEqual(items[1].getPosition(), { left: 0, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 0, top: 140 }); teardown(); - }); QUnit.test('layout: vertical - left/top - fill gaps', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {width: '140px'}); - utils.setStyles(children[0], {width: '70px'}); - utils.setStyles(children[3], {width: '30px'}); + utils.setStyles(container, { width: '140px' }); + utils.setStyles(children[0], { width: '70px' }); + utils.setStyles(children[3], { width: '30px' }); var grid = new Muuri(container, { layout: { horizontal: false, fillGaps: true, alignRight: false, - alignBottom: false - } + alignBottom: false, + }, }); var items = grid.getItems(); var teardown = function () { @@ -63,33 +59,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 0, top: 0}); - assert.deepEqual(items[1].getPosition(), {left: 0, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 90, top: 0}); + assert.deepEqual(items[0].getPosition(), { left: 0, top: 0 }); + assert.deepEqual(items[1].getPosition(), { left: 0, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 90, top: 0 }); teardown(); - }); QUnit.test('layout: vertical - right/top', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {width: '140px'}); - utils.setStyles(children[0], {width: '70px'}); - utils.setStyles(children[3], {width: '30px'}); + utils.setStyles(container, { width: '140px' }); + utils.setStyles(children[0], { width: '70px' }); + utils.setStyles(children[3], { width: '30px' }); var grid = new Muuri(container, { layout: { horizontal: false, fillGaps: false, alignRight: true, - alignBottom: false - } + alignBottom: false, + }, }); var items = grid.getItems(); var teardown = function () { @@ -97,33 +91,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 50, top: 0}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 0, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 90, top: 140}); + assert.deepEqual(items[0].getPosition(), { left: 50, top: 0 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 0, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 90, top: 140 }); teardown(); - }); QUnit.test('layout: vertical - right/top - fill gaps', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {width: '140px'}); - utils.setStyles(children[0], {width: '70px'}); - utils.setStyles(children[3], {width: '30px'}); + utils.setStyles(container, { width: '140px' }); + utils.setStyles(children[0], { width: '70px' }); + utils.setStyles(children[3], { width: '30px' }); var grid = new Muuri(container, { layout: { horizontal: false, fillGaps: true, alignRight: true, - alignBottom: false - } + alignBottom: false, + }, }); var items = grid.getItems(); var teardown = function () { @@ -131,33 +123,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 50, top: 0}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 0, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 0, top: 0}); + assert.deepEqual(items[0].getPosition(), { left: 50, top: 0 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 0, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 0, top: 0 }); teardown(); - }); QUnit.test('layout: vertical - left/bottom', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {width: '140px'}); - utils.setStyles(children[0], {width: '70px'}); - utils.setStyles(children[3], {width: '30px'}); + utils.setStyles(container, { width: '140px' }); + utils.setStyles(children[0], { width: '70px' }); + utils.setStyles(children[3], { width: '30px' }); var grid = new Muuri(container, { layout: { horizontal: false, fillGaps: false, alignRight: false, - alignBottom: true - } + alignBottom: true, + }, }); var items = grid.getItems(); var teardown = function () { @@ -165,33 +155,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 0, top: 140}); - assert.deepEqual(items[1].getPosition(), {left: 0, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 0, top: 0}); + assert.deepEqual(items[0].getPosition(), { left: 0, top: 140 }); + assert.deepEqual(items[1].getPosition(), { left: 0, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 0, top: 0 }); teardown(); - }); QUnit.test('layout: vertical - left/bottom - fill gaps', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {width: '140px'}); - utils.setStyles(children[0], {width: '70px'}); - utils.setStyles(children[3], {width: '30px'}); + utils.setStyles(container, { width: '140px' }); + utils.setStyles(children[0], { width: '70px' }); + utils.setStyles(children[3], { width: '30px' }); var grid = new Muuri(container, { layout: { horizontal: false, fillGaps: true, alignRight: false, - alignBottom: true - } + alignBottom: true, + }, }); var items = grid.getItems(); var teardown = function () { @@ -199,33 +187,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 0, top: 70}); - assert.deepEqual(items[1].getPosition(), {left: 0, top: 0}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 0}); - assert.deepEqual(items[3].getPosition(), {left: 90, top: 70}); + assert.deepEqual(items[0].getPosition(), { left: 0, top: 70 }); + assert.deepEqual(items[1].getPosition(), { left: 0, top: 0 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 0 }); + assert.deepEqual(items[3].getPosition(), { left: 90, top: 70 }); teardown(); - }); QUnit.test('layout: vertical - right/bottom', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {width: '140px'}); - utils.setStyles(children[0], {width: '70px'}); - utils.setStyles(children[3], {width: '30px'}); + utils.setStyles(container, { width: '140px' }); + utils.setStyles(children[0], { width: '70px' }); + utils.setStyles(children[3], { width: '30px' }); var grid = new Muuri(container, { layout: { horizontal: false, fillGaps: false, alignRight: true, - alignBottom: true - } + alignBottom: true, + }, }); var items = grid.getItems(); var teardown = function () { @@ -233,33 +219,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 50, top: 140}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 0, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 90, top: 0}); + assert.deepEqual(items[0].getPosition(), { left: 50, top: 140 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 0, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 90, top: 0 }); teardown(); - }); QUnit.test('layout: vertical - right/bottom - fill gaps', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {width: '140px'}); - utils.setStyles(children[0], {width: '70px'}); - utils.setStyles(children[3], {width: '30px'}); + utils.setStyles(container, { width: '140px' }); + utils.setStyles(children[0], { width: '70px' }); + utils.setStyles(children[3], { width: '30px' }); var grid = new Muuri(container, { layout: { horizontal: false, fillGaps: true, alignRight: true, - alignBottom: true - } + alignBottom: true, + }, }); var items = grid.getItems(); var teardown = function () { @@ -267,33 +251,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 50, top: 70}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 0}); - assert.deepEqual(items[2].getPosition(), {left: 0, top: 0}); - assert.deepEqual(items[3].getPosition(), {left: 0, top: 70}); + assert.deepEqual(items[0].getPosition(), { left: 50, top: 70 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 0 }); + assert.deepEqual(items[2].getPosition(), { left: 0, top: 0 }); + assert.deepEqual(items[3].getPosition(), { left: 0, top: 70 }); teardown(); - }); QUnit.test('layout: horizontal - left/top', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {height: '140px'}); - utils.setStyles(children[0], {height: '70px'}); - utils.setStyles(children[3], {height: '30px'}); + utils.setStyles(container, { height: '140px' }); + utils.setStyles(children[0], { height: '70px' }); + utils.setStyles(children[3], { height: '30px' }); var grid = new Muuri(container, { layout: { horizontal: true, fillGaps: false, alignRight: false, - alignBottom: false - } + alignBottom: false, + }, }); var items = grid.getItems(); var teardown = function () { @@ -301,33 +283,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 0, top: 0}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 0}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 140, top: 0}); + assert.deepEqual(items[0].getPosition(), { left: 0, top: 0 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 0 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 140, top: 0 }); teardown(); - }); QUnit.test('layout: horizontal - left/top - fill gaps', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {height: '140px'}); - utils.setStyles(children[0], {height: '70px'}); - utils.setStyles(children[3], {height: '30px'}); + utils.setStyles(container, { height: '140px' }); + utils.setStyles(children[0], { height: '70px' }); + utils.setStyles(children[3], { height: '30px' }); var grid = new Muuri(container, { layout: { horizontal: true, fillGaps: true, alignRight: false, - alignBottom: false - } + alignBottom: false, + }, }); var items = grid.getItems(); var teardown = function () { @@ -335,33 +315,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 0, top: 0}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 0}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 0, top: 90}); + assert.deepEqual(items[0].getPosition(), { left: 0, top: 0 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 0 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 0, top: 90 }); teardown(); - }); QUnit.test('layout: horizontal - right/top', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {height: '140px'}); - utils.setStyles(children[0], {height: '70px'}); - utils.setStyles(children[3], {height: '30px'}); + utils.setStyles(container, { height: '140px' }); + utils.setStyles(children[0], { height: '70px' }); + utils.setStyles(children[3], { height: '30px' }); var grid = new Muuri(container, { layout: { horizontal: true, fillGaps: false, alignRight: true, - alignBottom: false - } + alignBottom: false, + }, }); var items = grid.getItems(); var teardown = function () { @@ -369,33 +347,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 140, top: 0}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 0}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 0, top: 0}); + assert.deepEqual(items[0].getPosition(), { left: 140, top: 0 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 0 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 0, top: 0 }); teardown(); - }); QUnit.test('layout: horizontal - right/top - fill gaps', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {height: '140px'}); - utils.setStyles(children[0], {height: '70px'}); - utils.setStyles(children[3], {height: '30px'}); + utils.setStyles(container, { height: '140px' }); + utils.setStyles(children[0], { height: '70px' }); + utils.setStyles(children[3], { height: '30px' }); var grid = new Muuri(container, { layout: { horizontal: true, fillGaps: true, alignRight: true, - alignBottom: false - } + alignBottom: false, + }, }); var items = grid.getItems(); var teardown = function () { @@ -403,33 +379,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 70, top: 0}); - assert.deepEqual(items[1].getPosition(), {left: 0, top: 0}); - assert.deepEqual(items[2].getPosition(), {left: 0, top: 70}); - assert.deepEqual(items[3].getPosition(), {left: 70, top: 90}); + assert.deepEqual(items[0].getPosition(), { left: 70, top: 0 }); + assert.deepEqual(items[1].getPosition(), { left: 0, top: 0 }); + assert.deepEqual(items[2].getPosition(), { left: 0, top: 70 }); + assert.deepEqual(items[3].getPosition(), { left: 70, top: 90 }); teardown(); - }); QUnit.test('layout: horizontal - left/bottom', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {height: '140px'}); - utils.setStyles(children[0], {height: '70px'}); - utils.setStyles(children[3], {height: '30px'}); + utils.setStyles(container, { height: '140px' }); + utils.setStyles(children[0], { height: '70px' }); + utils.setStyles(children[3], { height: '30px' }); var grid = new Muuri(container, { layout: { horizontal: true, fillGaps: false, alignRight: false, - alignBottom: true - } + alignBottom: true, + }, }); var items = grid.getItems(); var teardown = function () { @@ -437,33 +411,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 0, top: 50}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 0}); - assert.deepEqual(items[3].getPosition(), {left: 140, top: 90}); + assert.deepEqual(items[0].getPosition(), { left: 0, top: 50 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 0 }); + assert.deepEqual(items[3].getPosition(), { left: 140, top: 90 }); teardown(); - }); QUnit.test('layout: horizontal - left/bottom - fill gaps', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {height: '140px'}); - utils.setStyles(children[0], {height: '70px'}); - utils.setStyles(children[3], {height: '30px'}); + utils.setStyles(container, { height: '140px' }); + utils.setStyles(children[0], { height: '70px' }); + utils.setStyles(children[3], { height: '30px' }); var grid = new Muuri(container, { layout: { horizontal: true, fillGaps: true, alignRight: false, - alignBottom: true - } + alignBottom: true, + }, }); var items = grid.getItems(); var teardown = function () { @@ -471,33 +443,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 0, top: 50}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 0}); - assert.deepEqual(items[3].getPosition(), {left: 0, top: 0}); + assert.deepEqual(items[0].getPosition(), { left: 0, top: 50 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 0 }); + assert.deepEqual(items[3].getPosition(), { left: 0, top: 0 }); teardown(); - }); QUnit.test('layout: horizontal - right/bottom', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {height: '140px'}); - utils.setStyles(children[0], {height: '70px'}); - utils.setStyles(children[3], {height: '30px'}); + utils.setStyles(container, { height: '140px' }); + utils.setStyles(children[0], { height: '70px' }); + utils.setStyles(children[3], { height: '30px' }); var grid = new Muuri(container, { layout: { horizontal: true, fillGaps: false, alignRight: true, - alignBottom: true - } + alignBottom: true, + }, }); var items = grid.getItems(); var teardown = function () { @@ -505,33 +475,31 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 140, top: 50}); - assert.deepEqual(items[1].getPosition(), {left: 70, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 70, top: 0}); - assert.deepEqual(items[3].getPosition(), {left: 0, top: 90}); + assert.deepEqual(items[0].getPosition(), { left: 140, top: 50 }); + assert.deepEqual(items[1].getPosition(), { left: 70, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 70, top: 0 }); + assert.deepEqual(items[3].getPosition(), { left: 0, top: 90 }); teardown(); - }); QUnit.test('layout: horizontal - right/bottom - fill gaps', function (assert) { - assert.expect(4); - var container = utils.createGridElements({itemCount: 4}); + var container = utils.createGridElements({ itemCount: 4 }); var children = [].slice.call(container.children); - utils.setStyles(container, {height: '140px'}); - utils.setStyles(children[0], {height: '70px'}); - utils.setStyles(children[3], {height: '30px'}); + utils.setStyles(container, { height: '140px' }); + utils.setStyles(children[0], { height: '70px' }); + utils.setStyles(children[3], { height: '30px' }); var grid = new Muuri(container, { layout: { horizontal: true, fillGaps: true, alignRight: true, - alignBottom: true - } + alignBottom: true, + }, }); var items = grid.getItems(); var teardown = function () { @@ -539,17 +507,15 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(items[0].getPosition(), {left: 70, top: 50}); - assert.deepEqual(items[1].getPosition(), {left: 0, top: 70}); - assert.deepEqual(items[2].getPosition(), {left: 0, top: 0}); - assert.deepEqual(items[3].getPosition(), {left: 70, top: 0}); + assert.deepEqual(items[0].getPosition(), { left: 70, top: 50 }); + assert.deepEqual(items[1].getPosition(), { left: 0, top: 70 }); + assert.deepEqual(items[2].getPosition(), { left: 0, top: 0 }); + assert.deepEqual(items[3].getPosition(), { left: 70, top: 0 }); teardown(); - }); - QUnit.test('layout: percentage widths with no rounding', function (assert) { - + QUnit.test('layout: percentage widths', function (assert) { assert.expect(33); for (var i = 1; i < 34; i++) { @@ -557,14 +523,14 @@ itemCount: i, itemStyles: { position: 'absolute', - width: (100 / i) + '%', + width: 100 / i + '%', height: '50px', background: '#000', border: '1px solid #ff0000', - boxSizing: 'border-box' - } + boxSizing: 'border-box', + }, }); - var grid = new Muuri(container, {layout: {rounding: false}}); + var grid = new Muuri(container); var hasIncorrectPosition = grid.getItems().some(function (item) { return item.getPosition().top !== 0; }); @@ -576,5 +542,4 @@ teardown(); } }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/showDuration.js b/tests/grid-options/showDuration.js index 2a58adc4..f1856498 100644 --- a/tests/grid-options/showDuration.js +++ b/tests/grid-options/showDuration.js @@ -1,16 +1,14 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); QUnit.test('showDuration: should disable show animation when set to 0', function (assert) { - assert.expect(2); var container = utils.createGridElements(); var grid = new Muuri(container, { - showDuration: 0 + showDuration: 0, }); var item = grid.getItems()[0]; var teardown = function () { @@ -18,11 +16,9 @@ container.parentNode.removeChild(container); }; - grid.hide(item, {instant: true}).show(item); + grid.hide([item], { instant: true }).show([item]); assert.strictEqual(item.isVisible(), true, 'item should be visible'); assert.strictEqual(item.isShowing(), false, 'item should not be showing'); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/grid-options/visibleStyles-hiddenStyles.js b/tests/grid-options/visibleStyles-hiddenStyles.js index fb1da520..caa732ae 100644 --- a/tests/grid-options/visibleStyles-hiddenStyles.js +++ b/tests/grid-options/visibleStyles-hiddenStyles.js @@ -1,38 +1,42 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Grid options'); - QUnit.test('visibleStyles/hiddenStyles: should change the visible/hidden state styles of items', function (assert) { - - assert.expect(2); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container, { - visibleStyles: { - fontSize: '30px' - }, - hiddenStyles: { - fontSize: '10px' - } - }); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - grid.hide(item, {onFinish: function () { - assert.strictEqual(item._child.style.fontSize, '10px', 'item has correct hidden styles'); - grid.show(item, {onFinish: function () { - assert.strictEqual(item._child.style.fontSize, '30px', 'item has correct visible styles'); - teardown(); - }}); - }}) - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'visibleStyles/hiddenStyles: should change the visible/hidden state styles of items', + function (assert) { + assert.expect(2); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container, { + visibleStyles: { + fontSize: '30px', + }, + hiddenStyles: { + fontSize: '10px', + }, + }); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + grid.hide([item], { + onFinish: function () { + var child = item.getElement().children[0]; + assert.strictEqual(child.style.fontSize, '10px', 'item has correct hidden styles'); + grid.show([item], { + onFinish: function () { + assert.strictEqual(child.style.fontSize, '30px', 'item has correct visible styles'); + teardown(); + }, + }); + }, + }); + } + ); +})(this); diff --git a/tests/index.js b/tests/index.js index 5a0c906d..3810e49a 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1 +1,5 @@ -QUnit.config.reorder = false; \ No newline at end of file +QUnit.config.reorder = false; + +Muuri.syncPacker = new Muuri.Packer(); +Muuri.asyncPacker = Muuri.defaultPacker; +Muuri.defaultPacker = Muuri.syncPacker; diff --git a/tests/item-methods/getElement.js b/tests/item-methods/getElement.js index f60b2368..0f7e68a1 100644 --- a/tests/item-methods/getElement.js +++ b/tests/item-methods/getElement.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); QUnit.test('getElement: should return the instance`s associated DOM element', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -19,7 +17,5 @@ assert.strictEqual(item.getElement(), itemElement); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/item-methods/getGrid.js b/tests/item-methods/getGrid.js index e34da561..da842617 100644 --- a/tests/item-methods/getGrid.js +++ b/tests/item-methods/getGrid.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); QUnit.test('getGrid: should return the instance`s associated Grid instance', function (assert) { - assert.expect(1); var container = utils.createGridElements(); @@ -18,7 +16,5 @@ assert.strictEqual(item.getGrid(), grid); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/item-methods/getHeight.js b/tests/item-methods/getHeight.js index 40f54560..517d0bd7 100644 --- a/tests/item-methods/getHeight.js +++ b/tests/item-methods/getHeight.js @@ -1,26 +1,33 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); - QUnit.test('getHeight: should return the instance element`s cached height that includes paddings and borders', function (assert) { - - assert.expect(2); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - assert.strictEqual(item.getHeight(), 50, 'The returned height is equal to the element`s content height + top/bottom paddings + top/bottom borders size'); - item.getElement().style.padding = '0px'; - assert.strictEqual(item.getHeight(), 50, 'The returned height is the cached height and not the element`s current height in DOM'); - teardown(); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'getHeight: should return the instance element`s cached height that includes paddings and borders', + function (assert) { + assert.expect(2); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + assert.strictEqual( + item.getHeight(), + 50, + 'The returned height is equal to the element`s content height + top/bottom paddings + top/bottom borders size' + ); + item.getElement().style.padding = '0px'; + assert.strictEqual( + item.getHeight(), + 50, + 'The returned height is the cached height and not the element`s current height in DOM' + ); + teardown(); + } + ); +})(this); diff --git a/tests/item-methods/getMargin.js b/tests/item-methods/getMargin.js index be0a35f4..179879b5 100644 --- a/tests/item-methods/getMargin.js +++ b/tests/item-methods/getMargin.js @@ -1,11 +1,9 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); QUnit.test('getMargin: should return the instance element`s cached margins', function (assert) { - assert.expect(2); var container = utils.createGridElements(); @@ -16,11 +14,17 @@ container.parentNode.removeChild(container); }; - assert.deepEqual(item.getMargin(), {left: 10, right: 10, top: 10, bottom: 10}, 'The margins should be retrieved from the DOM on init'); + assert.deepEqual( + item.getMargin(), + { left: 10, right: 10, top: 10, bottom: 10 }, + 'The margins should be retrieved from the DOM on init' + ); item.getElement().style.margin = '0px'; - assert.deepEqual(item.getMargin(), {left: 10, right: 10, top: 10, bottom: 10}, 'The returned margins are cached and not necessarilly the element`s current margins in DOM'); + assert.deepEqual( + item.getMargin(), + { left: 10, right: 10, top: 10, bottom: 10 }, + 'The returned margins are cached and not necessarilly the element`s current margins in DOM' + ); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/item-methods/getPosition.js b/tests/item-methods/getPosition.js index 61b0d738..73829dd4 100644 --- a/tests/item-methods/getPosition.js +++ b/tests/item-methods/getPosition.js @@ -1,36 +1,35 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); - QUnit.test('getPosition: should return the instance element`s cached position in the grid', function (assert) { - - assert.expect(4); - - var container = utils.createGridElements({ - containerStyles: { - position: 'relative', - width: '140px' - } - }); - var grid = new Muuri(container); - var items = grid.getItems(); - var itemA = items[0]; - var itemB = items[1]; - var itemC = items[2]; - var itemD = items[3]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - assert.deepEqual(itemA.getPosition(), {left: 0, top: 0}); - assert.deepEqual(itemB.getPosition(), {left: 70, top: 0}); - assert.deepEqual(itemC.getPosition(), {left: 0, top: 70}); - assert.deepEqual(itemD.getPosition(), {left: 70, top: 70}); - teardown(); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'getPosition: should return the instance element`s cached position in the grid', + function (assert) { + assert.expect(4); + + var container = utils.createGridElements({ + containerStyles: { + position: 'relative', + width: '140px', + }, + }); + var grid = new Muuri(container); + var items = grid.getItems(); + var itemA = items[0]; + var itemB = items[1]; + var itemC = items[2]; + var itemD = items[3]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + assert.deepEqual(itemA.getPosition(), { left: 0, top: 0 }); + assert.deepEqual(itemB.getPosition(), { left: 70, top: 0 }); + assert.deepEqual(itemC.getPosition(), { left: 0, top: 70 }); + assert.deepEqual(itemD.getPosition(), { left: 70, top: 70 }); + teardown(); + } + ); +})(this); diff --git a/tests/item-methods/getWidth.js b/tests/item-methods/getWidth.js index 9442ca23..f0528f03 100644 --- a/tests/item-methods/getWidth.js +++ b/tests/item-methods/getWidth.js @@ -1,26 +1,33 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); - QUnit.test('getWidth: should return the instance element`s cached width that includes paddings and borders', function (assert) { - - assert.expect(2); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - assert.strictEqual(item.getWidth(), 50, 'The returned width is equal to the element`s content width + left/right paddings + left/right borders size'); - item.getElement().style.padding = '0px'; - assert.strictEqual(item.getWidth(), 50, 'The returned width is the cached width and not the element`s current width in DOM'); - teardown(); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'getWidth: should return the instance element`s cached width that includes paddings and borders', + function (assert) { + assert.expect(2); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + assert.strictEqual( + item.getWidth(), + 50, + 'The returned width is equal to the element`s content width + left/right paddings + left/right borders size' + ); + item.getElement().style.padding = '0px'; + assert.strictEqual( + item.getWidth(), + 50, + 'The returned width is the cached width and not the element`s current width in DOM' + ); + teardown(); + } + ); +})(this); diff --git a/tests/item-methods/isActive.js b/tests/item-methods/isActive.js index d8458e4e..c1287575 100644 --- a/tests/item-methods/isActive.js +++ b/tests/item-methods/isActive.js @@ -1,11 +1,11 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); - QUnit.test('isActive: should return true if the item is active and otherwise false', function (assert) { - + QUnit.test('isActive: should return true if the item is active and otherwise false', function ( + assert + ) { assert.expect(2); var container = utils.createGridElements(); @@ -16,11 +16,13 @@ container.parentNode.removeChild(container); }; - assert.strictEqual(item.isActive(), true, 'An item should be active when the it`s initiated and it`s display value is set to block'); - grid.hide(item); + assert.strictEqual( + item.isActive(), + true, + 'An item should be active when the it`s initiated and it`s display value is set to block' + ); + grid.hide([item]); assert.strictEqual(item.isActive(), false, 'An item should not be active after hide is called'); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/item-methods/isDestroyed.js b/tests/item-methods/isDestroyed.js index a9642791..5c08dbc9 100644 --- a/tests/item-methods/isDestroyed.js +++ b/tests/item-methods/isDestroyed.js @@ -1,26 +1,33 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); - QUnit.test('isDestroyed: should return true if the item is destroyed and otherwise false', function (assert) { - - assert.expect(2); - - var container = utils.createGridElements(); - var grid = new Muuri(container); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - }; - - assert.strictEqual(item.isDestroyed(), false, 'An item should not be destroyed before it is removed from the grid'); - grid.remove(item); - assert.strictEqual(item.isDestroyed(), true, 'An item should be destroyed after it is removed from the grid'); - teardown(); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'isDestroyed: should return true if the item is destroyed and otherwise false', + function (assert) { + assert.expect(2); + + var container = utils.createGridElements(); + var grid = new Muuri(container); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + }; + + assert.strictEqual( + item.isDestroyed(), + false, + 'An item should not be destroyed before it is removed from the grid' + ); + grid.remove([item]); + assert.strictEqual( + item.isDestroyed(), + true, + 'An item should be destroyed after it is removed from the grid' + ); + teardown(); + } + ); +})(this); diff --git a/tests/item-methods/isDragging.js b/tests/item-methods/isDragging.js index f461533b..3c2d4398 100644 --- a/tests/item-methods/isDragging.js +++ b/tests/item-methods/isDragging.js @@ -1,16 +1,14 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); QUnit.test('isDragging: should return true if the item is being dragged', function (assert) { - assert.expect(4); var done = assert.async(); var container = utils.createGridElements(); - var grid = new Muuri(container, {dragEnabled: true}); + var grid = new Muuri(container, { dragEnabled: true }); var item = grid.getItems()[0]; var teardown = function () { grid.destroy(); @@ -20,28 +18,44 @@ function onDragStart() { grid.off('dragStart', onDragStart); - assert.strictEqual(item.isDragging(), true, 'An item should be in dragging state when dragging starts'); + assert.strictEqual( + item.isDragging(), + true, + 'An item should be in dragging state when dragging starts' + ); } function onDragMove() { grid.off('dragMove', onDragMove); - assert.strictEqual(item.isDragging(), true, 'An item should be in dragging state when dragging'); + assert.strictEqual( + item.isDragging(), + true, + 'An item should be in dragging state when dragging' + ); } function onDragEnd() { grid.off('dragEnd', onDragEnd); - assert.strictEqual(item.isDragging(), false, 'An item should not be in dragging state after dragging has ended'); + assert.strictEqual( + item.isDragging(), + false, + 'An item should not be in dragging state after dragging has ended' + ); } - grid - .on('dragStart', onDragStart) - .on('dragMove', onDragMove) - .on('dragEnd', onDragEnd); + grid.on('dragStart', onDragStart).on('dragMove', onDragMove).on('dragEnd', onDragEnd); - assert.strictEqual(item.isDragging(), false, 'An item should not be in dragging state when it`s not being dragged'); - - utils.dragElement(item.getElement(), 100, 100, teardown); + assert.strictEqual( + item.isDragging(), + false, + 'An item should not be in dragging state when it`s not being dragged' + ); + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + onFinished: teardown, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/item-methods/isHiding.js b/tests/item-methods/isHiding.js index d6de402f..44eae4da 100644 --- a/tests/item-methods/isHiding.js +++ b/tests/item-methods/isHiding.js @@ -1,32 +1,49 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); - QUnit.test('isHiding: should return true if the item is animating to hidden and otherwise false', function (assert) { - - assert.expect(4); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - assert.strictEqual(item.isHiding(), false, 'An item should not be in hiding state when the it`s visible'); - grid.hide(item, {onFinish: function () { - assert.strictEqual(item.isHiding(), false, 'An item should not be in hiding state after it has finished the hide animation'); - grid.show(item); - assert.strictEqual(item.isHiding(), false, 'An item should not be in hiding state when it`s being animated to visible'); - teardown(); - }}); - assert.strictEqual(item.isHiding(), true, 'An item should be in hiding state when the it`s being animated to hidden'); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'isHiding: should return true if the item is animating to hidden and otherwise false', + function (assert) { + assert.expect(4); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + assert.strictEqual( + item.isHiding(), + false, + 'An item should not be in hiding state when the it`s visible' + ); + grid.hide([item], { + onFinish: function () { + assert.strictEqual( + item.isHiding(), + false, + 'An item should not be in hiding state after it has finished the hide animation' + ); + grid.show([item]); + assert.strictEqual( + item.isHiding(), + false, + 'An item should not be in hiding state when it`s being animated to visible' + ); + teardown(); + }, + }); + assert.strictEqual( + item.isHiding(), + true, + 'An item should be in hiding state when the it`s being animated to hidden' + ); + } + ); +})(this); diff --git a/tests/item-methods/isPositioning.js b/tests/item-methods/isPositioning.js index c5d7eff9..f7c53ea9 100644 --- a/tests/item-methods/isPositioning.js +++ b/tests/item-methods/isPositioning.js @@ -1,34 +1,45 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); - QUnit.test('isPositioning: should return true if the item`s position is being animated', function (assert) { - - assert.expect(3); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - assert.strictEqual(item.isPositioning(), false, 'An item should not be in positioning state when it`s position is not being animated'); - - grid.move(item, -1, { - layout: function () { - assert.strictEqual(item.isPositioning(), false, 'An item should not be in positioning state after the positioning animation is finished'); - teardown(); - } - }); - - assert.strictEqual(item.isPositioning(), true, 'An item should be in positioning state when it`s position is being animated'); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'isPositioning: should return true if the item`s position is being animated', + function (assert) { + assert.expect(3); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + assert.strictEqual( + item.isPositioning(), + false, + 'An item should not be in positioning state when it`s position is not being animated' + ); + + grid.move(item, -1, { + layout: function () { + assert.strictEqual( + item.isPositioning(), + false, + 'An item should not be in positioning state after the positioning animation is finished' + ); + teardown(); + }, + }); + + assert.strictEqual( + item.isPositioning(), + true, + 'An item should be in positioning state when it`s position is being animated' + ); + } + ); +})(this); diff --git a/tests/item-methods/isReleasing.js b/tests/item-methods/isReleasing.js index e68d346b..5f2e877d 100644 --- a/tests/item-methods/isReleasing.js +++ b/tests/item-methods/isReleasing.js @@ -1,16 +1,14 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); QUnit.test('isReleasing: should return true if the item is being released', function (assert) { - assert.expect(6); var done = assert.async(); var container = utils.createGridElements(); - var grid = new Muuri(container, {dragEnabled: true}); + var grid = new Muuri(container, { dragEnabled: true }); var item = grid.getItems()[0]; var teardown = function () { grid.destroy(); @@ -20,41 +18,67 @@ function onDragStart() { grid.off('dragStart', onDragStart); - assert.strictEqual(item.isReleasing(), false, 'An item should not be in releasing state when dragging starts'); + assert.strictEqual( + item.isReleasing(), + false, + 'An item should not be in releasing state when dragging starts' + ); } function onDragMove() { grid.off('dragMove', onDragMove); - assert.strictEqual(item.isReleasing(), false, 'An item should not be in releasing state when dragging'); + assert.strictEqual( + item.isReleasing(), + false, + 'An item should not be in releasing state when dragging' + ); } function onDragEnd() { grid.off('dragEnd', onDragEnd); - assert.strictEqual(item.isReleasing(), false, 'An item should not be in releasing state when drag ends'); + assert.strictEqual( + item.isReleasing(), + false, + 'An item should not be in releasing state when drag ends' + ); } function onDragReleaseStart() { grid.off('dragReleaseStart', onDragReleaseStart); - assert.strictEqual(item.isReleasing(), true, 'An item should be in releasing state right after it has been released'); + assert.strictEqual( + item.isReleasing(), + true, + 'An item should be in releasing state right after it has been released' + ); } function onDragReleaseEnd() { grid.off('dragReleaseEnd', onDragReleaseEnd); - assert.strictEqual(item.isReleasing(), false, 'An item should not be in releasing state right after releasing has ended'); + assert.strictEqual( + item.isReleasing(), + false, + 'An item should not be in releasing state right after releasing has ended' + ); teardown(); } grid - .on('dragStart', onDragStart) - .on('dragMove', onDragMove) - .on('dragEnd', onDragEnd) - .on('dragReleaseStart', onDragReleaseStart) - .on('dragReleaseEnd', onDragReleaseEnd); + .on('dragStart', onDragStart) + .on('dragMove', onDragMove) + .on('dragEnd', onDragEnd) + .on('dragReleaseStart', onDragReleaseStart) + .on('dragReleaseEnd', onDragReleaseEnd); - assert.strictEqual(item.isReleasing(), false, 'An item should not be in releasing state when it`s not being released'); - - utils.dragElement(item.getElement(), 100, 100); + assert.strictEqual( + item.isReleasing(), + false, + 'An item should not be in releasing state when it`s not being released' + ); + utils.dragElement({ + element: item.getElement(), + x: 100, + y: 100, + }); }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/item-methods/isShowing.js b/tests/item-methods/isShowing.js index 854d0494..e78b38e0 100644 --- a/tests/item-methods/isShowing.js +++ b/tests/item-methods/isShowing.js @@ -1,34 +1,57 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); - QUnit.test('isShowing: should return true if the item is animating to visible and otherwise false', function (assert) { - - assert.expect(5); - - var done = assert.async(); - var container = utils.createGridElements(); - var grid = new Muuri(container); - var item = grid.getItems()[0]; - var teardown = function () { - grid.destroy(); - container.parentNode.removeChild(container); - done(); - }; - - assert.strictEqual(item.isShowing(), false, 'An item should not be in showing state when the it`s visible'); - grid.hide(item, {onFinish: function () { - assert.strictEqual(item.isShowing(), false, 'An item should not be in showing state when the it`s hidden'); - grid.show(item, {onFinish: function () { - assert.strictEqual(item.isShowing(), false, 'An item should not be in showing state after it has finished the show animation'); - teardown(); - }}); - assert.strictEqual(item.isShowing(), true, 'An item should be in showing state when it`s being animated to visible'); - }}); - assert.strictEqual(item.isShowing(), false, 'An item should not be in showing state when the it`s being animated to hidden'); - - }); - -})(this); \ No newline at end of file + QUnit.test( + 'isShowing: should return true if the item is animating to visible and otherwise false', + function (assert) { + assert.expect(5); + + var done = assert.async(); + var container = utils.createGridElements(); + var grid = new Muuri(container); + var item = grid.getItems()[0]; + var teardown = function () { + grid.destroy(); + container.parentNode.removeChild(container); + done(); + }; + + assert.strictEqual( + item.isShowing(), + false, + 'An item should not be in showing state when the it`s visible' + ); + grid.hide([item], { + onFinish: function () { + assert.strictEqual( + item.isShowing(), + false, + 'An item should not be in showing state when the it`s hidden' + ); + grid.show([item], { + onFinish: function () { + assert.strictEqual( + item.isShowing(), + false, + 'An item should not be in showing state after it has finished the show animation' + ); + teardown(); + }, + }); + assert.strictEqual( + item.isShowing(), + true, + 'An item should be in showing state when it`s being animated to visible' + ); + }, + }); + assert.strictEqual( + item.isShowing(), + false, + 'An item should not be in showing state when the it`s being animated to hidden' + ); + } + ); +})(this); diff --git a/tests/item-methods/isVisible.js b/tests/item-methods/isVisible.js index cfc996ac..7c534ad5 100644 --- a/tests/item-methods/isVisible.js +++ b/tests/item-methods/isVisible.js @@ -1,11 +1,11 @@ (function (window) { - var Muuri = window.Muuri; QUnit.module('Item methods'); - QUnit.test('isVisible: should return true if the item is visible and otherwise false', function (assert) { - + QUnit.test('isVisible: should return true if the item is visible and otherwise false', function ( + assert + ) { assert.expect(2); var container = utils.createGridElements(); @@ -16,11 +16,17 @@ container.parentNode.removeChild(container); }; - assert.strictEqual(item.isVisible(), true, 'An item should be visible when the it`s initiated and it`s display value is set to block'); - grid.hide(item); - assert.strictEqual(item.isVisible(), false, 'An item should not be visible after hide is called'); + assert.strictEqual( + item.isVisible(), + true, + 'An item should be visible when the it`s initiated and it`s display value is set to block' + ); + grid.hide([item]); + assert.strictEqual( + item.isVisible(), + false, + 'An item should not be visible after hide is called' + ); teardown(); - }); - -})(this); \ No newline at end of file +})(this); diff --git a/tests/utils.js b/tests/utils.js index de069ac8..d28c5cf4 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,6 +1,5 @@ -(function (window) { - - var utils = window.utils = {}; +(function(window) { + var utils = (window.utils = {}); var supportsTouch = !!('TouchEvent' in window); var supportsPointer = !!('PointerEvent' in window); @@ -8,8 +7,7 @@ // Methods // - utils.createGridElements = function (options) { - + utils.createGridElements = function(options) { var opts = options || {}; var container = opts.container || document.createElement('div'); var itemCount = typeof opts.itemCount === 'number' && opts.itemCount >= 0 ? opts.itemCount : 10; @@ -44,10 +42,16 @@ } return container; - }; - utils.dragElement = function(element, moveLeft, moveTop, onStop) { + utils.dragElement = function(config) { + var element = config.element; + var moveLeft = typeof config.x === 'number' ? config.x : 0; + var moveTop = typeof config.y === 'number' ? config.y : 0; + var pressDuration = typeof config.pressDuration === 'number' ? config.pressDuration : 100; + var moveDuration = typeof config.moveDuration === 'number' ? config.moveDuration : 100; + var holdDuration = typeof config.holdDuration === 'number' ? config.holdDuration : 200; + var onFinished = config.onFinished; // Calculate start point. var from = mezr.offset(element, window); @@ -57,9 +61,9 @@ // Create the hand istance. var hand = new Hand({ timing: 'fastFrame', - onStop: function () { - if (typeof onStop === 'function') { - window.setTimeout(onStop, 100); + onStop: function() { + if (typeof onFinished === 'function') { + window.setTimeout(onFinished, 100); } } }); @@ -76,104 +80,120 @@ // Do the drag if movement is defined. if (moveTop || moveLeft) { - finger.down().wait(100).moveTo(from.left + moveLeft, from.top + moveTop, 100).wait(200).up(); + finger + .down() + .wait(pressDuration) + .moveTo(from.left + moveLeft, from.top + moveTop, moveDuration) + .wait(holdDuration) + .up(); } // Otherwise do a press. else { - finger.down().wait(400).up(); + finger + .down() + .wait(pressDuration + holdDuration) + .up(); } - }; - utils.idList = function (collection) { - return collection.map(function (item) { + utils.idList = function(collection) { + return collection.map(function(item) { return item._id; }); }; - utils.sortedIdList = function (items) { - return utils.idList(items.sort(function (a, b) { - return a._id - b._id; - })); + utils.sortedIdList = function(items) { + return utils.idList( + items.sort(function(a, b) { + return a._id - b._id; + }) + ); }; - utils.getActiveItems = function (grid) { - return grid.getItems().filter(function (item) { + utils.getActiveItems = function(grid) { + return grid.getItems().filter(function(item) { return item.isActive(); }); }; - utils.getInactiveItems = function (grid) { - return grid.getItems().filter(function (item) { + utils.getInactiveItems = function(grid) { + return grid.getItems().filter(function(item) { return !item.isActive(); }); }; - utils.getVisibleItems = function (grid) { - return grid.getItems().filter(function (item) { + utils.getVisibleItems = function(grid) { + return grid.getItems().filter(function(item) { return item.isVisible(); }); }; - utils.getHiddenItems = function (grid) { - return grid.getItems().filter(function (item) { + utils.getHiddenItems = function(grid) { + return grid.getItems().filter(function(item) { return !item.isVisible(); }); }; - utils.getShowingItems = function (grid) { - return grid.getItems().filter(function (item) { + utils.getShowingItems = function(grid) { + return grid.getItems().filter(function(item) { return item.isShowing(); }); }; - utils.getHidingItems = function (grid) { - return grid.getItems().filter(function (item) { + utils.getHidingItems = function(grid) { + return grid.getItems().filter(function(item) { return item.isHiding(); }); }; - utils.getPositioningItems = function (grid) { - return grid.getItems().filter(function (item) { + utils.getPositioningItems = function(grid) { + return grid.getItems().filter(function(item) { return item.isPositioning(); }); }; - utils.getReleasingItems = function (grid) { - return grid.getItems().filter(function (item) { + utils.getReleasingItems = function(grid) { + return grid.getItems().filter(function(item) { return item.isReleasing(); }); }; - utils.setStyles = function (element, styles) { + utils.setStyles = function(element, styles) { var props = Object.keys(styles); for (var i = 0; i < props.length; i++) { element.style[props[i]] = styles[props[i]]; } }; - utils.matches = function (el, selector) { + utils.matches = function(el, selector) { var p = Element.prototype; - return (p.matches || p.matchesSelector || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || p.oMatchesSelector).call(el, selector); - }; - - utils.raf = function (cb) { - return (window.requestAnimationFrame - || window.webkitRequestAnimationFrame - || window.mozRequestAnimationFrame - || window.msRequestAnimationFrame - || function (cb) { + return ( + p.matches || + p.matchesSelector || + p.webkitMatchesSelector || + p.mozMatchesSelector || + p.msMatchesSelector || + p.oMatchesSelector + ).call(el, selector); + }; + + utils.raf = function(cb) { + return ( + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.msRequestAnimationFrame || + function(cb) { return window.setTimeout(cb, 16); } )(cb); }; - utils.isScrollEvent = function (e) { + utils.isScrollEvent = function(e) { return e.type === 'scroll'; }; - utils.isDraggerEvent = function (e) { - + utils.isDraggerEvent = function(e) { var ret = true; var eventKeys = Object.keys(e); var requiredKeys = [ @@ -195,14 +215,12 @@ 'target' ]; - requiredKeys.forEach(function (key) { + requiredKeys.forEach(function(key) { if (eventKeys.indexOf(key) === -1) { ret = false; } }); return ret; - }; - -})(this); \ No newline at end of file +})(this);