From ec236720a2c96e505ee7e4456b375584db2a8463 Mon Sep 17 00:00:00 2001 From: Erwin Schalks Date: Tue, 31 Oct 2017 20:10:39 +0100 Subject: [PATCH] Component for defining a specific part of an element that can be dragged. --- examples/Handle.html | 49 ++++++++++ examples/css/main.css | 5 + src/vuedraggable.js | 212 +++++++++++++++++++++++++++--------------- 3 files changed, 189 insertions(+), 77 deletions(-) create mode 100644 examples/Handle.html diff --git a/examples/Handle.html b/examples/Handle.html new file mode 100644 index 00000000..f32ed7c0 --- /dev/null +++ b/examples/Handle.html @@ -0,0 +1,49 @@ + + + + + + Basic example + + + + + + + +
+

Vue Draggable

+ +
+

Draggable

+ +
+ [+] {{element.name}} +
+
+
+ +
+

Normal v-for

+
+
{{element.name}}
+
+
+ + + + +
+ + See 2 lists example + See clone element example + See clone v-if element example + +
+ + + + + + + \ No newline at end of file diff --git a/examples/css/main.css b/examples/css/main.css index ae8a0f9c..65277f25 100644 --- a/examples/css/main.css +++ b/examples/css/main.css @@ -85,3 +85,8 @@ border: 1px solid black; } +.drag-handler { + cursor: move; + font-weight: bold; +} + diff --git a/src/vuedraggable.js b/src/vuedraggable.js index 435c862e..ad453aa0 100644 --- a/src/vuedraggable.js +++ b/src/vuedraggable.js @@ -1,41 +1,41 @@ (function () { - "use strict"; + 'use strict' if (!Array.from) { Array.from = function (object) { - return [].slice.call(object); + return [].slice.call(object) } } - function buildDraggable(Sortable) { - function removeNode(node) { + function buildDraggable (Sortable) { + function removeNode (node) { node.parentElement.removeChild(node) } - function insertNodeAt(fatherNode, node, position) { - const refNode = (position ===0) ? fatherNode.children[0] : fatherNode.children[position-1].nextSibling + function insertNodeAt (fatherNode, node, position) { + const refNode = (position === 0) ? fatherNode.children[0] : fatherNode.children[position - 1].nextSibling fatherNode.insertBefore(node, refNode) } - function computeVmIndex(vnodes, element) { + function computeVmIndex (vnodes, element) { return vnodes.map(elt => elt.elm).indexOf(element) } - function computeIndexes(slots, children, isTransition) { + function computeIndexes (slots, children, isTransition) { if (!slots) { return [] } - const elmFromNodes = slots.map(elt => elt.elm); - const rawIndexes = [...children].map(elt => elmFromNodes.indexOf(elt)) - return isTransition? rawIndexes.filter(ind => ind!==-1) : rawIndexes + const elmFromNodes = slots.map(elt => elt.elm) + const rawIndexes = [...children].map(elt => elmFromNodes.indexOf(elt)) + return isTransition ? rawIndexes.filter(ind => ind !== -1) : rawIndexes } - function emit(evtName, evtData) { + function emit (evtName, evtData) { this.$nextTick(() => this.$emit(evtName.toLowerCase(), evtData)) } - function delegateAndEmit(evtName) { + function delegateAndEmit (evtName) { return (evtData) => { if (this.realList !== null) { this['onDrag' + evtName](evtData) @@ -67,7 +67,7 @@ }, clone: { type: Function, - default: (original) => { return original; } + default: (original) => { return original } }, element: { type: String, @@ -84,86 +84,95 @@ props, - data() { + data () { return { transitionMode: false, - componentMode: false + componentMode: false, + handleClass: null, } }, - render(h) { + render (h) { const slots = this.$slots.default if (slots && slots.length === 1) { const child = slots[0] - if (child.componentOptions && child.componentOptions.tag === "transition-group") { + if (child.componentOptions && child.componentOptions.tag === 'transition-group') { this.transitionMode = true } } let children = slots const {footer} = this.$slots if (footer) { - children = slots? [...slots, ...footer] : [...footer] + children = slots ? [...slots, ...footer] : [...footer] } - return h(this.element, null, children); + return h(this.element, null, children) }, - mounted() { + mounted () { this.componentMode = this.element.toLowerCase() !== this.$el.nodeName.toLowerCase() if (this.componentMode && this.transitionMode) { - throw new Error(`Transition-group inside component is not supported. Please alter element value or remove transition-group. Current element value: ${this.element}`); + throw new Error(`Transition-group inside component is not supported. Please alter element value or remove transition-group. Current element value: ${this.element}`) } - var optionsAdded = {}; + var optionsAdded = {} eventsListened.forEach(elt => { optionsAdded['on' + elt] = delegateAndEmit.call(this, elt) - }); + }) eventsToEmit.forEach(elt => { optionsAdded['on' + elt] = emit.bind(this, elt) - }); + }) - const options = Object.assign({}, this.options, optionsAdded, { onMove: (evt, originalEvent) => { return this.onDragMove(evt, originalEvent); } }) - !('draggable' in options) && (options.draggable = '>*'); + const options = Object.assign({}, this.allOptions, optionsAdded, {onMove: (evt, originalEvent) => { return this.onDragMove(evt, originalEvent) }}) + !('draggable' in options) && (options.draggable = '>*') this._sortable = new Sortable(this.rootContainer, options) this.computeIndexes() }, - beforeDestroy() { + beforeDestroy () { this._sortable.destroy() }, computed: { - rootContainer() { - return this.transitionMode ? this.$el.children[0] : this.$el; + rootContainer () { + return this.transitionMode ? this.$el.children[0] : this.$el }, - isCloning() { - return (!!this.options) && (!!this.options.group) && (this.options.group.pull === 'clone') + isCloning () { + return (!!this.allOptions) && (!!this.allOptions.group) && (this.allOptions.group.pull === 'clone') }, - realList() { - return (!!this.list) ? this.list : this.value; - } + realList () { + return (!!this.list) ? this.list : this.value + }, + + allOptions () { + const options = Object.assign({}, this.options || {}) + if (this.handleClass && !options.handle) { + options.handle = '.'+this.handleClass + } + return options + }, }, watch: { - options:{ - handler(newOptionValue) { + allOptions: { + handler (newOptionValue) { for (var property in newOptionValue) { if (readonlyProperties.indexOf(property) == -1) { - this._sortable.option(property, newOptionValue[property]); + this._sortable.option(property, newOptionValue[property]) } } }, deep: true }, - realList() { + realList () { this.computeIndexes() } }, methods: { - getChildrenNodes() { + getChildrenNodes () { if (this.componentMode) { return this.$children[0].$slots.default } @@ -171,13 +180,13 @@ return this.transitionMode ? rawNodes[0].child.$slots.default : rawNodes }, - computeIndexes() { + computeIndexes () { this.$nextTick(() => { this.visibleIndexes = computeIndexes(this.getChildrenNodes(), this.rootContainer.children, this.transitionMode) }) }, - getUnderlyingVm(htmlElt) { + getUnderlyingVm (htmlElt) { const index = computeVmIndex(this.getChildrenNodes() || [], htmlElt) if (index === -1) { //Edge case during move callback: related element might be @@ -185,23 +194,23 @@ return null } const element = this.realList[index] - return { index, element } + return {index, element} }, - getUnderlyingPotencialDraggableComponent({ __vue__ }) { - if (!__vue__ || !__vue__.$options || __vue__.$options._componentTag !== "transition-group") { + getUnderlyingPotencialDraggableComponent ({__vue__}) { + if (!__vue__ || !__vue__.$options || __vue__.$options._componentTag !== 'transition-group') { return __vue__ } return __vue__.$parent }, - emitChanges(evt) { + emitChanges (evt) { this.$nextTick(() => { this.$emit('change', evt) - }); + }) }, - alterList(onList) { + alterList (onList) { if (!!this.list) { onList(this.list) } @@ -212,23 +221,23 @@ } }, - spliceList() { + spliceList () { const spliceList = list => list.splice(...arguments) this.alterList(spliceList) }, - updatePosition(oldIndex, newIndex) { + updatePosition (oldIndex, newIndex) { const updatePosition = list => list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]) this.alterList(updatePosition) }, - getRelatedContextFromMoveEvent({ to, related }) { + getRelatedContextFromMoveEvent ({to, related}) { const component = this.getUnderlyingPotencialDraggableComponent(to) if (!component) { - return { component } + return {component} } const list = component.realList - const context = { list, component } + const context = {list, component} if (to !== related && list && component.getUnderlyingVm) { const destination = component.getUnderlyingVm(related) if (destination) { @@ -239,17 +248,17 @@ return context }, - getVmIndex(domIndex) { + getVmIndex (domIndex) { const indexes = this.visibleIndexes const numberIndexes = indexes.length return (domIndex > numberIndexes - 1) ? numberIndexes : indexes[domIndex] }, - getComponent() { + getComponent () { return this.$slots.default[0].componentInstance }, - resetTransitionData(index) { + resetTransitionData (index) { if (!this.noTransitionOnDrag || !this.transitionMode) { return } @@ -260,13 +269,13 @@ transitionContainer.kept = undefined }, - onDragStart(evt) { + onDragStart (evt) { this.context = this.getUnderlyingVm(evt.item) evt.item._underlying_vm_ = this.clone(this.context.element) draggingElement = evt.item }, - onDragAdd(evt) { + onDragAdd (evt) { const element = evt.item._underlying_vm_ if (element === undefined) { return @@ -275,11 +284,11 @@ const newIndex = this.getVmIndex(evt.newIndex) this.spliceList(newIndex, 0, element) this.computeIndexes() - const added = { element, newIndex } - this.emitChanges({ added }) + const added = {element, newIndex} + this.emitChanges({added}) }, - onDragRemove(evt) { + onDragRemove (evt) { insertNodeAt(this.rootContainer, evt.item, evt.oldIndex) if (this.isCloning) { removeNode(evt.clone) @@ -287,33 +296,33 @@ } const oldIndex = this.context.index this.spliceList(oldIndex, 1) - const removed = { element: this.context.element, oldIndex } + const removed = {element: this.context.element, oldIndex} this.resetTransitionData(oldIndex) - this.emitChanges({ removed }) + this.emitChanges({removed}) }, - onDragUpdate(evt) { + onDragUpdate (evt) { removeNode(evt.item) insertNodeAt(evt.from, evt.item, evt.oldIndex) const oldIndex = this.context.index const newIndex = this.getVmIndex(evt.newIndex) this.updatePosition(oldIndex, newIndex) - const moved = { element: this.context.element, oldIndex, newIndex } - this.emitChanges({ moved }) + const moved = {element: this.context.element, oldIndex, newIndex} + this.emitChanges({moved}) }, - computeFutureIndex(relatedContext, evt) { + computeFutureIndex (relatedContext, evt) { if (!relatedContext.element) { return 0 } - const domChildren = [...evt.to.children].filter(el => el.style['display']!=='none') + const domChildren = [...evt.to.children].filter(el => el.style['display'] !== 'none') const currentDOMIndex = domChildren.indexOf(evt.related) const currentIndex = relatedContext.component.getVmIndex(currentDOMIndex) const draggedInList = domChildren.indexOf(draggingElement) != -1 return (draggedInList || !evt.willInsertAfter) ? currentIndex : currentIndex + 1 }, - onDragMove(evt, originalEvent) { + onDragMove (evt, originalEvent) { const onMove = this.move if (!onMove || !this.realList) { return true @@ -322,27 +331,76 @@ const relatedContext = this.getRelatedContextFromMoveEvent(evt) const draggedContext = this.context const futureIndex = this.computeFutureIndex(relatedContext, evt) - Object.assign(draggedContext, { futureIndex }) - Object.assign(evt, { relatedContext, draggedContext }) + Object.assign(draggedContext, {futureIndex}) + Object.assign(evt, {relatedContext, draggedContext}) return onMove(evt, originalEvent) }, - onDragEnd(evt) { + onDragEnd (evt) { this.computeIndexes() draggingElement = null } } } + draggableComponent.draggableHandle = buildDraggableHandle(Sortable) return draggableComponent } - if (typeof exports == "object") { - var Sortable = require("sortablejs") + function buildDraggableHandle () { + let nextHandleClassId = 0 + + function findParentDraggable (handle) { + for (let parent = handle.$parent; parent; parent = parent.$parent) { + if (parent.$options.name === 'draggable') { + return parent + } + } + return null + } + + return { + name: 'draggable-handle', + props: { + tag: { + type: String, + 'default': 'span', + }, + }, + + render (h) { + return h(this.tag, { + class: this.handleClass, + }, this.$slots.default) + }, + + computed: { + draggable () { + return findParentDraggable(this) + }, + + handleClass () { + if (!this.draggable) { + return null + } + + if (!this.draggable.handleClass) { + this.draggable.handleClass = 'v-draggable-handle-' + (nextHandleClassId++) + } + + return this.draggable.handleClass + }, + }, + } + } + + if (typeof exports == 'object') { + var Sortable = require('sortablejs') module.exports = buildDraggable(Sortable) - } else if (typeof define == "function" && define.amd) { - define(['sortablejs'], function (Sortable) { return buildDraggable(Sortable); }); + } else if (typeof define == 'function' && define.amd) { + define(['sortablejs'], function (Sortable) { return buildDraggable(Sortable) }) } else if (window && (window.Vue) && (window.Sortable)) { var draggable = buildDraggable(window.Sortable) Vue.component('draggable', draggable) + Vue.component('draggable-handle', draggable.draggableHandle) } -})(); +})()