UNPKG

vue-mover

Version:

A simple list picker vue-component, with drag and drop and sortable items.

487 lines (426 loc) 18 kB
/* Vue-Mover Component ------------------- by Rick Strahl, West Wind Technologies Version 0.3.0 February 8th, 2018 depends on: ----------- CSS: * font-awesome (optional) * vue-mover.css Script: * vuejs * sortablejs Usage: ------ <mover :left-items="selectedItems" :right-items="unselectedItems" title-left="Available Items" title-right="Selected Items" moved-item-location="top" :font-awesome="true" targetId="MyMover"> </mover> Vue code: var app = new Vue({ el: "#Body", data: function() { return { unselectedItems: [ { value: "vitem1", displayValue: "vItem 1", isSelected: false }, { value: "vitem2", displayValue: "vItem 2", isSelected: true }, ], selectedItems: [ { value: "xitem3", displayValue: "xItem 3", isSelected: false } ] } } }); */ if (!Sortable) { throw new Error('[vue-mover] cannot locate `Sortablejs` dependency.') } var vue = Vue.component("mover", { vue: vue, props: { // Left side title - defaults to Available titleLeft: { type: String, default: 'Available' }, // Right side title - defaults to Selected titleRight: { type: String, default: 'Selected' }, // Location where moved items are dropped: top, bottom movedItemLocation: { type: String, default: "top" }, // Array of objects to bind to left list. { value: "xxx", displayValue: "show", isSelected: false} leftItems: Array, // Array of objects to bind right list. { value: "xxx", displayValue: "show", isSelected: false} rightItems: Array, // The ID assigned to the wrapping element of this component targetId: { type: String, default: "Mover" }, // determines whether duplicated values are cleared in left items normalizeLists: { type: Boolean, default: true }, fontAwesome: { type: Boolean, default: true } }, template: '<div :id="targetId" class="mover-container">' + '\n' + ' <div id="MoverLeft" class="mover-panel-box mover-left">' + '\n' + ' <div class="mover-header">{{titleLeft}}</div>' + '\n' + ' <div :id="targetId + \'LeftItems\'" class="mover-panel ">\n' + ' <div class="mover-item"' + '\n' + ' v-for="item in unselectedItems"' + '\n' + ' :class="{\'mover-selected\': item.isSelected }"' + '\n' + ' v-on:click="selectItem(item, unselectedItems)"' + '\n' + ' :data-id="item.value" data-side="left"' + '\n' + ' >{{item.displayValue}}</div>' + '\n' + ' </div>\n' + ' </div>' + '\n' + '' + '\n' + ' <div class="mover-controls" >' + '\n' + ' <button type="button" v-on:click="moveAllRight()">' + '\n' + ' <i v-if="fontAwesome" class="fa fa-forward fa-1.5x" aria-hidden="true"></i>' + '\n' + ' <b v-if="!fontAwesome" aria-hidden="true">>></b>' + '\n' + ' </button>' + '\n' + ' <button type="button" v-on:click="moveRight()" style="margin-bottom: 30px;" >' + '\n' + ' <i v-if="fontAwesome" class="fa fa-caret-right fa-2x" aria-hidden="true"></i>' + '\n' + ' <b v-if="!fontAwesome" aria-hidden="true">></b>' + '\n' + ' </button>' + '\n' + ' <button type="button" v-on:click="moveLeft()">' + '\n' + ' <i v-if="fontAwesome" class="fa fa-caret-left fa-2x" aria-hidden="true"></i>' + '\n' + ' <b v-if="!fontAwesome" aria-hidden="true"><</b>' + '\n' + ' </button>' + '\n' + ' <button type="button" v-on:click="moveAllLeft()">' + '\n' + ' <i v-if="fontAwesome" class="fa fa-backward" aria-hidden="true"></i>' + '\n' + ' <b v-if="!fontAwesome" aria-hidden="true"><<</b>' + '\n' + ' </button>' + '\n' + '' + '\n' + ' </div>' + '\n' + '' + '\n' + ' <div id="MoverRight" class="mover-panel-box mover-right">' + '\n' + ' <div class="mover-header">{{titleRight}}</div>' + '\n' + ' <div :id="targetId + \'RightItems\'" class="mover-panel">\n' + ' <div class="mover-item"' + '\n' + ' v-for="item in selectedItems"' + '\n' + ' :class="{\'mover-selected\': item.isSelected }"' + '\n' + ' v-on:click="selectItem(item, selectedItems)"' + '\n' + ' :data-id="item.value" data-side="right"' + '\n' + ' >{{item.displayValue}}</div>' + '\n' + ' </div>\n' + ' </div>' + '\n' + '</div>' + '\n', data: function () { var vm = { selectedSortable: null, selectedItem: {}, selectedList: null, selectedItems: this.rightItems, unselectedItems: this.leftItems, // hook up sortable - call from end of data retrieval initialize: function (vue) { var options = { group: "_mvgp_" + new Date().getTime(), ghostClass: "mover-ghost", chosenClass: "mover-selected", onAdd: vm.onListDrop, onUpdate: vm.onSorted, }; var targetId = vue.targetId; var el = document.getElementById(targetId + 'LeftItems'); vm.unselectedSortable = Sortable.create(el, options); var el2 = document.getElementById(targetId + 'RightItems'); vm.selectedSortable = Sortable.create(el2, options); if (vue.normalizeLists) vm.normalizeListValues(); }, selectItem: function (item, items) { if (!item) { if (items.length > 0) item = items[0]; if (!item) return; } items.forEach(function (itm) { itm.isSelected = false; }); item.isSelected = true; vm.selectedItem = item; vm.selectedList = items; }, moveRight: function (item, index) { if (!item) { var item = vm.unselectedItems.find(function (itm) { return itm.isSelected; }); } if (!item) return; // remove item and select next item var selectNext = false; var idx = vm.unselectedItems.findIndex(function (itm) { return itm.value == item.value; }); vm.unselectedItems.splice(idx, 1); if (vm.unselectedItems.length > 0) vm.selectItem(vm.unselectedItems[idx], vm.unselectedItems); if (typeof index === "number") vm.selectedItems.splice(index, 0, item); else{ if(vue.movedItemLocation == "top") vm.selectedItems.unshift(item); else{ vm.selectedItems.push(item); var container = this.$el.querySelector(".mover-right>.mover-panel"); setTimeout(function() { container.scrollTop = container.scrollHeight; }); } } setTimeout(function () { vm.selectItem(item, vm.selectedItems); }, 10); }, moveLeft: function (item, index) { var item = vm.selectedItems.find(function (itm) { return itm.isSelected; }); if (!item) return; // remove item var selectNext = false; var idx = vm.selectedItems.findIndex(function (itm) { return itm.value == item.value; }); vm.selectedItems.splice(idx, 1); if (vm.selectedItems.length > 0) vm.selectItem(vm.selectedItems[idx], vm.selectedItems); if (typeof index === "number") vm.unselectedItems.splice(index, 0, item); else { if(vue.movedItemLocation == "top") vm.unselectedItems.unshift(item); else { vm.unselectedItems.push(item); var container = this.$el.querySelector(".mover-left>.mover-panel"); setTimeout(function() { container.scrollTop = container.scrollHeight; }); } } setTimeout(function () { vm.selectItem(item, vm.unselectedItems); }, 10); }, moveAllRight: function () { for (var i = vm.unselectedItems.length - 1; i >= 0; i--) { var item = vm.unselectedItems[i]; vm.unselectedItems.splice(i, 1); vm.selectedItems.push(item); } }, moveAllLeft: function () { for (var i = vm.selectedItems.length - 1; i >= 0; i--) { var item = vm.selectedItems[i]; vm.selectedItems.splice(i, 1); vm.unselectedItems.push(item); } }, refreshListDisplay: function () { setTimeout(function () { var list = vm.selectedItems; vm.selectedItems = []; vm.selectedItems = list; list = vm.unselectedItems; vm.unselectedItems = []; vm.unselectedItems = list; }, 10); }, onSorted: function (e) { var key = e.item.dataset["id"]; var side = e.item.dataset["side"]; var list; if (side == "left") { list = vm.unselectedItems; vm.unselectedItems = []; } else { list = vm.selectedItems; vm.selectedItems = []; } var item = list.find(function (itm) { return itm.value == key; }); if (!item) return; setTimeout(function () { list.splice(e.oldIndex, 1); list.splice(e.newIndex, 0, item); if (side == "left") { vm.unselectedItems = list; vm.selectItem(item, vm.unselectedItems); } else { vm.selectedItems = list; vm.selectItem(item, vm.selectedItems); } }); }, onListDrop: function (e) { var key = e.item.dataset["id"]; var side = e.item.dataset["side"]; var insertAt = e.newIndex; // Hack! Remove the dropped item and let Vue handle rendering //e.item.remove(); if (side == "left") { var item = vm.unselectedItems.find(function (itm) { return itm.value == key; }); vm.moveRight(item, insertAt); item.isSelected = true; // force list to refresh var list = vm.unselectedItems; vm.unselectedItems = []; setTimeout(function () { vm.unselectedItems = list; }); } else { var item = vm.selectedItems.find(function (itm) { return itm.value == key; }); item.isSelected = true; vm.moveLeft(item, insertAt); // force list to refresh completely var list = vm.selectedItems; vm.selectedItems = []; setTimeout(function () { vm.selectedItems = list; }); } }, // removes dupes from unselected list that exist in selected items normalizeListValues: function () { if (!vm.selectedItems || vm.selectedItems.length == 0 || !vm.unselectedItems || vm.unselectedItems.length == 0) return; for (var i = 0; i < vm.selectedItems.length; i++) { var selected = vm.selectedItems[i]; var idx = vm.unselectedItems.findIndex(function (itm) { return itm.value == selected.value; }); if (idx > -1) vm.unselectedItems.splice(idx, 1); } } } var vue = this; setTimeout(function () { vm.initialize(vue); }); return vm; } }); // if (typeof exports == "object") { // module.exports = vue; // } else if (typeof define == "function" && define.amd) { // define([], function () { // return vue; // }) // } else if (window.Vue) { // window.vMover = vue; // } // IE Array Polyfills // https://tc39.github.io/ecma262/#sec-array.prototype.find if (!Array.prototype.find) { Object.defineProperty(Array.prototype, 'find', { value: function (predicate) { // 1. Let O be ? ToObject(this value). if (this == null) { throw new TypeError('"this" is null or not defined'); } var o = Object(this); // 2. Let len be ? ToLength(? Get(O, "length")). var len = o.length >>> 0; // 3. If IsCallable(predicate) is false, throw a TypeError exception. if (typeof predicate !== 'function') { throw new TypeError('predicate must be a function'); } // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. var thisArg = arguments[1]; // 5. Let k be 0. var k = 0; // 6. Repeat, while k < len while (k < len) { // a. Let Pk be ! ToString(k). // b. Let kValue be ? Get(O, Pk). // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). // d. If testResult is true, return kValue. var kValue = o[k]; if (predicate.call(thisArg, kValue, k, o)) { return kValue; } // e. Increase k by 1. k++; } // 7. Return undefined. return undefined; } }); } // https://tc39.github.io/ecma262/#sec-array.prototype.findIndex if (!Array.prototype.findIndex) { Object.defineProperty(Array.prototype, 'findIndex', { value: function (predicate) { // 1. Let O be ? ToObject(this value). if (this == null) { throw new TypeError('"this" is null or not defined'); } var o = Object(this); // 2. Let len be ? ToLength(? Get(O, "length")). var len = o.length >>> 0; // 3. If IsCallable(predicate) is false, throw a TypeError exception. if (typeof predicate !== 'function') { throw new TypeError('predicate must be a function'); } // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. var thisArg = arguments[1]; // 5. Let k be 0. var k = 0; // 6. Repeat, while k < len while (k < len) { // a. Let Pk be ! ToString(k). // b. Let kValue be ? Get(O, Pk). // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). // d. If testResult is true, return k. var kValue = o[k]; if (predicate.call(thisArg, kValue, k, o)) { return k; } // e. Increase k by 1. k++; } // 7. Return -1. return -1; } }); }