1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-27 03:57:21 +00:00

Improve support for drag and drop

Documentation TBD
This commit is contained in:
Jermolene 2017-03-19 19:33:56 +00:00
parent c2391c5250
commit eba1c3c160
10 changed files with 510 additions and 79 deletions

View File

@ -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;
};
})();

View File

@ -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<importDataTypes.length; t++) {
if(!$tw.browser.isIE || importDataTypes[t].IECompatible) {
// Get the data
var dataType = importDataTypes[t];
var data = dataTransfer.getData(dataType.type);
// Import the tiddlers in the data
if(data !== "" && data !== null) {
if($tw.log.IMPORT) {
console.log("Importing data type '" + dataType.type + "', data: '" + data + "'")
}
var tiddlerFields = dataType.toTiddlerFieldsArray(data,fallbackTitle);
callback(tiddlerFields);
return;
}
}
}
};
var importDataTypes = [
{type: "text/vnd.tiddler", IECompatible: false, toTiddlerFieldsArray: function(data,fallbackTitle) {
return parseJSONTiddlers(data,fallbackTitle);
}},
{type: "URL", IECompatible: true, toTiddlerFieldsArray: function(data,fallbackTitle) {
// Check for tiddler data URI
var match = decodeURIComponent(data).match(/^data\:text\/vnd\.tiddler,(.*)/i);
if(match) {
return parseJSONTiddlers(match[1],fallbackTitle);
} else {
return { // As URL string
text: data
};
}
}},
{type: "text/x-moz-url", IECompatible: false, toTiddlerFieldsArray: function(data,fallbackTitle) {
// Check for tiddler data URI
var match = decodeURIComponent(data).match(/^data\:text\/vnd\.tiddler,(.*)/i);
if(match) {
return parseJSONTiddlers(match[1],fallbackTitle);
} else {
return { // As URL string
text: data
};
}
}},
{type: "text/html", IECompatible: false, toTiddlerFieldsArray: function(data,fallbackTitle) {
return {
text: data
};
}},
{type: "text/plain", IECompatible: false, toTiddlerFieldsArray: function(data,fallbackTitle) {
return {
text: data
};
}},
{type: "Text", IECompatible: true, toTiddlerFieldsArray: function(data,fallbackTitle) {
return {
text: data
};
}},
{type: "text/uri-list", IECompatible: false, toTiddlerFieldsArray: function(data,fallbackTitle) {
return {
text: data
};
}}
];
function parseJSONTiddlers(json,fallbackTitle) {
var data = JSON.parse(json);
if(!$tw.utils.isArray(data)) {
data = [data];
}
data.forEach(function(fields) {
fields.title = fields.title || fallbackTitle;
});
return data;
};
})();

View File

@ -0,0 +1,160 @@
/*\
title: $:/core/modules/widgets/draggable.js
type: application/javascript
module-type: widget
Draggable widget
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var DraggableWidget = function(parseTreeNode,options) {
this.initialise(parseTreeNode,options);
};
/*
Inherit from the base widget class
*/
DraggableWidget.prototype = new Widget();
/*
Render this widget into the DOM
*/
DraggableWidget.prototype.render = function(parent,nextSibling) {
var self = this;
// Save the parent dom node
this.parentDomNode = parent;
// Compute our attributes
this.computeAttributes();
// Execute our logic
this.execute();
// Sanitise the specified tag
var tag = this.draggableTag;
if($tw.config.htmlUnsafeElements.indexOf(tag) !== -1) {
tag = "div";
}
// Create our element
var domNode = this.document.createElement(tag);
// Assign classes
var classes = ["tc-draggable"];
if(this.draggableClasses) {
classes.push(this.draggableClasses);
}
domNode.setAttribute("class",classes.join(" "));
domNode.setAttribute("draggable","true");
// Add event handlers
$tw.utils.addEventListeners(domNode,[
{name: "dragstart", handlerObject: this, handlerMethod: "handleDragStartEvent"},
{name: "dragend", handlerObject: this, handlerMethod: "handleDragEndEvent"}
]);
// Insert the link into the DOM and render any children
parent.insertBefore(domNode,nextSibling);
this.renderChildren(domNode,null);
this.domNodes.push(domNode);
};
DraggableWidget.prototype.handleDragStartEvent = function(event) {
var self = this;
// Collect the tiddlers
var titles = [];
if(this.draggableTiddler) {
titles.push(this.draggableTiddler);
}
if(this.draggableFilter) {
titles.push.apply(titles,this.wiki.filterTiddlers(this.draggableFilter,this));
}
var titleString = titles.join("\n");
if(titles.length > 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;
})();

View File

@ -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;
})();

View File

@ -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<this.importDataTypes.length; t++) {
if(!$tw.browser.isIE || this.importDataTypes[t].IECompatible) {
// Get the data
var dataType = this.importDataTypes[t];
var data = dataTransfer.getData(dataType.type);
// Import the tiddlers in the data
if(data !== "" && data !== null) {
if($tw.log.IMPORT) {
console.log("Importing data type '" + dataType.type + "', data: '" + data + "'")
}
var tiddlerFields = dataType.convertToFields(data);
if(!tiddlerFields.title) {
tiddlerFields.title = this.wiki.generateNewTitle("Untitled");
}
this.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify([tiddlerFields])});
return;
}
}
}
};
DropZoneWidget.prototype.importDataTypes = [
{type: "text/vnd.tiddler", IECompatible: false, convertToFields: function(data) {
return JSON.parse(data);
}},
{type: "URL", IECompatible: true, convertToFields: function(data) {
// Check for tiddler data URI
var match = decodeURIComponent(data).match(/^data\:text\/vnd\.tiddler,(.*)/i);
if(match) {
return JSON.parse(match[1]);
} else {
return { // As URL string
text: data
};
}
}},
{type: "text/x-moz-url", IECompatible: false, convertToFields: function(data) {
// Check for tiddler data URI
var match = decodeURIComponent(data).match(/^data\:text\/vnd\.tiddler,(.*)/i);
if(match) {
return JSON.parse(match[1]);
} else {
return { // As URL string
text: data
};
}
}},
{type: "text/html", IECompatible: false, convertToFields: function(data) {
return {
text: data
};
}},
{type: "text/plain", IECompatible: false, convertToFields: function(data) {
return {
text: data
};
}},
{type: "Text", IECompatible: true, convertToFields: function(data) {
return {
text: data
};
}},
{type: "text/uri-list", IECompatible: false, convertToFields: function(data) {
return {
text: data
};
}}
];
DropZoneWidget.prototype.handlePasteEvent = function(event) {
// Let the browser handle it if we're in a textarea or input box
if(["TEXTAREA","INPUT"].indexOf(event.target.tagName) == -1) {

View File

@ -145,7 +145,7 @@ LinkWidget.prototype.handleClickEvent = function(event) {
LinkWidget.prototype.handleDragStartEvent = function(event) {
if(event.target === this.domNodes[0]) {
if(this.to) {
$tw.dragInProgress = true;
$tw.dragInProgress = this.domNodes[0];
// Set the dragging class on the element being dragged
$tw.utils.addClass(event.target,"tc-tiddlylink-dragging");
// Create the drag image elements
@ -185,7 +185,7 @@ LinkWidget.prototype.handleDragStartEvent = function(event) {
LinkWidget.prototype.handleDragEndEvent = function(event) {
if(event.target === this.domNodes[0]) {
$tw.dragInProgress = false;
$tw.dragInProgress = null;
// Remove the dragging class on the element being dragged
$tw.utils.removeClass(event.target,"tc-tiddlylink-dragging");
// Delete the drag image element

View File

@ -512,7 +512,7 @@ Widget.prototype.invokeActions = function(triggeringWidget,event) {
/*
Invoke the action widgets defined in a string
*/
Widget.prototype.invokeActionString = function(actions,triggeringWidget,event) {
Widget.prototype.invokeActionString = function(actions,triggeringWidget,event,variables) {
actions = actions || "";
var parser = this.wiki.parseText("text/vnd.tiddlywiki",actions,{
parentWidget: this,
@ -520,7 +520,8 @@ Widget.prototype.invokeActionString = function(actions,triggeringWidget,event) {
}),
widgetNode = this.wiki.makeWidget(parser,{
parentWidget: this,
document: this.document
document: this.document,
variables: variables
});
var container = this.document.createElement("div");
widgetNode.render(container,null);

View File

@ -3,10 +3,22 @@ tags: $:/tags/SideBar
caption: {{$:/language/SideBar/Open/Caption}}
\define lingo-base() $:/language/CloseAll/
\define drop-actions()
<$action-listops $tiddler="$:/StoryList" $subfilter="+[insertbefore:currentTiddler<actionTiddler>]"/>
\end
<$list filter="[list[$:/StoryList]]" history="$:/HistoryList" storyview="pop">
<div style="position: relative;">
<$droppable actions=<<drop-actions>>>
<div class="tc-droppable-placeholder">
&nbsp;
</div>
<div>
<$button message="tm-close-tiddler" tooltip={{$:/language/Buttons/Close/Hint}} aria-label={{$:/language/Buttons/Close/Caption}} class="tc-btn-invisible tc-btn-mini">&times;</$button> <$link to={{!!title}}><$view field="title"/></$link>
</div>
</$droppable>
</div>
</$list>
<$button message="tm-close-all-tiddlers" class="tc-btn-invisible tc-btn-mini"><<lingo Button>></$button>

View File

@ -1,6 +1,24 @@
title: $:/core/ui/TagTemplate
\define drop-actions()
<!-- Save the current ordering of the tiddlers with this tag -->
<$set name="order" filter="[<tag>tagging[]]">
<!-- Remove any list-after or list-before fields from the tiddlers with this tag -->
<$list filter="[<tag>tagging[]]">
<$action-deletefield $field="list-before"/>
<$action-deletefield $field="list-after"/>
</$list>
<!-- Assign the list field of the tag with the current ordering -->
<$action-setfield $tiddler=<<tag>> $field="list" $value=<<order>>/>
<!-- Add the newly inserted item to the list -->
<$action-listops $tiddler=<<tag>> $field="list" $subfilter="+[insertbefore:currentTiddler<actionTiddler>]"/>
<!-- Make sure the newly added item has the right tag -->
<$action-listops $tiddler=<<actionTiddler>> $field="tags" $subfilter="+[<tag>]"/>
</$set>
\end
<span class="tc-tag-list-item">
<$draggable tag="span" filter="[all[current]tagging[]]">
<$set name="transclusion" value=<<currentTiddler>>>
<$macrocall $name="tag-pill-body" tag=<<currentTiddler>> icon={{!!icon}} colour={{!!color}} palette={{$:/palette}} element-tag="""$button""" element-attributes="""popup=<<qualify "$:/state/popup/tag">>"""/>
<$reveal state=<<qualify "$:/state/popup/tag">> type="popup" position="below" animate="yes" class="tc-drop-down">
@ -9,7 +27,23 @@ title: $:/core/ui/TagTemplate
<$transclude tiddler=<<listItem>>/>
</$list>
<hr>
<$list filter="[all[current]tagging[]]" template="$:/core/ui/ListItemTemplate"/>
<$set name="tag" value=<<currentTiddler>>>
<$list filter="[all[current]tagging[]]">
<div class="tc-menu-list-item" style="position: relative;">
<$droppable actions=<<drop-actions>>>
<div class="tc-droppable-placeholder">
&nbsp;
</div>
<div>
<$link to={{!!title}}>
<$view field="title"/>
</$link>
</div>
</$droppable>
</div>
</$list>
</$set>
</$reveal>
</$set>
</$draggable>
</span>

View File

@ -312,6 +312,19 @@ a.tc-tiddlylink-external:hover {
content: "<<lingo DropMessage>>";
}
.tc-droppable .tc-droppable-placeholder {
display: none;
}
.tc-droppable.tc-dragover .tc-droppable-placeholder {
display: block;
border: 2px dashed <<colour dropzone-background>>;
}
.tc-draggable {
cursor: move;
}
/*
** Plugin reload warning
*/