diff --git a/bower.json b/bower.json deleted file mode 100644 index 4e8134c..0000000 --- a/bower.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "sortable-list", - "authors": [ - "Pedro Garcia " - ], - "main": "sortable-list.html", - "dependencies": { - "polymer": "Polymer/polymer#^2.0.0" - }, - "devDependencies": { - "web-component-tester": "v6.0.0" - } -} diff --git a/demo/index.html b/demo/index.html index c30b1bd..24434a5 100644 --- a/demo/index.html +++ b/demo/index.html @@ -6,8 +6,8 @@ sortable-list demo - - + + - -
- -
- - - - - diff --git a/sortable-list.js b/sortable-list.js new file mode 100644 index 0000000..7f9006a --- /dev/null +++ b/sortable-list.js @@ -0,0 +1,349 @@ +/** +`sortable-list` + + +@demo demo/index.html +*/ +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import { GestureEventListeners } from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { idlePeriod } from '@polymer/polymer/lib/utils/async.js'; +import { addListener, removeListener } from '@polymer/polymer/lib/utils/gestures.js'; + +class SortableList extends GestureEventListeners(PolymerElement) { + static get template() { + return html` + + +
+ +
+`; + } + + static get is() {return 'sortable-list';} + + static get properties() { + return { + + /** + * This is a CSS selector string. If this is set, only items that + * match the CSS selector are sortable. + */ + sortable: String, + + /** + * The list of sortable items. + */ + items: { + type: Array, + notify: true, + readOnly: true + }, + + /** + * Returns true when an item is being drag. + */ + dragging: { + type: Boolean, + notify: true, + readOnly: true, + reflectToAttribute: true, + value: false + }, + + /** + * Disables the draggable if set to true. + */ + disabled: { + type: Boolean, + reflectToAttribute: true, + value: false + } + + }; + } + + constructor() { + super(); + this._observer = null; + this._target = null; + this._targetRect = null; + this._rects = null; + this._onTrack = this._onTrack.bind(this); + this._onDragStart = this._onDragStart.bind(this); + this._onTransitionEnd = this._onTransitionEnd.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onTouchMove = this._onTouchMove.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + idlePeriod.run(_ => { + this._observeItems(); + this._updateItems(); + this._toggleListeners({enable: true}); + }); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._unobserveItems(); + this._toggleListeners({enable: false}); + } + + _toggleListeners({enable}) { + const m = enable ? 'addEventListener' : 'removeEventListener'; + this.$.items[m]('dragstart', this._onDragStart); + this.$.items[m]('transitionend', this._onTransitionEnd); + this.$.items[m]('contextmenu', this._onContextMenu); + this.$.items[m]('touchmove', this._onTouchMove); + if (enable) { + addListener(this, 'track', this._onTrack); + } else { + removeListener(this, 'track', this._onTrack); + } + } + + _onTrack(event) { + switch(event.detail.state) { + case 'start': this._trackStart(event); break; + case 'track': this._track(event); break; + case 'end': this._trackEnd(event); break; + } + } + + _trackStart(event) { + if (this.disabled) { + return; + } + this._target = this._itemFromEvent(event); + if (!this._target) { + return; + } + event.stopPropagation(); + this._rects = this._getItemsRects(); + this._targetRect = this._rects[this.items.indexOf(this._target)]; + this._target.classList.add('item--dragged', 'item--pressed'); + if ('vibrate' in navigator) { + navigator.vibrate(30); + } + const rect = this.getBoundingClientRect(); + this.style.height = rect.height + 'px'; + this.style.width = rect.width + 'px'; + this.items.forEach((item, idx) => { + const rect = this._rects[idx]; + item.classList.add('item--transform'); + item.style.transition = 'none'; + item.__originalWidth = item.style.width; + item.__originalHeight = item.style.height; + item.style.width = rect.width + 'px'; + item.style.height = rect.height + 'px'; + this._translate3d(rect.left, rect.top, 1, item); + setTimeout(_ => { + item.style.transition = null; + }, 20); + }); + this._setDragging(true); + } + + _track(event) { + if (!this.dragging) { + return; + } + const left = this._targetRect.left + event.detail.dx; + const top = this._targetRect.top + event.detail.dy; + this._translate3d(left, top, 1, this._target); + const overItem = this._itemFromCoords(event.detail); + if (overItem && overItem !== this._target) { + const overItemIndex = this.items.indexOf(overItem); + const targetIndex = this.items.indexOf(this._target); + this._moveItemArray(this.items, targetIndex, overItemIndex); + for(let i = 0; i < this.items.length; i++) { + if (this.items[i] !== this._target) { + const rect = this._rects[i]; + requestAnimationFrame(_ => { + this._translate3d(rect.left, rect.top, 1, this.items[i]); + }); + } + } + } + } + + // The track really ends + _trackEnd(event) { + if (!this.dragging) { + return; + } + const rect = this._rects[this.items.indexOf(this._target)]; + this._target.classList.remove('item--pressed'); + this._setDragging(false); + this._translate3d(rect.left, rect.top, 1, this._target); + } + + _onTransitionEnd() { + if (this.dragging || !this._target) { + return; + } + const fragment = document.createDocumentFragment(); + this.items.forEach(item => { + item.style.transform = ''; + item.style.width = item.__originalWidth; + item.style.height = item.__originalHeight; + item.classList.remove('item--transform'); + fragment.appendChild(item); + }); + if (this.children[0]) { + this.insertBefore(fragment, this.children[0]); + } else { + this.appendChild(fragment); + } + this.style.height = ''; + this._target.classList.remove('item--dragged'); + this._rects = null; + this._targetRect = null; + this._updateItems(); + this.dispatchEvent(new CustomEvent('sort-finish', { + composed: true, + detail: { + target: this._target + } + })); + this._target = null; + } + + _onDragStart(event) { + event.preventDefault(); + } + + _onContextMenu(event) { + if (this.dragging) { + event.preventDefault(); + this._trackEnd(); + } + } + + _onTouchMove(event) { + event.preventDefault(); + } + + _updateItems() { + if (this.dragging) { + return; + } + const items = this.$.slot.assignedNodes().filter(node => { + if ((node.nodeType === Node.ELEMENT_NODE) && + (!this.sortable || node.matches(this.sortable))) { + return true; + } + }); + this._setItems(items); + } + + _itemFromCoords({x, y}) { + if (!this._rects) {return;} + let match = null; + this._rects.forEach((rect, i) => { + if ((x >= rect.left) && + (x <= rect.left + rect.width) && + (y >= rect.top) && + (y <= rect.top + rect.height)) { + match = this.items[i]; + } + }); + return match; + } + + _itemFromEvent(event) { + const path = event.composedPath(); + for (var i = 0; i < path.length; i++) { + if (this.items.indexOf(path[i]) > -1) { + return path[i]; + } + } + } + + _getItemsRects() { + return this.items.map(item => { + return item.getBoundingClientRect(); + }) + } + + _observeItems() { + if (!this._observer) { + this._observer = new MutationObserver(_ => { + this._updateItems(); + }); + this._observer.observe(this, {childList: true}); + } + } + + _unobserveItems() { + if (this._observer) { + this._observer.disconnect(); + this._observer = null; + } + } + + /** + * Move an array item from one position to another. + * Source: http://stackoverflow.com/questions/5306680/move-an-array-element-from-one-array-position-to-another + */ + _moveItemArray(array, oldIndex, newIndex) { + if (newIndex >= array.length) { + var k = newIndex - array.length; + while ((k--) + 1) { + array.push(undefined); + } + } + array.splice(newIndex, 0, array.splice(oldIndex, 1)[0]); + return array; + } + + _translate3d(x, y, z, el) { + el.style.transform = `translate3d(${x}px, ${y}px, ${z}px)`; + } +} + +customElements.define(SortableList.is, SortableList); diff --git a/test/sortable-list_test.html b/test/sortable-list_test.html index 11cd414..e37227b 100644 --- a/test/sortable-list_test.html +++ b/test/sortable-list_test.html @@ -6,10 +6,7 @@ - - - - + @@ -22,11 +19,14 @@ suite('sortable-list', function() { test('instantiating the element works', function() { - var element = fixture('basic'); - assert.equal(element.is, 'sortable-list'); - }); + - - +}); + \ No newline at end of file