Rendered widget elements moving relative to neighboring elements on refresh

I’ve written a widget for dropdowns in the style of toc-expandable, and it works for the most part, but when it updates, any elements rendered to the same parent that are after my widget get pushed before it.

I’m not sure what causes this, but I have to assume I’m doing something wrong with refreshing, but I can’t work it out. The issue doesn’t manifest if I wrap the reveals in their own span or div, but I’m concerned I’m doing something wrong and maybe shouldn’t just hack around it.

What’s causing this?

The full text of my widget:

/*\
title: $:/plugins/tavi-vi/collapse/collapse.js
type: application/javascript
module-type: widget
Collapse widget
\*/

(function(){

/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";

var Widget = require("$:/core/modules/widgets/widget.js").widget;

var CollapseWidget = function(parseTreeNode,options) {
	this.initialise(parseTreeNode,options);
};

/*
Inherit from the base widget class
*/
CollapseWidget.prototype = new Widget();

/*
Render this widget into the DOM
*/
CollapseWidget.prototype.render = function(parent,nextSibling) {
	this.computeAttributes();
	this.execute();
	this.renderChildren(parent,nextSibling);
};

/*
Compute the internal state of the widget
*/
CollapseWidget.prototype.execute = function() {
	var addAttributes = function(node, attributes) {
		for(const key in attributes) {
			$tw.utils.addAttributeToParseTreeNode(node, key, attributes[key]);
		}
	}
	this.collapseSpoiler = this.getAttribute("spoiler");
	this.collapseState = this.getAttribute("state");
	var buttonOpen = {
		type: "reveal",
		tag: "$reveal",
		children: [
			{
				type: "button",
				tag: "$button",
				children: [
					{
						type: "transclude",
						tag: "$transclude",
					},
					{
						type: "element",
						tag: "span",
						children: [
							{ type: "text", text: this.collapseSpoiler }
						]
					}
				]
			}
		]
	};
	var buttonClose = JSON.parse(JSON.stringify(buttonOpen));

	/* Open attributes */
	addAttributes(buttonOpen, {
		stateTitle: this.collapseState,
		text: "open",
		type: "nomatch"
	});
	addAttributes(buttonOpen.children[0], {
		setTitle: this.collapseState,
		"class": "tc-btn-invisible",
		setTo: "open"
	});
	addAttributes(buttonOpen.children[0].children[0], {
		tiddler: "$:/core/images/right-arrow"
	});

	/* Close attributes */
	addAttributes(buttonClose, {
		stateTitle: this.collapseState,
		text: "open",
		type: "match"
	});
	addAttributes(buttonClose.children[0], {
		setTitle: this.collapseState,
		"class": "tc-btn-invisible",
		setTo: "close"
	});
	addAttributes(buttonClose.children[0].children[0], {
		tiddler: "$:/core/images/down-arrow"
	});

	/* Content */
	var content = {
		type: "reveal",
		tag: "$reveal",
		children: [
			{
				type: "element",
				tag: "div",
				children: this.parseTreeNode.children
			}
		]
	};
	addAttributes(content, {
		stateTitle: this.collapseState,
		text: "open",
		type: "match"
	});
	this.makeChildWidgets([ buttonOpen, buttonClose, content ]);
}

/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
CollapseWidget.prototype.refresh = function(changedTiddlers) {
	this.computeAttributes();
	return this.refreshChildren(changedTiddlers);
};

exports.collapse = CollapseWidget;

})();

An example of an invocation that exhibits the behavior (toggle the dropdown a few times and AFTER appears before the dropdown):

<$collapse spoiler="._." state="$:/state/tavi-vi/collapse-journal">
    :o
</$collapse>

AFTER

It looks like when reveal gets rendered on refresh, it’s being given an incorrect nextSibling. Maybe it’s a tiddlywiki bug? I’m not sure what I would be doing to cause that. It looks like it’s correct until the last reveal, then nextsibling is null, so it puts it at the end of the parent element instead of before the AFTER text node.

Maybe what I’m doing wrong is that I’m not making a single element. Reading the findNextSiblingDomNode implementation, it looks like the assumption is that one widget=one element (and children). If that’s the case, then wrapping everything into a span or div is the solution…

Okay, I figured it out, looking at the let widget’s code, it doesn’t create a DOM element, but what it does do is set this.parentDomNode = parent which seems to fix this.

1 Like