From eba1c3c160c63ac3dfc15f55df086971a832d657 Mon Sep 17 00:00:00 2001 From: Jermolene Date: Sun, 19 Mar 2017 19:33:56 +0000 Subject: [PATCH] Improve support for drag and drop Documentation TBD --- core/modules/filters/insertbefore.js | 41 +++++++ core/modules/utils/dom/datatransfer.js | 94 +++++++++++++++ core/modules/widgets/draggable.js | 160 +++++++++++++++++++++++++ core/modules/widgets/droppable.js | 144 ++++++++++++++++++++++ core/modules/widgets/dropzone.js | 76 +----------- core/modules/widgets/link.js | 4 +- core/modules/widgets/widget.js | 5 +- core/ui/SideBar/Open.tid | 16 ++- core/ui/TagTemplate.tid | 36 +++++- themes/tiddlywiki/vanilla/base.tid | 13 ++ 10 files changed, 510 insertions(+), 79 deletions(-) create mode 100644 core/modules/filters/insertbefore.js create mode 100644 core/modules/utils/dom/datatransfer.js create mode 100644 core/modules/widgets/draggable.js create mode 100644 core/modules/widgets/droppable.js diff --git a/core/modules/filters/insertbefore.js b/core/modules/filters/insertbefore.js new file mode 100644 index 000000000..ed391c297 --- /dev/null +++ b/core/modules/filters/insertbefore.js @@ -0,0 +1,41 @@ +/*\ +title: $:/core/modules/filters/insertbefore.js +type: application/javascript +module-type: filteroperator + +Insert an item before another item in a list + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Order a list +*/ +exports.insertbefore = function(source,operator,options) { + var results = []; + source(function(tiddler,title) { + results.push(title); + }); + var target = options.widget && options.widget.getVariable(operator.suffix || "currentTiddler"); + if(target && target !== operator.operand) { + // Remove the entry from the list if it is present + var pos = results.indexOf(operator.operand); + if(pos !== -1) { + results.splice(pos,1); + } + // Insert the entry before the target marker + pos = results.indexOf(target); + if(pos !== -1) { + results.splice(pos,0,operator.operand); + } else { + results.push(operator.operand); + } + } + return results; +}; + +})(); diff --git a/core/modules/utils/dom/datatransfer.js b/core/modules/utils/dom/datatransfer.js new file mode 100644 index 000000000..75ee410f2 --- /dev/null +++ b/core/modules/utils/dom/datatransfer.js @@ -0,0 +1,94 @@ +/*\ +title: $:/core/modules/utils/dom/datatransfer.js +type: application/javascript +module-type: utils + +Browser data transfer utilities, used with the clipboard and drag and drop + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.importDataTransfer = function(dataTransfer,fallbackTitle,callback) { + // Try each provided data type in turn + for(var t=0; t 0 && event.target === this.domNodes[0]) { + $tw.dragInProgress = this.domNodes[0]; + // Set the dragging class on the element being dragged + $tw.utils.addClass(event.target,"tc-dragging"); + // Create the drag image elements + this.dragImage = this.document.createElement("div"); + this.dragImage.className = "tc-tiddler-dragger"; + var inner = this.document.createElement("div"); + inner.className = "tc-tiddler-dragger-inner"; + inner.appendChild(this.document.createTextNode( + titles.length === 1 ? + titles[0] : + titles.length + " tiddlers" + )); + this.dragImage.appendChild(inner); + this.document.body.appendChild(this.dragImage); + // Set the data transfer properties + var dataTransfer = event.dataTransfer; + // Set up the image + dataTransfer.effectAllowed = "copy"; + if(dataTransfer.setDragImage) { + dataTransfer.setDragImage(this.dragImage.firstChild,-16,-16); + } + // Set up the data transfer + dataTransfer.clearData(); + var jsonData = []; + if(titles.length > 1) { + titles.forEach(function(title) { + jsonData.push(self.wiki.getTiddlerAsJson(title)); + }); + jsonData = "[" + jsonData.join(",") + "]"; + } else { + jsonData = this.wiki.getTiddlerAsJson(titles[0]); + } + // IE doesn't like these content types + if(!$tw.browser.isIE) { + dataTransfer.setData("text/vnd.tiddler",jsonData); + dataTransfer.setData("text/plain",titleString); + dataTransfer.setData("text/x-moz-url","data:text/vnd.tiddler," + encodeURIComponent(jsonData)); + } + dataTransfer.setData("URL","data:text/vnd.tiddler," + encodeURIComponent(jsonData)); + dataTransfer.setData("Text",titleString); + event.stopPropagation(); + } else { + event.preventDefault(); + } +}; + +DraggableWidget.prototype.handleDragEndEvent = function(event) { + if(event.target === this.domNodes[0]) { + $tw.dragInProgress = null; + // Remove the dragging class on the element being dragged + $tw.utils.removeClass(event.target,"tc-dragging"); + // Delete the drag image element + if(this.dragImage) { + this.dragImage.parentNode.removeChild(this.dragImage); + } + } +}; + +/* +Compute the internal state of the widget +*/ +DraggableWidget.prototype.execute = function() { + // Pick up our attributes + this.draggableTiddler = this.getAttribute("tiddler"); + this.draggableFilter = this.getAttribute("filter"); + this.draggableTag = this.getAttribute("tag","div"); + this.draggableClasses = this.getAttribute("class"); + // Make the child widgets + this.makeChildWidgets(); +}; + +/* +Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering +*/ +DraggableWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(changedAttributes.tiddler || changedTiddlers.tag || changedTiddlers["class"]) { + this.refreshSelf(); + return true; + } + return this.refreshChildren(changedTiddlers); +}; + +exports.draggable = DraggableWidget; + +})(); diff --git a/core/modules/widgets/droppable.js b/core/modules/widgets/droppable.js new file mode 100644 index 000000000..1dacac5f2 --- /dev/null +++ b/core/modules/widgets/droppable.js @@ -0,0 +1,144 @@ +/*\ +title: $:/core/modules/widgets/droppable.js +type: application/javascript +module-type: widget + +Droppable widget + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget; + +var DroppableWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +/* +Inherit from the base widget class +*/ +DroppableWidget.prototype = new Widget(); + +/* +Render this widget into the DOM +*/ +DroppableWidget.prototype.render = function(parent,nextSibling) { + var self = this; + // Remember parent + this.parentDomNode = parent; + // Compute attributes and execute state + this.computeAttributes(); + this.execute(); + // Create element + var domNode = this.document.createElement("div"); + domNode.className = "tc-droppable"; + // Add event handlers + $tw.utils.addEventListeners(domNode,[ + {name: "dragenter", handlerObject: this, handlerMethod: "handleDragEnterEvent"}, + {name: "dragover", handlerObject: this, handlerMethod: "handleDragOverEvent"}, + {name: "dragleave", handlerObject: this, handlerMethod: "handleDragLeaveEvent"}, + {name: "drop", handlerObject: this, handlerMethod: "handleDropEvent"} + ]); + // Insert element + parent.insertBefore(domNode,nextSibling); + this.renderChildren(domNode,null); + this.domNodes.push(domNode); + // Stack of outstanding enter/leave events + this.currentlyEntered = []; +}; + +DroppableWidget.prototype.enterDrag = function(event) { + if(this.currentlyEntered.indexOf(event.target) === -1) { + this.currentlyEntered.push(event.target); + } + // If we're entering for the first time we need to apply highlighting + $tw.utils.addClass(this.domNodes[0],"tc-dragover"); +}; + +DroppableWidget.prototype.leaveDrag = function(event) { + var pos = this.currentlyEntered.indexOf(event.target); + if(pos !== -1) { + this.currentlyEntered.splice(pos,1); + } + // Remove highlighting if we're leaving externally. The hacky second condition is to resolve a problem with Firefox whereby there is an erroneous dragenter event if the node being dragged is within the dropzone + if(this.currentlyEntered.length === 0 || (this.currentlyEntered.length === 1 && this.currentlyEntered[0] === $tw.dragInProgress)) { + this.currentlyEntered = []; + $tw.utils.removeClass(this.domNodes[0],"tc-dragover"); + } +}; + +DroppableWidget.prototype.handleDragEnterEvent = function(event) { + this.enterDrag(event); + // Tell the browser that we're ready to handle the drop + event.preventDefault(); + // Tell the browser not to ripple the drag up to any parent drop handlers + event.stopPropagation(); +}; + +DroppableWidget.prototype.handleDragOverEvent = function(event) { + // Check for being over a TEXTAREA or INPUT + if(["TEXTAREA","INPUT"].indexOf(event.target.tagName) !== -1) { + return false; + } + // Tell the browser that we're still interested in the drop + event.preventDefault(); + event.dataTransfer.dropEffect = "copy"; // Explicitly show this is a copy +}; + +DroppableWidget.prototype.handleDragLeaveEvent = function(event) { + this.leaveDrag(event); +}; + +DroppableWidget.prototype.handleDropEvent = function(event) { + var self = this; + this.leaveDrag(event); + // Check for being over a TEXTAREA or INPUT + if(["TEXTAREA","INPUT"].indexOf(event.target.tagName) !== -1) { + return false; + } + var dataTransfer = event.dataTransfer; + // Remove highlighting + $tw.utils.removeClass(this.domNodes[0],"tc-dragover"); + // Try to import the various data types we understand + $tw.utils.importDataTransfer(dataTransfer,null,function(fieldsArray) { + fieldsArray.forEach(function(fields) { + if(fields.title) { + self.performActions(fields.title,event); + } + }); + }); + // Tell the browser that we handled the drop + event.preventDefault(); + // Stop the drop ripple up to any parent handlers + event.stopPropagation(); +}; + +DroppableWidget.prototype.performActions = function(title,event) { + if(this.dropzoneActions) { + this.invokeActionString(this.dropzoneActions,this,event,{actionTiddler: title}); + } +}; + +/* +Compute the internal state of the widget +*/ +DroppableWidget.prototype.execute = function() { + this.dropzoneActions = this.getAttribute("actions"); + // Make child widgets + this.makeChildWidgets(); +}; + +/* +Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering +*/ +DroppableWidget.prototype.refresh = function(changedTiddlers) { + return this.refreshChildren(changedTiddlers); +}; + +exports.droppable = DroppableWidget; + +})(); diff --git a/core/modules/widgets/dropzone.js b/core/modules/widgets/dropzone.js index 48c1b0368..d77d86393 100644 --- a/core/modules/widgets/dropzone.js +++ b/core/modules/widgets/dropzone.js @@ -104,6 +104,7 @@ DropZoneWidget.prototype.handleDragLeaveEvent = function(event) { }; DropZoneWidget.prototype.handleDropEvent = function(event) { + var self = this; this.leaveDrag(event); // Check for being over a TEXTAREA or INPUT if(["TEXTAREA","INPUT"].indexOf(event.target.tagName) !== -1) { @@ -123,7 +124,9 @@ DropZoneWidget.prototype.handleDropEvent = function(event) { }); // Try to import the various data types we understand if(numFiles === 0) { - this.importData(dataTransfer); + $tw.utils.importDataTransfer(dataTransfer,this.wiki.generateNewTitle("Untitled"),function(fieldsArray) { + self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(fieldsArray)}); + }); } // Tell the browser that we handled the drop event.preventDefault(); @@ -131,77 +134,6 @@ DropZoneWidget.prototype.handleDropEvent = function(event) { event.stopPropagation(); }; -DropZoneWidget.prototype.importData = function(dataTransfer) { - // Try each provided data type in turn - for(var t=0; t +\end + <$list filter="[list[$:/StoryList]]" history="$:/HistoryList" storyview="pop"> - +
+<$droppable actions=<>> +
+  +
+
<$button message="tm-close-tiddler" tooltip={{$:/language/Buttons/Close/Hint}} aria-label={{$:/language/Buttons/Close/Caption}} class="tc-btn-invisible tc-btn-mini">× <$link to={{!!title}}><$view field="title"/> - +
+ +
<$button message="tm-close-all-tiddlers" class="tc-btn-invisible tc-btn-mini"><> diff --git a/core/ui/TagTemplate.tid b/core/ui/TagTemplate.tid index 0280df4d6..4bdc9b378 100644 --- a/core/ui/TagTemplate.tid +++ b/core/ui/TagTemplate.tid @@ -1,6 +1,24 @@ title: $:/core/ui/TagTemplate +\define drop-actions() + +<$set name="order" filter="[tagging[]]"> + +<$list filter="[tagging[]]"> +<$action-deletefield $field="list-before"/> +<$action-deletefield $field="list-after"/> + + +<$action-setfield $tiddler=<> $field="list" $value=<>/> + +<$action-listops $tiddler=<> $field="list" $subfilter="+[insertbefore:currentTiddler]"/> + +<$action-listops $tiddler=<> $field="tags" $subfilter="+[]"/> + +\end + +<$draggable tag="span" filter="[all[current]tagging[]]"> <$set name="transclusion" value=<>> <$macrocall $name="tag-pill-body" tag=<> icon={{!!icon}} colour={{!!color}} palette={{$:/palette}} element-tag="""$button""" element-attributes="""popup=<>"""/> <$reveal state=<> type="popup" position="below" animate="yes" class="tc-drop-down"> @@ -9,7 +27,23 @@ title: $:/core/ui/TagTemplate <$transclude tiddler=<>/>
-<$list filter="[all[current]tagging[]]" template="$:/core/ui/ListItemTemplate"/> +<$set name="tag" value=<>> +<$list filter="[all[current]tagging[]]"> +
+<$droppable actions=<>> +
+  +
+
+<$link to={{!!title}}> +<$view field="title"/> + +
+ +
+ + +
diff --git a/themes/tiddlywiki/vanilla/base.tid b/themes/tiddlywiki/vanilla/base.tid index 2e4a9ca4d..19f80688c 100644 --- a/themes/tiddlywiki/vanilla/base.tid +++ b/themes/tiddlywiki/vanilla/base.tid @@ -312,6 +312,19 @@ a.tc-tiddlylink-external:hover { content: "<>"; } +.tc-droppable .tc-droppable-placeholder { + display: none; +} + +.tc-droppable.tc-dragover .tc-droppable-placeholder { + display: block; + border: 2px dashed <>; +} + +.tc-draggable { + cursor: move; +} + /* ** Plugin reload warning */