mirror of
https://github.com/Jermolene/TiddlyWiki5.git
synced 2026-03-11 09:12:00 -07:00
* Introduced preliminary idea for infinite recurse exception * Better handling of infinite recursion But it could be better still... * the TransclusionError is a proper error Moved the magic number to be on the error's class. Not sure if that's a great idea. * Fixed minor minor issue that came up in conflict The minor fix to the jasmine regexp that escaped a '+' somehow broke some random test. * Removing patch fix for recursion errors * Fixed issue where buttton and other widgets don't clean up * Added release notes for #9548 * Update test-widget.js If I don't fix those indentations, the entire TW codebase will explode or soemthing. * Update test-widget.js These lint problems are wasting my time. * Fixed all core widgets to not leak when renderChildren fails * Updated release notes to reflect what I'm actually fixing * Update test-widget.js Added warning not to use for-of loop for defining tests. The iterating variable needs to have its own method scope, or it risks being the same value for all tests.
234 lines
7.5 KiB
JavaScript
Executable file
234 lines
7.5 KiB
JavaScript
Executable file
/*\
|
|
title: $:/core/modules/widgets/link.js
|
|
type: application/javascript
|
|
module-type: widget
|
|
|
|
Link widget
|
|
|
|
\*/
|
|
|
|
"use strict";
|
|
|
|
var Widget = require("$:/core/modules/widgets/widget.js").widget;
|
|
|
|
var LinkWidget = function(parseTreeNode,options) {
|
|
this.initialise(parseTreeNode,options);
|
|
};
|
|
|
|
/*
|
|
Inherit from the base widget class
|
|
*/
|
|
LinkWidget.prototype = new Widget();
|
|
|
|
/*
|
|
Render this widget into the DOM
|
|
*/
|
|
LinkWidget.prototype.render = function(parent,nextSibling) {
|
|
// Save the parent dom node
|
|
this.parentDomNode = parent;
|
|
// Compute our attributes
|
|
this.computeAttributes();
|
|
// Execute our logic
|
|
this.execute();
|
|
// Get the value of the tv-wikilinks configuration macro
|
|
var wikiLinksMacro = this.getVariable("tv-wikilinks"),
|
|
useWikiLinks = wikiLinksMacro ? (wikiLinksMacro.trim() !== "no") : true,
|
|
missingLinksEnabled = !(this.hideMissingLinks && this.isMissing && !this.isShadow);
|
|
// Render the link if required
|
|
if(useWikiLinks && missingLinksEnabled) {
|
|
this.renderLink(parent,nextSibling);
|
|
} else {
|
|
// Just insert the link text
|
|
var domNode = this.document.createElement("span");
|
|
// Assign data- attributes
|
|
this.assignAttributes(domNode,{
|
|
sourcePrefix: "data-",
|
|
destPrefix: "data-"
|
|
});
|
|
this.assignAttributes(domNode,{
|
|
sourcePrefix: "aria-",
|
|
destPrefix: "aria-"
|
|
});
|
|
parent.insertBefore(domNode,nextSibling);
|
|
this.domNodes.push(domNode);
|
|
this.renderChildren(domNode,null);
|
|
}
|
|
};
|
|
|
|
/*
|
|
Render this widget into the DOM
|
|
*/
|
|
LinkWidget.prototype.renderLink = function(parent,nextSibling) {
|
|
var self = this;
|
|
// Sanitise the specified tag
|
|
var tag = this.linkTag;
|
|
if($tw.config.htmlUnsafeElements.indexOf(tag) !== -1) {
|
|
tag = "a";
|
|
}
|
|
// Create our element
|
|
var namespace = this.getVariable("namespace",{defaultValue: "http://www.w3.org/1999/xhtml"}),
|
|
domNode = this.document.createElementNS(namespace,tag);
|
|
// Assign classes
|
|
var classes = [];
|
|
if(this.overrideClasses === undefined) {
|
|
classes.push("tc-tiddlylink");
|
|
if(this.isShadow) {
|
|
classes.push("tc-tiddlylink-shadow");
|
|
}
|
|
if(this.isMissing && !this.isShadow) {
|
|
classes.push("tc-tiddlylink-missing");
|
|
} else {
|
|
if(!this.isMissing) {
|
|
classes.push("tc-tiddlylink-resolves");
|
|
}
|
|
}
|
|
if(this.linkClasses) {
|
|
classes.push(this.linkClasses);
|
|
}
|
|
} else if(this.overrideClasses !== "") {
|
|
classes.push(this.overrideClasses)
|
|
}
|
|
if(classes.length > 0) {
|
|
domNode.setAttribute("class",classes.join(" "));
|
|
}
|
|
// Set an href
|
|
var wikilinkTransformFilter = this.getVariable("tv-filter-export-link"),
|
|
wikiLinkText;
|
|
if(wikilinkTransformFilter) {
|
|
// Use the filter to construct the href
|
|
wikiLinkText = this.wiki.filterTiddlers(wikilinkTransformFilter,this,function(iterator) {
|
|
iterator(self.wiki.getTiddler(self.to),self.to)
|
|
})[0];
|
|
} else {
|
|
// Expand the tv-wikilink-template variable to construct the href
|
|
var wikiLinkTemplateMacro = this.getVariable("tv-wikilink-template"),
|
|
wikiLinkTemplate = wikiLinkTemplateMacro ? wikiLinkTemplateMacro.trim() : "#$uri_encoded$";
|
|
wikiLinkText = $tw.utils.replaceString(wikiLinkTemplate,"$uri_encoded$",$tw.utils.encodeURIComponentExtended(this.to));
|
|
wikiLinkText = $tw.utils.replaceString(wikiLinkText,"$uri_doubleencoded$",$tw.utils.encodeURIComponentExtended($tw.utils.encodeURIComponentExtended(this.to)));
|
|
}
|
|
// Override with the value of tv-get-export-link if defined
|
|
wikiLinkText = this.getVariable("tv-get-export-link",{params: [{name: "to",value: this.to}],defaultValue: wikiLinkText});
|
|
if(tag === "a") {
|
|
var namespaceHref = (namespace === "http://www.w3.org/2000/svg") ? "http://www.w3.org/1999/xlink" : undefined;
|
|
domNode.setAttributeNS(namespaceHref,"href",wikiLinkText);
|
|
}
|
|
// Set the tabindex
|
|
if(this.tabIndex) {
|
|
domNode.setAttribute("tabindex",this.tabIndex);
|
|
}
|
|
// Set the tooltip
|
|
// HACK: Performance issues with re-parsing the tooltip prevent us defaulting the tooltip to "<$transclude field='tooltip'><$transclude field='title'/></$transclude>"
|
|
var tooltipWikiText = this.tooltip || this.getVariable("tv-wikilink-tooltip");
|
|
if(tooltipWikiText) {
|
|
var tooltipText = this.wiki.renderText("text/plain","text/vnd.tiddlywiki",tooltipWikiText,{
|
|
parseAsInline: true,
|
|
variables: {
|
|
currentTiddler: this.to
|
|
},
|
|
parentWidget: this
|
|
});
|
|
domNode.setAttribute("title",tooltipText);
|
|
}
|
|
if(this.role) {
|
|
domNode.setAttribute("role",this.role);
|
|
}
|
|
this.assignAttributes(domNode,{
|
|
sourcePrefix: "aria-",
|
|
destPrefix: "aria-"
|
|
})
|
|
// Add a click event handler
|
|
$tw.utils.addEventListeners(domNode,[
|
|
{name: "click", handlerObject: this, handlerMethod: "handleClickEvent"},
|
|
]);
|
|
// Make the link draggable if required
|
|
if(this.draggable === "yes") {
|
|
$tw.utils.makeDraggable({
|
|
domNode: domNode,
|
|
dragTiddlerFn: function() {return self.to;},
|
|
widget: this
|
|
});
|
|
} else if(this.draggable === "no") {
|
|
domNode.setAttribute("draggable","false");
|
|
}
|
|
// Assign data- attributes
|
|
this.assignAttributes(domNode,{
|
|
sourcePrefix: "data-",
|
|
destPrefix: "data-"
|
|
});
|
|
// Insert the link into the DOM and render any children
|
|
parent.insertBefore(domNode,nextSibling);
|
|
this.domNodes.push(domNode);
|
|
this.renderChildren(domNode,null);
|
|
};
|
|
|
|
LinkWidget.prototype.handleClickEvent = function(event) {
|
|
// Send the click on its way as a navigate event
|
|
var bounds = this.domNodes[0].getBoundingClientRect();
|
|
this.dispatchEvent({
|
|
type: "tm-navigate",
|
|
navigateTo: this.to,
|
|
navigateFromTitle: this.getVariable("storyTiddler"),
|
|
navigateFromNode: this,
|
|
navigateFromClientRect: { top: bounds.top, left: bounds.left, width: bounds.width, right: bounds.right, bottom: bounds.bottom, height: bounds.height
|
|
},
|
|
navigateFromClientTop: bounds.top,
|
|
navigateFromClientLeft: bounds.left,
|
|
navigateFromClientWidth: bounds.width,
|
|
navigateFromClientRight: bounds.right,
|
|
navigateFromClientBottom: bounds.bottom,
|
|
navigateFromClientHeight: bounds.height,
|
|
navigateSuppressNavigation: event.metaKey || event.ctrlKey || (event.button === 1),
|
|
metaKey: event.metaKey,
|
|
ctrlKey: event.ctrlKey,
|
|
altKey: event.altKey,
|
|
shiftKey: event.shiftKey,
|
|
event: event
|
|
});
|
|
if(this.domNodes[0].hasAttribute("href")) {
|
|
event.preventDefault();
|
|
}
|
|
event.stopPropagation();
|
|
return false;
|
|
};
|
|
|
|
/*
|
|
Compute the internal state of the widget
|
|
*/
|
|
LinkWidget.prototype.execute = function() {
|
|
// Pick up our attributes
|
|
this.to = this.getAttribute("to",this.getVariable("currentTiddler"));
|
|
this.tooltip = this.getAttribute("tooltip");
|
|
this.role = this.getAttribute("role");
|
|
this.linkClasses = this.getAttribute("class");
|
|
this.overrideClasses = this.getAttribute("overrideClass");
|
|
this.tabIndex = this.getAttribute("tabindex");
|
|
this.draggable = this.getAttribute("draggable","yes");
|
|
this.linkTag = this.getAttribute("tag","a");
|
|
// Determine the link characteristics
|
|
this.isMissing = !this.wiki.tiddlerExists(this.to);
|
|
this.isShadow = this.wiki.isShadowTiddler(this.to);
|
|
this.hideMissingLinks = (this.getVariable("tv-show-missing-links") || "yes") === "no";
|
|
// Make the child widgets
|
|
var templateTree;
|
|
if(this.parseTreeNode.children && this.parseTreeNode.children.length > 0) {
|
|
templateTree = this.parseTreeNode.children;
|
|
} else {
|
|
// Default template is a link to the title
|
|
templateTree = [{type: "text", text: this.to}];
|
|
}
|
|
this.makeChildWidgets(templateTree);
|
|
};
|
|
|
|
/*
|
|
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
|
|
*/
|
|
LinkWidget.prototype.refresh = function(changedTiddlers) {
|
|
var changedAttributes = this.computeAttributes();
|
|
if($tw.utils.count(changedAttributes) > 0 || changedTiddlers[this.to]) {
|
|
this.refreshSelf();
|
|
return true;
|
|
}
|
|
return this.refreshChildren(changedTiddlers);
|
|
};
|
|
|
|
exports.link = LinkWidget;
|