This commit is contained in:
Simon Huber 2026-01-19 14:52:07 +00:00 committed by GitHub
commit 09b7fe3b4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 553 additions and 132 deletions

View file

@ -193,6 +193,7 @@ Subscript/Caption: subscript
Subscript/Hint: Apply subscript formatting to selection
Superscript/Caption: superscript
Superscript/Hint: Apply superscript formatting to selection
ToggleDragnDrop/Hint: Toggle drag-and-drop of the tiddlers in the story river
ToggleSidebar/Hint: Toggle the sidebar visibility
Transcludify/Caption: transclusion
Transcludify/Hint: Wrap selection in curly brackets

View file

@ -27,122 +27,127 @@ exports.makeDraggable = function(options) {
if(!options.selector && ((domNode.tagName || "").toLowerCase() !== "a")) {
domNode.setAttribute("draggable","true");
}
// Add event handlers
$tw.utils.addEventListeners(domNode,[
{name: "dragstart", handlerFunction: function(event) {
if(event.dataTransfer === undefined) {
return false;
var dragStartHandler = function(event) {
if(event.dataTransfer === undefined) {
return false;
}
// Collect the tiddlers being dragged
var dragTiddler = options.dragTiddlerFn && options.dragTiddlerFn(),
dragFilter = options.dragFilterFn && options.dragFilterFn(),
titles = dragTiddler ? [dragTiddler] : [],
startActions = options.startActions,
variables;
if(dragFilter) {
titles.push.apply(titles,options.widget.wiki.filterTiddlers(dragFilter,options.widget));
}
var titleString = $tw.utils.stringifyList(titles);
// Check that we've something to drag
if(titles.length > 0 && (options.selector && $tw.utils.domMatchesSelector(event.target,options.selector) || event.target === domNode)) {
// Mark the drag in progress
$tw.dragInProgress = domNode;
// Set the dragging class on the element being dragged
$tw.utils.addClass(domNode,"tc-dragging");
// Invoke drag-start actions if given
if(startActions !== undefined) {
// Collect our variables
variables = $tw.utils.collectDOMVariables(domNode,null,event);
variables.modifier = $tw.keyboardManager.getEventModifierKeyDescriptor(event);
variables["actionTiddler"] = titleString;
options.widget.invokeActionString(startActions,options.widget,event,variables);
}
// Create the drag image elements
dragImage = options.widget.document.createElement("div");
dragImage.className = "tc-tiddler-dragger";
var inner = options.widget.document.createElement("div");
inner.className = "tc-tiddler-dragger-inner";
inner.appendChild(options.widget.document.createTextNode(
titles.length === 1 ?
titles[0] :
titles.length + " tiddlers"
));
dragImage.appendChild(inner);
options.widget.document.body.appendChild(dragImage);
// Set the data transfer properties
var dataTransfer = event.dataTransfer;
// Set up the image
dataTransfer.effectAllowed = "all";
if(dataTransfer.setDragImage) {
if(dragImageType === "pill") {
dataTransfer.setDragImage(dragImage.firstChild,-16,-16);
} else if(dragImageType === "blank") {
dragImage.removeChild(dragImage.firstChild);
dataTransfer.setDragImage(dragImage,0,0);
} else {
var r = domNode.getBoundingClientRect();
dataTransfer.setDragImage(domNode,event.clientX-r.left,event.clientY-r.top);
}
}
// Set up the data transfer
if(dataTransfer.clearData) {
dataTransfer.clearData();
}
var jsonData = [];
if(titles.length > 1) {
titles.forEach(function(title) {
jsonData.push(options.widget.wiki.getTiddlerAsJson(title));
});
jsonData = "[" + jsonData.join(",") + "]";
} else {
jsonData = options.widget.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));
}
// If browser is Chrome-like and has a touch-input device do NOT .setData
if(!($tw.browser.isMobileChrome)) {
dataTransfer.setData("URL","data:text/vnd.tiddler," + encodeURIComponent(jsonData));
}
dataTransfer.setData("Text",titleString);
event.stopPropagation();
}
return false;
};
var dragEndHandler = function(event) {
if((options.selector && $tw.utils.domMatchesSelector(event.target,options.selector)) || event.target === domNode) {
// Collect the tiddlers being dragged
var dragTiddler = options.dragTiddlerFn && options.dragTiddlerFn(),
dragFilter = options.dragFilterFn && options.dragFilterFn(),
titles = dragTiddler ? [dragTiddler] : [],
startActions = options.startActions,
variables,
domNodeRect;
endActions = options.endActions,
variables;
if(dragFilter) {
titles.push.apply(titles,options.widget.wiki.filterTiddlers(dragFilter,options.widget));
}
var titleString = $tw.utils.stringifyList(titles);
// Check that we've something to drag
if(titles.length > 0 && (options.selector && $tw.utils.domMatchesSelector(event.target,options.selector) || event.target === domNode)) {
// Mark the drag in progress
$tw.dragInProgress = domNode;
// Set the dragging class on the element being dragged
$tw.utils.addClass(domNode,"tc-dragging");
// Invoke drag-start actions if given
if(startActions !== undefined) {
// Collect our variables
variables = $tw.utils.collectDOMVariables(domNode,null,event);
variables.modifier = $tw.keyboardManager.getEventModifierKeyDescriptor(event);
variables["actionTiddler"] = titleString;
options.widget.invokeActionString(startActions,options.widget,event,variables);
}
// Create the drag image elements
dragImage = options.widget.document.createElement("div");
dragImage.className = "tc-tiddler-dragger";
var inner = options.widget.document.createElement("div");
inner.className = "tc-tiddler-dragger-inner";
inner.appendChild(options.widget.document.createTextNode(
titles.length === 1 ?
titles[0] :
titles.length + " tiddlers"
));
dragImage.appendChild(inner);
options.widget.document.body.appendChild(dragImage);
// Set the data transfer properties
var dataTransfer = event.dataTransfer;
// Set up the image
dataTransfer.effectAllowed = "all";
if(dataTransfer.setDragImage) {
if(dragImageType === "pill") {
dataTransfer.setDragImage(dragImage.firstChild,-16,-16);
} else if(dragImageType === "blank") {
dragImage.removeChild(dragImage.firstChild);
dataTransfer.setDragImage(dragImage,0,0);
} else {
var r = domNode.getBoundingClientRect();
dataTransfer.setDragImage(domNode,event.clientX-r.left,event.clientY-r.top);
}
}
// Set up the data transfer
if(dataTransfer.clearData) {
dataTransfer.clearData();
}
var jsonData = [];
if(titles.length > 1) {
titles.forEach(function(title) {
jsonData.push(options.widget.wiki.getTiddlerAsJson(title));
});
jsonData = "[" + jsonData.join(",") + "]";
} else {
jsonData = options.widget.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));
}
// If browser is Chrome-like and has a touch-input device do NOT .setData
if(!($tw.browser.isMobileChrome)) {
dataTransfer.setData("URL","data:text/vnd.tiddler," + encodeURIComponent(jsonData));
}
dataTransfer.setData("Text",titleString);
event.stopPropagation();
$tw.dragInProgress = null;
// Invoke drag-end actions if given
if(endActions !== undefined) {
variables = $tw.utils.collectDOMVariables(domNode,null,event);
variables.modifier = $tw.keyboardManager.getEventModifierKeyDescriptor(event);
variables["actionTiddler"] = titleString;
options.widget.invokeActionString(endActions,options.widget,event,variables);
}
return false;
}},
{name: "dragend", handlerFunction: function(event) {
if((options.selector && $tw.utils.domMatchesSelector(event.target,options.selector)) || event.target === domNode) {
// Collect the tiddlers being dragged
var dragTiddler = options.dragTiddlerFn && options.dragTiddlerFn(),
dragFilter = options.dragFilterFn && options.dragFilterFn(),
titles = dragTiddler ? [dragTiddler] : [],
endActions = options.endActions,
variables;
if(dragFilter) {
titles.push.apply(titles,options.widget.wiki.filterTiddlers(dragFilter,options.widget));
}
var titleString = $tw.utils.stringifyList(titles);
$tw.dragInProgress = null;
// Invoke drag-end actions if given
if(endActions !== undefined) {
variables = $tw.utils.collectDOMVariables(domNode,null,event);
variables.modifier = $tw.keyboardManager.getEventModifierKeyDescriptor(event);
variables["actionTiddler"] = titleString;
options.widget.invokeActionString(endActions,options.widget,event,variables);
}
// Remove the dragging class on the element being dragged
$tw.utils.removeClass(domNode,"tc-dragging");
// Delete the drag image element
if(dragImage) {
dragImage.parentNode.removeChild(dragImage);
dragImage = null;
}
// Remove the dragging class on the element being dragged
$tw.utils.removeClass(domNode,"tc-dragging");
// Delete the drag image element
if(dragImage) {
dragImage.parentNode.removeChild(dragImage);
dragImage = null;
}
return false;
}}
}
return false;
};
// Add event handlers
options.widget.dragStartListenerReference = dragStartHandler;
options.widget.dragEndListenerReference = dragEndHandler;
$tw.utils.addEventListeners(domNode,[
{name: "dragstart", handlerFunction: dragStartHandler},
{name: "dragend", handlerFunction: dragEndHandler}
]);
};

View file

@ -57,6 +57,12 @@ DraggableWidget.prototype.render = function(parent,nextSibling) {
// Insert the node into the DOM and render any children
parent.insertBefore(domNode,nextSibling);
this.renderChildren(domNode,null);
this.makeDraggable(domNode);
this.domNodes.push(domNode);
};
DraggableWidget.prototype.makeDraggable = function(domNode) {
var self = this;
// Add event handlers
if(this.dragEnable) {
$tw.utils.makeDraggable({
@ -69,8 +75,11 @@ DraggableWidget.prototype.render = function(parent,nextSibling) {
widget: this,
selector: self.dragHandleSelector
});
} else if(this.dragStartListenerReference && this.dragEndListenerReference) {
domNode.removeEventListener("dragstart",this.dragStartListenerReference,false);
domNode.removeEventListener("dragend",this.dragEndListenerReference,false);
domNode.removeAttribute("draggable");
}
this.domNodes.push(domNode);
};
/*
@ -111,10 +120,17 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
DraggableWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.tag || changedAttributes.selector || changedAttributes.dragimagetype || changedAttributes.enable || changedAttributes.startactions || changedAttributes.endactions) {
if(changedAttributes.tag || changedAttributes.selector || changedAttributes.dragimagetype || changedAttributes.startactions || changedAttributes.endactions) {
this.refreshSelf();
return true;
} else {
this.dragEnable = this.getAttribute("enable","yes") === "yes";
this.makeDraggable(this.domNodes[0]);
if(!this.dragHandleSelector && this.dragEnable && (this.domNodes[0].classList && !this.domNodes[0].classList.contains("tc-draggable"))) {
this.domNodes[0].classList.add("tc-draggable");
} else if(!this.dragHandleSelector && !this.dragEnable && (this.domNodes[0].classList && this.domNodes[0].classList.contains("tc-draggable"))) {
this.domNodes[0].classList.remove("tc-draggable");
}
if(changedAttributes["class"]) {
this.updateDomNodeClasses();
}

View file

@ -50,7 +50,8 @@ DroppableWidget.prototype.render = function(parent,nextSibling) {
{name: "dragenter", handlerObject: this, handlerMethod: "handleDragEnterEvent"},
{name: "dragover", handlerObject: this, handlerMethod: "handleDragOverEvent"},
{name: "dragleave", handlerObject: this, handlerMethod: "handleDragLeaveEvent"},
{name: "drop", handlerObject: this, handlerMethod: "handleDropEvent"}
{name: "drop", handlerObject: this, handlerMethod: "handleDropEvent"},
{name: "dragend", handlerObject: this, handlerMethod: "handleDragLeaveEvent"}
]);
} else {
$tw.utils.addClass(this.domNode,this.disabledClass);
@ -69,6 +70,11 @@ DroppableWidget.prototype.enterDrag = function(event) {
}
// If we're entering for the first time we need to apply highlighting
$tw.utils.addClass(this.domNodes[0],"tc-dragover");
// Invoke any enter actions
if(this.droppableEnterActions) {
var modifierKey = $tw.keyboardManager.getEventModifierKeyDescriptor(event);
this.invokeActionString(this.droppableEnterActions,this,event,{modifier: modifierKey});
}
};
DroppableWidget.prototype.leaveDrag = function(event) {
@ -82,6 +88,11 @@ DroppableWidget.prototype.leaveDrag = function(event) {
if(this.domNodes[0]) {
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
}
// Invoke any leave actions
if(this.droppableLeaveActions) {
var modifierKey = $tw.keyboardManager.getEventModifierKeyDescriptor(event);
this.invokeActionString(this.droppableLeaveActions,this,event,{modifier: modifierKey});
}
}
};
@ -106,7 +117,20 @@ DroppableWidget.prototype.handleDragOverEvent = function(event) {
return false;
};
DroppableWidget.prototype.handleDragLeaveEvent = function(event) {
DroppableWidget.prototype.handleDragEndEvent = function(event) {
if(this.domNodes[0]) {
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
}
this.currentlyEntered = [];
// Invoke any end actions
if(this.droppableEndActions) {
var modifierKey = $tw.keyboardManager.getEventModifierKeyDescriptor(event);
this.invokeActionString(this.droppableEndActions,this,event,{modifier: modifierKey});
}
return false;
};
DroppableWidget.prototype.handleDragLeaveEvent = function(event) {
this.leaveDrag(event);
return false;
};
@ -166,6 +190,9 @@ Compute the internal state of the widget
DroppableWidget.prototype.execute = function() {
this.droppableActions = this.getAttribute("actions");
this.droppableListActions = this.getAttribute("listActions");
this.droppableEnterActions = this.getAttribute("dragenteractions");
this.droppableLeaveActions = this.getAttribute("dragleaveactions");
this.droppableEndActions = this.getAttribute("dragendactions");
this.droppableEffect = this.getAttribute("effect","copy");
this.droppableTag = this.getAttribute("tag");
this.droppableEnable = (this.getAttribute("enable") || "yes") === "yes";

View file

@ -0,0 +1,9 @@
title: $:/core/ui/KeyboardShortcuts/toggle-dragndrop
tags: $:/tags/KeyboardShortcut
key: ((toggle-dragndrop))
<%if [{$:/config/story/dragndrop/enable}match[no]] %>
<$action-setfield $tiddler="$:/config/story/dragndrop/enable" text="yes"/>
<% else %>
<$action-setfield $tiddler="$:/config/story/dragndrop/enable" text="no"/>
<% endif %>

View file

@ -1,6 +1,11 @@
title: $:/core/ui/PageTemplate/story
tags: $:/tags/PageTemplate
\procedure content()
\whitespace trim
{{||$:/core/ui/StoryTiddlerTemplate}}
\end
\whitespace trim
<section class="tc-story-river" role="main">
@ -14,7 +19,11 @@ tags: $:/tags/PageTemplate
</section>
<$list filter="[list[$:/StoryList]]" history="$:/HistoryList" template="$:/core/ui/StoryTiddlerTemplate" storyview={{$:/view}} emptyMessage={{$:/config/EmptyStoryMessage}}/>
<$list filter="[list[$:/StoryList]]" history="$:/HistoryList" storyview={{$:/view}} emptyMessage={{$:/config/EmptyStoryMessage}}>
<$transclude $variable="make-draggable-droppable" tiddler=<<currentTiddler>> enable={{$:/config/story/dragndrop/enable}} list="$:/StoryList" content=<<content>> styleMarginBottom="28px" stylePaddingLeft="42px" stylePaddingRight="42px"/>
</$list>
<section class="story-frontdrop">

View file

@ -5,33 +5,18 @@ caption: {{$:/language/SideBar/Open/Caption}}
\whitespace trim
\define lingo-base() $:/language/CloseAll/
\define drop-actions()
<$action-listops $tiddler=<<tv-story-list>> $subfilter="+[insertbefore<actionTiddler>,<currentTiddler>]"/>
\end
\define placeholder()
<div class="tc-droppable-placeholder"/>
\end
\define droppable-item(button)
\whitespace trim
<$droppable actions=<<drop-actions>> enable=<<tv-allow-drag-and-drop>> tag="div">
<<placeholder>>
<div>
$button$
\define lingo-base() $:/language/CloseAll/
\define content()
<div class="tc-sidebar-tab-open-item">
<$button message='tm-close-tiddler' tooltip={{$:/language/Buttons/Close/Hint}} aria-label={{$:/language/Buttons/Close/Caption}} class='tc-btn-invisible tc-btn-mini tc-small-gap-right'>{{$:/core/images/close-button}}</$button><$link draggable="no"/>
</div>
</$droppable>
\end
<div class="tc-sidebar-tab-open">
<$list filter="[list<tv-story-list>]" history=<<tv-history-list>> storyview="pop">
<div class="tc-sidebar-tab-open-item">
<$macrocall $name="droppable-item" button="<$button message='tm-close-tiddler' tooltip={{$:/language/Buttons/Close/Hint}} aria-label={{$:/language/Buttons/Close/Caption}} class='tc-btn-invisible tc-btn-mini tc-small-gap-right'>{{$:/core/images/close-button}}</$button><$link/>"/>
</div>
<$transclude $variable="make-draggable-droppable" tiddler=<<currentTiddler>> list=<<tv-story-list>> content=<<content>> animationDuration="200"/>
</$list>
<$tiddler tiddler="">
<div>
<$macrocall $name="droppable-item" button="<$button message='tm-close-all-tiddlers' class='tc-btn-invisible tc-btn-mini'><<lingo Button>></$button>"/>
</div>
</$tiddler>
</div>
<$button message='tm-close-all-tiddlers' class='tc-btn-invisible tc-btn-mini'><<lingo Button>></$button>

View file

@ -0,0 +1,3 @@
title: $:/config/story/dragndrop/enable
no

View file

@ -44,6 +44,7 @@ stamp: {{$:/language/Buttons/Stamp/Hint}}
strikethrough: {{$:/language/Buttons/Strikethrough/Hint}}
subscript: {{$:/language/Buttons/Subscript/Hint}}
superscript: {{$:/language/Buttons/Superscript/Hint}}
toggle-dragndrop: {{$:/language/Buttons/ToggleDragnDrop/Hint}}
toggle-sidebar: {{$:/language/Buttons/ToggleSidebar/Hint}}
transcludify: {{$:/language/Buttons/Transcludify/Hint}}
underline: {{$:/language/Buttons/Underline/Hint}}

View file

@ -38,5 +38,6 @@ stamp: ctrl-S
strikethrough: ctrl-T
subscript: ctrl-shift-B
superscript: ctrl-shift-P
toggle-dragndrop: alt-D
toggle-sidebar: alt-shift-S
transcludify: alt-shift-T

View file

@ -0,0 +1,349 @@
title: $:/core/wiki/macros/dragndrop
tags: $:/tags/Global
\procedure draggable-droppable-bottom-drag-enter-actions()
<$action-setfield
$tiddler=<<dragStateTiddler>>
drag-count={{{ [<dragStateTiddler>get[drag-count]add[1]] }}}
drag-enter-class=<<qualifiedDragEnterClass>>
/>
<%if [<dragStateTiddler>has[drag-enter-count]] %>
<$action-setfield
$tiddler=<<dragStateTiddler>>
target-before=""
target-after={{{ [list<list>after<tiddler>] }}}
/>
<$action-setfield
$tiddler=<<dragStateTiddler>>
start-next-tiddler=""
copy=""
target=<<tiddler>>
margin="bottom"
nth={{{ [list<list>allbefore<tiddler>count[]add[1]] }}}
next-tiddler=""
from-index={{{ [list<list>allbefore<tiddler>count[]add[1]] }}}
from-list=<<list>>
drag-enter-count={{{ [<dragStateTiddler>get[drag-enter-count]add[1]] }}}
/>
<% else %>
<$action-setfield
$tiddler=<<dragStateTiddler>>
drag-enter-count="1"
/>
<% endif %>
\end
\procedure draggable-droppable-top-drag-enter-actions()
<$action-setfield
$tiddler=<<dragStateTiddler>>
drag-count={{{ [<dragStateTiddler>get[drag-count]add[1]] }}}
drag-enter-class=<<qualifiedDragEnterClass>>
/>
<%if [<dragStateTiddler>has[drag-enter-count]] %>
<$action-setfield
$tiddler=<<dragStateTiddler>>
target-before={{{ [list<list>before<tiddler>] }}}
target-after=""
/>
<$action-setfield
$tiddler=<<dragStateTiddler>>
start-next-tiddler=""
copy=""
target=<<tiddler>>
margin="top"
nth={{{ [list<list>allbefore<tiddler>count[]] }}}
next-tiddler=""
from-index={{{ [list<list>allbefore<tiddler>count[]] }}}
from-list=<<list>>
drag-enter-count={{{ [<dragStateTiddler>get[drag-enter-count]add[1]] }}}
/>
<% else %>
<$action-setfield
$tiddler=<<dragStateTiddler>>
drag-enter-count="1"
/>
<% endif %>
\end
\procedure draggable-droppable-bottom-drop-actions-inner()
<$let
nextTiddler={{{ [list<list>after<tiddler>] }}}
>
<%if [<nextTiddler>match[]] %>
<$action-listops
$tiddler=<<list>>
$subfilter="[<actionTiddler>]"
/>
<!-- here we could set the current-tiddler field of the history to the value of the actionTiddler -->
<% else %>
<%if [<actionTiddler>!match<nextTiddler>] %>
<$action-listops
$tiddler=<<list>>
$subfilter="+[insertbefore<actionTiddler>,<nextTiddler>]"
/>
<!-- here we could set the current-tiddler field of the history to the value of the actionTiddler -->
<% else %>
<$action-deletetiddler
$tiddler={{{ [<dragStateTiddler>addsuffix[/drag-handled]] }}}
/>
<$action-deletetiddler
$tiddler=<<dragStateTiddler>>
/>
<% endif %>
<% endif %>
<$action-deletetiddler
$tiddler=<<dragStateTiddler>>
/>
<<dropActions>>
</$let>
<!-- here we could set the current-tiddler field of the start-history to a different value -->
\end
\procedure draggable-droppable-bottom-drop-actions()
<$action-setfield
$tiddler={{{ [<dragStateTiddler>addsuffix[/drag-handled]] }}}
text="yes"
/>
<%if [<actionTiddler>removeprefix<prefix>removesuffix<suffix>] %>
<$let
actionTiddler={{{ [<actionTiddler>removeprefix<prefix>removesuffix<suffix>] }}}
>
<$transclude
$variable="draggable-droppable-bottom-drop-actions-inner"
/>
</$let>
<% else %>
<$transclude
$variable="draggable-droppable-bottom-drop-actions-inner"
/>
<% endif %>
<<dropActions>>
\end
\procedure draggable-droppable-top-drop-actions-inner()
<%if [<actionTiddler>!match<tiddler>] %>
<$action-listops
$tiddler=<<list>>
$subfilter="+[insertbefore<actionTiddler>,<tiddler>]"
/>
<!-- here we could set the current-tiddler field of the history to the value of the actionTiddler -->
<% else %>
<$action-deletetiddler
$tiddler={{{ [<dragStateTiddler>addsuffix[/drag-handled]] }}}
/>
<$action-deletetiddler
$tiddler=<<dragStateTiddler>>
/>
<% endif %>
<$action-deletetiddler
$tiddler=<<dragStateTiddler>>
/>
<!-- here we could set the current-tiddler field of the start-history to a different value -->
\end
\procedure draggable-droppable-top-drop-actions()
<$action-setfield
$tiddler={{{ [<dragStateTiddler>addsuffix[/drag-handled]] }}}
text="yes"
/>
<%if [<actionTiddler>removeprefix<prefix>removesuffix<suffix>] %>
<$let
actionTiddler={{{ [<actionTiddler>removeprefix<prefix>removesuffix<suffix>] }}}
>
<$transclude
$variable="draggable-droppable-top-drop-actions-inner"
/>
</$let>
<% else %>
<$transclude
$variable="draggable-droppable-top-drop-actions-inner"
/>
<% endif %>
\end
\procedure draggable-droppable-drag-end-actions()
<%if [<dragStateTiddler>addsuffix[/drag-handled]is[missing]] %>
<$let
fromList={{{ [<dragStateTiddler>get[start-from-list]] }}}
fromIndex={{{ [<dragStateTiddler>get[start-from-index]] }}}
listBeforeTiddler={{{ [list<fromList>zth<fromIndex>] }}}
actionTiddler={{{ [<actionTiddler>removeprefix<prefix>removesuffix<suffix>] :else[<actionTiddler>] }}}
>
<%if [<listBeforeTiddler>!is[blank]] %>
<$action-listops
$tiddler=<<fromStoryList>>
$subfilter="+[insertbefore<actionTiddler>,<listBeforeTiddler>]"
/>
<% else %>
<$action-listops
$tiddler=<<fromStoryList>>
$subfilter="[<actionTiddler>]"
/>
<% endif %>
<$action-deletetiddler
$tiddler={{{ [<dragStateTiddler>addsuffix[/drag-handled]] }}}
/>
<$action-deletetiddler
$tiddler=<<dragStateTiddler>>
/>
</$let>
<% else %>
<$action-deletetiddler
$tiddler={{{ [<dragStateTiddler>addsuffix[/drag-handled]] }}}
/>
<% endif %>
\end
\procedure draggable-droppable-drag-start-actions()
<$let
nth={{{ [list<list>allbefore<tiddler>count[]] }}}
listLength={{{ [list<list>count[]subtract[1]] }}}
>
<%if [<nth>match<listLength>] %>
<$action-setfield
$tiddler=<<dragStateTiddler>>
before-tiddler={{{ [list<list>before<tiddler>] }}}
/>
<% endif %>
<$action-setfield
$tiddler=<<dragStateTiddler>>
drag-count="0"
nth=<<nth>>
height=<<tv-selectednode-height>>
width=<<tv-selectednode-width>>
margin-bottom=<<styleMarginBottom>>
start-next-tiddler={{{ [list<list>after<tiddler>] }}}
start-from-index={{{ [list<list>allbefore<tiddler>count[]] }}}
start-from-list=<<list>>
/>
<$action-setfield
$tiddler=<<dragStateTiddler>>
drag-tiddler=<<tiddler>>
next-tiddler={{{ [list<list>after<tiddler>] }}}
from-index={{{ [list<list>allbefore<tiddler>count[]] }}}
from-list=<<list>>
from-history=<<history>>
target={{{ [list<list>after<tiddler>] }}}
/>
<$action-setfield
$tiddler=<<dragStateTiddler>>
tags="$:/tags/DragStateTiddler"
text="yes"
/>
<%if [<modifier>!match[ctrl]] %>
<$action-listops
$tiddler=<<list>>
$subfilter="-[<tiddler>]"
/>
<% else %>
<$action-setfield
$tiddler=<<dragStateTiddler>>
copy="yes"
/>
<% endif %>
</$let>
\end
\function get.drag.state.tiddler.height() [<dragStateTiddler>get[height]addsuffix[px]]
\function get.drag.state.tiddler.margin.bottom() [<dragStateTiddler>get[margin-bottom]]
\function get.draggable.droppable.bottom.class() tc-draggable-droppable-drop-bottom [<dragStateTiddler>get[margin]match[bottom]then<qualifiedDragEnterClass>] :and[join[ ]]
\function get.draggable.droppable.top.class() tc-draggable-droppable-drop-top [<dragStateTiddler>get[margin]match[top]then<qualifiedDragEnterClass>] :and[join[ ]]
\function get.tiddler.before.target() [<dragStateTiddler>get[target]] :reduce[list<list>before<currentTiddler>]
\function get.tiddler.after.target() [<dragStateTiddler>get[target]] :reduce[list<list>after<currentTiddler>]
\function get.styleTransition()
[<dragStateTiddler>get[start-next-tiddler]match<tiddler>then[none]]
:else[[margin-top ]addsuffix<animationDuration>addsuffix[ms ]addsuffix<animationCurve>addsuffix[, margin-bottom ]addsuffix<animationDuration>addsuffix[ms ]addsuffix<animationCurve>]
\end
\function get.styleTop() [<dragStateTiddler>get[start-next-tiddler]match<tiddler>then[calc(-]addsuffix<get.drag.state.tiddler.height>addsuffix[ - (2 * ]addsuffix<get.drag.state.tiddler.margin.bottom>addsuffix[)]] :else[<dragStateTiddler>get[drag-enter-class]match<qualifiedDragEnterClass>then<dragStateTiddler>get[margin]match[top]then[calc(-]addsuffix<get.drag.state.tiddler.height>addsuffix[ - (2 * ]addsuffix<get.drag.state.tiddler.margin.bottom>addsuffix[)]] :else[[calc( ]addsuffix[-]addsuffix<styleMarginTop>addsuffix[)]]
\function get.styleBottom() [[calc( ]addsuffix[-]addsuffix<styleMarginBottom>addsuffix[)]]
\function get.styleLeft() [[calc( ]addsuffix[-]addsuffix<stylePaddingLeft>addsuffix[)]]
\function get.styleTopHeight() [<dragStateTiddler>get[start-next-tiddler]match<tiddler>then[calc(50% + ]addsuffix<get.drag.state.tiddler.margin.bottom>addsuffix[ + ]addsuffix<get.drag.state.tiddler.height>addsuffix[)]] :else[<dragStateTiddler>get[drag-enter-class]match<qualifiedDragEnterClass>then<dragStateTiddler>get[margin]match[top]then[calc(50% + (2 * ]addsuffix<get.drag.state.tiddler.margin.bottom>addsuffix[) + ]addsuffix<get.drag.state.tiddler.height>addsuffix[)]] :else[[calc(50% + ]addsuffix<styleMarginBottom>addsuffix[)]]
\function get.styleBottomHeight() [<dragStateTiddler>get[drag-enter-class]match<qualifiedDragEnterClass>then<dragStateTiddler>get[margin]match[bottom]then[calc(50% + ]addsuffix<get.drag.state.tiddler.height>addsuffix[ + ]addsuffix<get.drag.state.tiddler.margin.bottom>addsuffix[)]] :else[[50%]]
\function get.styleWidth() [[calc(100% + ]addsuffix<stylePaddingLeft>addsuffix[ + ]addsuffix<stylePaddingRight>addsuffix[)]]
\function get.styleMarginTop() [<dragStateTiddler>get[start-next-tiddler]match<tiddler>then[calc(]addsuffix<get.drag.state.tiddler.height>addsuffix[ + (2 * ]addsuffix<get.drag.state.tiddler.margin.bottom>addsuffix[))]] :else[<dragStateTiddler>get[drag-enter-class]match<qualifiedDragEnterClass>then<dragStateTiddler>get[margin]match[top]then<dragStateTiddler>get[height]addsuffix[px]addprefix[calc(]addsuffix[ + (2 * ]addsuffix<styleMarginBottom>addsuffix[))]] :else[[0px]]
\function get.styleMarginBottom() [<dragStateTiddler>get[drag-enter-class]match<qualifiedDragEnterClass>then<dragStateTiddler>get[margin]match[bottom]then<dragStateTiddler>get[height]addsuffix[px]addprefix[calc(]addsuffix[ + (2 * ]addsuffix<styleMarginBottom>addsuffix[))]] :else[[0px]]
\function get.draggable.droppable.class() tc-draggable-droppable-qualifier [<qualifiedDragEnterClass>] :and[join[ ]]
\function get.flexDirection() [<direction>match[vertical]then[row]] [<direction>match[horizontal]then[column]]
\procedure make-draggable-droppable(tiddler:"",class:"",content:"",list:"",enable:"yes",dropActions:"",history:"",dragStateTiddler:"$:/state/tiddlywiki/dragging",direction:"vertical",styleMarginTop:"0px",styleMarginBottom:"0px",stylePaddingLeft:"0px",stylePaddingRight:"0px",styleZIndex:"501",animationDuration:"175",animationCurve:"linear")
\whitespace trim
<$tiddler tiddler=<<tiddler>>>
<$let
prefix="[["
suffix="]]"
transclusion=<<tiddler>>
qualifiedDragEnterClass={{{ [<tiddler>addsuffix<qualfiy>sha256[64]] }}}
>
<$draggable
enable=<<enable>>
tiddler=<<tiddler>>
startactions=<<draggable-droppable-drag-start-actions>>
endactions=<<draggable-droppable-drag-end-actions>>
class=<<class>>
>
<div
class=<<get.draggable.droppable.class>>
style.display="flex"
style.position="relative"
style.flex-direction=<<get.flexDirection>>
style.border="none"
style.margin-top=<<get.styleMarginTop>>
style.margin-bottom=<<get.styleMarginBottom>>
style.transition=<<get.styleTransition>>
>
<$set name="tv-enable-drag-and-drop" value={{{ [<enable>match[yes]then[no]] :else[<tv-enable-drag-and-drop>] }}}>
<<content>>
</$set>
<%if [<dragStateTiddler>get[text]match[yes]] %>
<%if [<direction>match[vertical]] %>
<$droppable
class=<<get.draggable.droppable.top.class>>
actions=<<draggable-droppable-top-drop-actions>>
dragenteractions=<<draggable-droppable-top-drag-enter-actions>>
>
<div
class="tc-draggable-droppable-drop-top-placeholder"
style.position="absolute"
style.border="none"
style.top=<<get.styleTop>>
style.left=<<get.styleLeft>>
style.height=<<get.styleTopHeight>>
style.width=<<get.styleWidth>>
style.z-index=<<styleZIndex>>
/>
</$droppable>
<$droppable
class=<<get.draggable.droppable.bottom.class>>
actions=<<draggable-droppable-bottom-drop-actions>>
dragenteractions=<<draggable-droppable-bottom-drag-enter-actions>>
>
<div
class="tc-draggable-droppable-drop-bottom-placeholder"
style.position="absolute"
style.border="none"
style.top="50%"
style.left=<<get.styleLeft>>
style.height=<<get.styleBottomHeight>>
style.width=<<get.styleWidth>>
style.z-index=<<styleZIndex>>
/>
</$droppable>
<%elseif [<direction>match[horizontal]] %>
<!-- horizontal handling missing at the moment -->
<% endif %>
<% endif %>
</div>
</$draggable>
</$let>
</$tiddler>
\end

View file

@ -0,0 +1,15 @@
title: $:/changenotes/5.4.0/#9507
created: 20251218204100502
modified: 20251218204100502
tags: $:/tags/ChangeNote
change-type: enhancement
change-category: usability
description: Dragndrop procedures for enhanced dragndrop lists
release: 5.4.0
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9507
github-contributors: BurningTreeC
type: text/vnd.tiddlywiki
This pull request extends Tiddlywiki with advanced drag-and-drop procedures.
Lists can be expanded using drag-and-drop, and the drag-and-drop experience feels more natural.