mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-12-30 20:10:28 +00:00
477 lines
14 KiB
JavaScript
Executable File
477 lines
14 KiB
JavaScript
Executable File
(function(doc) {
|
|
|
|
function _exposeIosHtml5DragDropShim(config) {
|
|
log = noop; // noOp, remove this line to enable debugging
|
|
|
|
var coordinateSystemForElementFromPoint;
|
|
|
|
var DRAG_OVER_EMIT_FREQ = 50;
|
|
|
|
function main() {
|
|
config = config || {};
|
|
if (!config.hasOwnProperty("simulateAnchorClick")) config.simulateAnchorClick = true;
|
|
|
|
coordinateSystemForElementFromPoint = navigator.userAgent.match(/OS [1-4](?:_\d+)+ like Mac/) ? "page" : "client";
|
|
|
|
var div = doc.createElement('div');
|
|
var dragDiv = 'draggable' in div;
|
|
var evts = 'ondragstart' in div && 'ondrop' in div;
|
|
|
|
var needsPatch = !(dragDiv || evts) || /iPad|iPhone|iPod|Android/.test(navigator.userAgent);
|
|
log((needsPatch ? "" : "not ") + "patching html5 drag drop");
|
|
|
|
if(!needsPatch) {
|
|
return;
|
|
}
|
|
|
|
if(!config.enableEnterLeave) {
|
|
DragDrop.prototype.synthesizeEnterLeave = noop;
|
|
}
|
|
|
|
if(config.holdToDrag){
|
|
doc.addEventListener("touchstart", touchstartDelay(config.holdToDrag), {passive:false});
|
|
}
|
|
else {
|
|
doc.addEventListener("touchstart", touchstart, {passive:false});
|
|
}
|
|
}
|
|
|
|
function DragDrop(event, el) {
|
|
|
|
this.dragData = {};
|
|
this.dragDataTypes = [];
|
|
this.dragImage = null;
|
|
this.dragImageTransform = null;
|
|
this.dragImageWebKitTransform = null;
|
|
this.customDragImage = null;
|
|
this.customDragImageX = null;
|
|
this.customDragImageY = null;
|
|
this.el = el || event.target;
|
|
this.dragOverTimer = null;
|
|
this.lastMoveEvent = null;
|
|
|
|
log("dragstart");
|
|
|
|
if (this.dispatchDragStart()) {
|
|
this.createDragImage();
|
|
this.listen();
|
|
}
|
|
}
|
|
|
|
DragDrop.prototype = {
|
|
listen: function() {
|
|
var move = onEvt(doc, "touchmove", this.move, this);
|
|
var end = onEvt(doc, "touchend", ontouchend, this);
|
|
var cancel = onEvt(doc, "touchcancel", cleanup, this);
|
|
|
|
function ontouchend(event) {
|
|
this.dragend(event, event.target);
|
|
cleanup.call(this);
|
|
}
|
|
function cleanup() {
|
|
log("cleanup");
|
|
this.dragDataTypes = [];
|
|
if (this.dragImage !== null) {
|
|
this.dragImage.parentNode.removeChild(this.dragImage);
|
|
this.dragImage = null;
|
|
this.dragImageTransform = null;
|
|
this.dragImageWebKitTransform = null;
|
|
}
|
|
this.customDragImage = null;
|
|
this.customDragImageX = null;
|
|
this.customDragImageY = null;
|
|
this.el = this.dragData = null;
|
|
return [move, end, cancel].forEach(function(handler) {
|
|
return handler.off();
|
|
});
|
|
}
|
|
},
|
|
move: function(event) {
|
|
event.preventDefault();
|
|
var pageXs = [], pageYs = [];
|
|
[].forEach.call(event.changedTouches, function(touch) {
|
|
pageXs.push(touch.pageX);
|
|
pageYs.push(touch.pageY);
|
|
});
|
|
|
|
var x = average(pageXs) - (this.customDragImageX || parseInt(this.dragImage.offsetWidth, 10) / 2);
|
|
var y = average(pageYs) - (this.customDragImageY || parseInt(this.dragImage.offsetHeight, 10) / 2);
|
|
this.translateDragImage(x, y);
|
|
|
|
this.synthesizeEnterLeave(event);
|
|
this.synthesizeOver(event);
|
|
},
|
|
// We use translate instead of top/left because of sub-pixel rendering and for the hope of better performance
|
|
// http://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/
|
|
translateDragImage: function(x, y) {
|
|
var translate = "translate(" + x + "px," + y + "px) ";
|
|
|
|
if (this.dragImageWebKitTransform !== null) {
|
|
this.dragImage.style["-webkit-transform"] = translate + this.dragImageWebKitTransform;
|
|
}
|
|
if (this.dragImageTransform !== null) {
|
|
this.dragImage.style.transform = translate + this.dragImageTransform;
|
|
}
|
|
},
|
|
synthesizeEnterLeave: function(event) {
|
|
var target = elementFromTouchEvent(this.el,event)
|
|
if (target != this.lastEnter) {
|
|
if (this.lastEnter) {
|
|
this.dispatchLeave(event);
|
|
}
|
|
this.lastEnter = target;
|
|
if (this.lastEnter) {
|
|
this.dispatchEnter(event);
|
|
}
|
|
}
|
|
},
|
|
synthesizeOver: function(event) {
|
|
this.lastMoveEvent = event;
|
|
if(this.lastEnter && !this.dragOverTimer) {
|
|
this.dragOverTimer = setInterval(this.dispatchOver.bind(this), DRAG_OVER_EMIT_FREQ);
|
|
}
|
|
},
|
|
clearDragOverTimer: function() {
|
|
if(this.dragOverTimer) {
|
|
clearInterval(this.dragOverTimer);
|
|
this.dragOverTimer = null;
|
|
}
|
|
},
|
|
dragend: function(event) {
|
|
|
|
// we'll dispatch drop if there's a target, then dragEnd.
|
|
// drop comes first http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model
|
|
log("dragend");
|
|
|
|
if (this.lastEnter) {
|
|
this.dispatchLeave(event);
|
|
}
|
|
|
|
var target = elementFromTouchEvent(this.el,event)
|
|
if (target) {
|
|
log("found drop target " + target.tagName);
|
|
this.dispatchDrop(target, event);
|
|
} else {
|
|
log("no drop target");
|
|
}
|
|
|
|
var dragendEvt = doc.createEvent("Event");
|
|
dragendEvt.initEvent("dragend", true, true);
|
|
this.el.dispatchEvent(dragendEvt);
|
|
this.clearDragOverTimer();
|
|
},
|
|
dispatchDrop: function(target, event) {
|
|
var dropEvt = doc.createEvent("Event");
|
|
dropEvt.initEvent("drop", true, true);
|
|
|
|
var touch = event.changedTouches[0];
|
|
var x = touch[coordinateSystemForElementFromPoint + 'X'];
|
|
var y = touch[coordinateSystemForElementFromPoint + 'Y'];
|
|
|
|
var targetOffset = getOffset(target);
|
|
|
|
dropEvt.offsetX = x - targetOffset.x;
|
|
dropEvt.offsetY = y - targetOffset.y;
|
|
|
|
dropEvt.dataTransfer = {
|
|
types: this.dragDataTypes,
|
|
getData: function(type) {
|
|
return this.dragData[type];
|
|
}.bind(this),
|
|
dropEffect: "move"
|
|
};
|
|
dropEvt.preventDefault = function() {
|
|
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=14638 - if we don't cancel it, we'll snap back
|
|
}.bind(this);
|
|
|
|
once(doc, "drop", function() {
|
|
log("drop event not canceled");
|
|
},this);
|
|
|
|
target.dispatchEvent(dropEvt);
|
|
},
|
|
dispatchEnter: function(event) {
|
|
|
|
var enterEvt = doc.createEvent("Event");
|
|
enterEvt.initEvent("dragenter", true, true);
|
|
enterEvt.dataTransfer = {
|
|
types: this.dragDataTypes,
|
|
getData: function(type) {
|
|
return this.dragData[type];
|
|
}.bind(this)
|
|
};
|
|
|
|
var touch = event.changedTouches[0];
|
|
enterEvt.pageX = touch.pageX;
|
|
enterEvt.pageY = touch.pageY;
|
|
enterEvt.clientX = touch.clientX;
|
|
enterEvt.clientY = touch.clientY;
|
|
|
|
this.lastEnter.dispatchEvent(enterEvt);
|
|
},
|
|
dispatchOver: function() {
|
|
|
|
var overEvt = doc.createEvent("Event");
|
|
overEvt.initEvent("dragover", true, true);
|
|
overEvt.dataTransfer = {
|
|
types: this.dragDataTypes,
|
|
getData: function(type) {
|
|
return this.dragData[type];
|
|
}.bind(this)
|
|
};
|
|
|
|
var touch = this.lastMoveEvent.changedTouches[0];
|
|
overEvt.pageX = touch.pageX;
|
|
overEvt.pageY = touch.pageY;
|
|
overEvt.clientX = touch.clientX;
|
|
overEvt.clientY = touch.clientY;
|
|
|
|
this.lastEnter.dispatchEvent(overEvt);
|
|
},
|
|
dispatchLeave: function(event) {
|
|
|
|
var leaveEvt = doc.createEvent("Event");
|
|
leaveEvt.initEvent("dragleave", true, true);
|
|
leaveEvt.dataTransfer = {
|
|
types: this.dragDataTypes,
|
|
getData: function(type) {
|
|
return this.dragData[type];
|
|
}.bind(this)
|
|
};
|
|
|
|
var touch = event.changedTouches[0];
|
|
leaveEvt.pageX = touch.pageX;
|
|
leaveEvt.pageY = touch.pageY;
|
|
leaveEvt.clientX = touch.clientX;
|
|
leaveEvt.clientY = touch.clientY;
|
|
|
|
this.lastEnter.dispatchEvent(leaveEvt);
|
|
this.lastEnter = null;
|
|
this.clearDragOverTimer();
|
|
},
|
|
dispatchDragStart: function() {
|
|
var evt = doc.createEvent("Event");
|
|
evt.initEvent("dragstart", true, true);
|
|
evt.dataTransfer = {
|
|
setData: function(type, val) {
|
|
this.dragData[type] = val;
|
|
if (this.dragDataTypes.indexOf(type) == -1) {
|
|
this.dragDataTypes[this.dragDataTypes.length] = type;
|
|
}
|
|
return val;
|
|
}.bind(this),
|
|
setDragImage: function(el, x, y){
|
|
this.customDragImage = el;
|
|
this.customDragImageX = x
|
|
this.customDragImageY = y
|
|
}.bind(this),
|
|
dropEffect: "move"
|
|
};
|
|
return this.el.dispatchEvent(evt);
|
|
},
|
|
createDragImage: function() {
|
|
if (this.customDragImage) {
|
|
this.dragImage = this.customDragImage.cloneNode(true);
|
|
duplicateStyle(this.customDragImage, this.dragImage);
|
|
} else {
|
|
this.dragImage = this.el.cloneNode(true);
|
|
duplicateStyle(this.el, this.dragImage);
|
|
}
|
|
this.dragImage.style.opacity = "0.5";
|
|
this.dragImage.style.position = "absolute";
|
|
this.dragImage.style.left = "0px";
|
|
this.dragImage.style.top = "0px";
|
|
this.dragImage.style.zIndex = "999999";
|
|
|
|
var transform = this.dragImage.style.transform;
|
|
if (typeof transform !== "undefined") {
|
|
this.dragImageTransform = "";
|
|
if (transform != "none") {
|
|
this.dragImageTransform = transform.replace(/translate\(\D*\d+[^,]*,\D*\d+[^,]*\)\s*/g, '');
|
|
}
|
|
}
|
|
|
|
var webkitTransform = this.dragImage.style["-webkit-transform"];
|
|
if (typeof webkitTransform !== "undefined") {
|
|
this.dragImageWebKitTransform = "";
|
|
if (webkitTransform != "none") {
|
|
this.dragImageWebKitTransform = webkitTransform.replace(/translate\(\D*\d+[^,]*,\D*\d+[^,]*\)\s*/g, '');
|
|
}
|
|
}
|
|
|
|
this.translateDragImage(-9999, -9999);
|
|
|
|
doc.body.appendChild(this.dragImage);
|
|
}
|
|
};
|
|
|
|
// delayed touch start event
|
|
function touchstartDelay(delay) {
|
|
return function(evt){
|
|
var el = evt.target;
|
|
|
|
do {
|
|
if (elementIsDraggable(el)) {
|
|
var heldItem = function() {
|
|
end.off();
|
|
cancel.off();
|
|
scroll.off();
|
|
touchstart(evt);
|
|
};
|
|
|
|
var onReleasedItem = function() {
|
|
end.off();
|
|
cancel.off();
|
|
scroll.off();
|
|
clearTimeout(timer);
|
|
};
|
|
|
|
var timer = setTimeout(heldItem, delay);
|
|
|
|
var end = onEvt(el, 'touchend', onReleasedItem, this);
|
|
var cancel = onEvt(el, 'touchcancel', onReleasedItem, this);
|
|
var scroll = onEvt(window, 'scroll', onReleasedItem, this);
|
|
break;
|
|
}
|
|
} while ((el = el.parentNode) && el !== doc.body);
|
|
};
|
|
};
|
|
|
|
// event listeners
|
|
function touchstart(evt) {
|
|
var el = evt.target;
|
|
do {
|
|
if (elementIsDraggable(el)) {
|
|
handleTouchStartOnAnchor(evt, el);
|
|
|
|
evt.preventDefault();
|
|
new DragDrop(evt,el);
|
|
break;
|
|
}
|
|
} while((el = el.parentNode) && el !== doc.body);
|
|
}
|
|
|
|
function elementIsDraggable(el){
|
|
// if an element is not draggable either explicitly or implicitly we can exit immediately
|
|
if(!el.draggable) return false;
|
|
|
|
// if an element has been explicitly set to be draggable we're good to go
|
|
if(el.hasAttribute("draggable")) return true;
|
|
|
|
// otherwise we investigate the implicit option
|
|
return (!config.requireExplicitDraggable);
|
|
}
|
|
|
|
function elementIsAnchor(el){
|
|
return el.tagName.toLowerCase() == "a";
|
|
}
|
|
|
|
function handleTouchStartOnAnchor(evt, el){
|
|
// If draggable isn't explicitly set for anchors, then simulate a click event.
|
|
// Otherwise plain old vanilla links will stop working.
|
|
// https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Touch_events#Handling_clicks
|
|
if (!el.hasAttribute("draggable") && elementIsAnchor(el) && config.simulateAnchorClick) {
|
|
var clickEvt = document.createEvent("MouseEvents");
|
|
clickEvt.initMouseEvent("click", true, true, el.ownerDocument.defaultView, 1,
|
|
evt.screenX, evt.screenY, evt.clientX, evt.clientY,
|
|
evt.ctrlKey, evt.altKey, evt.shiftKey, evt.metaKey, 0, null);
|
|
el.dispatchEvent(clickEvt);
|
|
log("Simulating click to anchor");
|
|
}
|
|
}
|
|
|
|
// DOM helpers
|
|
function elementFromTouchEvent(el,event) {
|
|
var touch = event.changedTouches[0];
|
|
var target = doc.elementFromPoint(
|
|
touch[coordinateSystemForElementFromPoint + "X"],
|
|
touch[coordinateSystemForElementFromPoint + "Y"]
|
|
);
|
|
return target;
|
|
}
|
|
|
|
//calculate the offset position of an element (relative to the window, not the document)
|
|
function getOffset(el) {
|
|
var rect = el.getBoundingClientRect();
|
|
return {
|
|
"x": rect.left,
|
|
"y": rect.top
|
|
};
|
|
}
|
|
|
|
function onEvt(el, event, handler, context) {
|
|
if(context) {
|
|
handler = handler.bind(context);
|
|
}
|
|
el.addEventListener(event, handler, {passive:false});
|
|
return {
|
|
off: function() {
|
|
return el.removeEventListener(event, handler, {passive:false});
|
|
}
|
|
};
|
|
}
|
|
|
|
function once(el, event, handler, context) {
|
|
if(context) {
|
|
handler = handler.bind(context);
|
|
}
|
|
function listener(evt) {
|
|
handler(evt);
|
|
return el.removeEventListener(event,listener);
|
|
}
|
|
return el.addEventListener(event,listener);
|
|
}
|
|
|
|
// duplicateStyle expects dstNode to be a clone of srcNode
|
|
function duplicateStyle(srcNode, dstNode) {
|
|
// Is this node an element?
|
|
if (srcNode.nodeType == 1) {
|
|
// Remove any potential conflict attributes
|
|
dstNode.removeAttribute("id");
|
|
dstNode.removeAttribute("class");
|
|
dstNode.removeAttribute("style");
|
|
dstNode.removeAttribute("draggable");
|
|
|
|
// Clone the style
|
|
var cs = window.getComputedStyle(srcNode);
|
|
for (var i = 0; i < cs.length; i++) {
|
|
var csName = cs[i];
|
|
dstNode.style.setProperty(csName, cs.getPropertyValue(csName), cs.getPropertyPriority(csName));
|
|
}
|
|
|
|
// Pointer events as none makes the drag image transparent to document.elementFromPoint()
|
|
dstNode.style.pointerEvents = "none";
|
|
}
|
|
|
|
// Do the same for the children
|
|
if (srcNode.hasChildNodes()) {
|
|
for (var j = 0; j < srcNode.childNodes.length; j++) {
|
|
duplicateStyle(srcNode.childNodes[j], dstNode.childNodes[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// general helpers
|
|
function log(msg) {
|
|
console.log(msg);
|
|
}
|
|
|
|
function average(arr) {
|
|
if (arr.length === 0) return 0;
|
|
return arr.reduce((function(s, v) {
|
|
return v + s;
|
|
}), 0) / arr.length;
|
|
}
|
|
|
|
function noop() {}
|
|
|
|
main();
|
|
|
|
};
|
|
|
|
if (typeof module === 'object' && typeof module.exports === 'object') {
|
|
module.exports = _exposeIosHtml5DragDropShim;
|
|
} else if (typeof window !== 'undefined') {
|
|
_exposeIosHtml5DragDropShim(window.iosDragDropShim);
|
|
}
|
|
})(document); |