Skip to content

Instantly share code, notes, and snippets.

@archseer
Created March 16, 2018 15:20
Show Gist options
  • Save archseer/8529c45c5b76e564c53eb4f48524f623 to your computer and use it in GitHub Desktop.
Save archseer/8529c45c5b76e564c53eb4f48524f623 to your computer and use it in GitHub Desktop.
<script>
export default {
name: 'vddl-list',
// css: placeholder, dragover
props: {
list: String,
allowedTypes: Array,
disableIf: Boolean,
horizontal: Boolean,
externalSources: Boolean,
dragover: Function,
inserted: Function,
drop: Function,
},
data() {
return {
placeholderIndex: null,
};
},
render(createElement) {
const placeholder = this.$slots.placeholder ?
this.$slots.placeholder[0]
: createElement('div', { attrs: { class: 'placeholder' } });
const children = [...(this.$slots.default || [])];
if (this.placeholderIndex !== null) {
children.splice(this.placeholderIndex, 0, placeholder);
}
return createElement(
'div',
{
attrs: {
class: 'vddl-list',
},
on: {
dragenter: this.handleDragenter,
dragover: this.handleDragover,
drop: this.handleDrop,
dragleave: this.handleDragleave,
},
},
children,
);
},
computed: {},
methods: {
handleDragenter(event) {
event.preventDefault();
if (!this.isDropAllowed(event)) { return true; }
},
handleDragover(event) {
event.stopPropagation();
event.preventDefault();
if (!this.isDropAllowed(event)) { return true; }
if (event.target !== this.$el) {
// Try to find the node direct directly below the list node.
let listItemNode = event.target;
while (listItemNode.parentNode !== this.$el && listItemNode.parentNode) {
listItemNode = listItemNode.parentNode;
}
if (listItemNode.parentNode === this.$el && listItemNode.className !== 'placeholder') {
// this is the first frame of the drag and items are wrongly spaced
if (!this.placeholderIndex) {
this.placeholderIndex = this.getNodeIndex(listItemNode);
// If the mouse pointer is in the upper half of the child element,
// we place it before the child element, otherwise below it.
} else if (this.isMouseInFirstHalf(event, listItemNode)) {
this.placeholderIndex = this.getNodeIndex(listItemNode);
} else {
this.placeholderIndex = this.getNodeIndex(listItemNode) + 1;
}
}
} else {
// TODO:
/*
// This branch is reached when we are dragging directly over the list element.
// Usually we wouldn't need to do anything here, but the IE does not fire it's
// events for the child element, only for the list directly. Therefore, we repeat
// the positioning algorithm for IE here.
if (this.isMouseInFirstHalf(event, this.placeholderNode, true)) {
// Check if we should move the placeholder element one spot towards the top.
// Note that display none elements will have offsetTop and offsetHeight set to
// zero, therefore we need a special check for them.
while (this.placeholderNode.previousElementSibling
&& (this.isMouseInFirstHalf(event, this.placeholderNode.previousElementSibling, true)
|| this.placeholderNode.previousElementSibling.offsetHeight === 0)) {
//this.$el.insertBefore(this.placeholderNode, this.placeholderNode.previousElementSibling);
}
} else {
// Check if we should move the placeholder element one spot towards the bottom
while (this.placeholderNode.nextElementSibling &&
!this.isMouseInFirstHalf(event, this.placeholderNode.nextElementSibling, true)) {
//this.$el.insertBefore(this.placeholderNode,
// this.placeholderNode.nextElementSibling.nextElementSibling);
}
}
*/
}
// At this point we invoke the callback, which still can disallow the drop.
// We can't do this earlier because we want to pass the index of the placeholder.
if (this.dragover && !this.invokeCallback('dragover', event, this.placeholderIndex)) {
return this.stopDragover(event);
}
if (this.$el.className.indexOf('vddl-dragover') < 0) { this.$el.className = this.$el.className.trim() + ' vddl-dragover'; }
return false;
},
handleDrop(event) {
event.stopPropagation();
event.preventDefault();
if (!this.isDropAllowed(event)) { return true; }
// The default behavior in Firefox is to interpret the dropped element as URL and
// forward to it. We want to prevent that even if our drop is aborted.
// Unserialize the data that was serialized in dragstart. According to the HTML5 specs,
// the "Text" drag type will be converted to text/plain, but IE does not do that.
const data = event.dataTransfer.getData('Text') || event.dataTransfer.getData('text/plain');
let transferredObject;
try {
transferredObject = JSON.parse(data);
} catch (e) {
return this.stopDragover();
}
// Invoke the callback, which can transform the transferredObject and even abort the drop.
const index = this.placeholderIndex;
if (this.drop) {
transferredObject = this.invokeCallback('drop', event, index, transferredObject);
if (!transferredObject) {
return this.stopDragover();
}
}
// Insert the object into the array, unless drop took care of that (returned true).
if (transferredObject !== true) {
this.list.splice(index, 0, transferredObject);
}
this.invokeCallback('inserted', event, index, transferredObject);
// In Chrome on Windows the dropEffect will always be none...
// We have to determine the actual effect manually from the allowed effects
if (event.dataTransfer.dropEffect === 'none') {
if (event.dataTransfer.effectAllowed === 'copy' ||
event.dataTransfer.effectAllowed === 'move') {
this.vddlDropEffectWorkaround.dropEffect = event.dataTransfer.effectAllowed;
} else {
this.vddlDropEffectWorkaround.dropEffect = event.ctrlKey ? 'copy' : 'move';
}
} else {
this.vddlDropEffectWorkaround.dropEffect = event.dataTransfer.dropEffect;
}
// Clean up
this.stopDragover();
return false;
},
handleDragleave(event) {
this.$el.className = this.$el.className.replace('vddl-dragover', '').trim();
setTimeout(() => {
if (this.$el.className.indexOf('vddl-dragover') < 0) {
this.placeholderIndex = null;
}
}, 100);
},
// Checks whether the mouse pointer is in the first half of the given target element.
isMouseInFirstHalf(event, targetNode, relativeToParent) {
const mousePointer = this.horizontal ? (event.offsetX || event.layerX)
: (event.offsetY || event.layerY);
const targetSize = this.horizontal ? targetNode.offsetWidth : targetNode.offsetHeight;
let targetPosition = this.horizontal ? targetNode.offsetLeft : targetNode.offsetTop;
targetPosition = relativeToParent ? targetPosition : 0;
return mousePointer < targetPosition + targetSize / 2;
},
getNodeIndex(node) {
return Array.from(this.$el.children)
.filter((c) => !c.classList.contains('vddl-dragging'))
.indexOf(node);
},
/**
* Checks various conditions that must be fulfilled for a drop to be allowed
*/
isDropAllowed(event) {
// Disallow drop from external source unless it's allowed explicitly.
if (!this.vddlDragTypeWorkaround.isDragging && !this.externalSources) { return false; }
// Check mimetype. Usually we would use a custom drag type instead of Text, but IE doesn't
// support that.
if (!this.hasTextMimetype(event.dataTransfer.types)) { return false; }
// Now check the allowed-types against the type of the incoming element. For drops from
// external sources we don't know the type, so it will need to be checked via drop.
if (this.allowedTypes && this.vddlDragTypeWorkaround.isDragging) {
const allowed = this.allowedTypes;
if (Array.isArray(allowed) && allowed.indexOf(this.vddlDragTypeWorkaround.dragType) === -1) {
return false;
}
}
// Check whether droping is disabled completely
if (this.disableIf) { return false; }
return true;
},
/**
* Small helper function that cleans up if we aborted a drop.
*/
stopDragover() {
this.placeholderIndex = null;
this.$el.className = this.$el.className.replace('vddl-dragover', '').trim();
return true;
},
/**
* Invokes a callback with some interesting parameters and returns the callbacks return value.
*/
invokeCallback(expression, event, index, item) {
const fn = this[expression];
if (fn) {
fn({
event,
index,
item: item || undefined,
list: this.list,
external: !this.vddlDragTypeWorkaround.isDragging,
type: this.vddlDragTypeWorkaround.isDragging ? this.vddlDragTypeWorkaround.dragType : undefined,
});
}
return fn ? true : false;
},
/**
* Check if the dataTransfer object contains a drag type that we can handle. In old versions
* of IE the types collection will not even be there, so we just assume a drop is possible.
*/
hasTextMimetype(types) {
if (!types) { return true; }
for (let i = 0; i < types.length; i += 1) {
if (types[i] === 'Text' || types[i] === 'text/plain') { return true; }
}
return false;
},
},
};
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment