23 changes: 23 additions & 0 deletions core/modules/editor/operations/text/replace-all.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*\
title: $:/core/modules/editor/operations/text/replace-all.js
type: application/javascript
module-type: texteditoroperation
Text editor operation to replace the entire text
\*/
(function(){

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

exports["replace-all"] = function(event,operation) {
operation.cutStart = 0;
operation.cutEnd = operation.text.length;
operation.replacement = event.paramObject.text;
operation.newSelStart = 0;
operation.newSelEnd = operation.replacement.length;
};

})();
23 changes: 23 additions & 0 deletions core/modules/editor/operations/text/replace-selection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*\
title: $:/core/modules/editor/operations/text/replace-selection.js
type: application/javascript
module-type: texteditoroperation
Text editor operation to replace the selection
\*/
(function(){

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

exports["replace-selection"] = function(event,operation) {
operation.replacement = event.paramObject.text;
operation.cutStart = operation.selStart;
operation.cutEnd = operation.selEnd;
operation.newSelStart = operation.selStart;
operation.newSelEnd = operation.selStart + operation.replacement.length;
};

})();
28 changes: 28 additions & 0 deletions core/modules/editor/operations/text/wrap-lines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*\
title: $:/core/modules/editor/operations/text/wrap-lines.js
type: application/javascript
module-type: texteditoroperation
Text editor operation to wrap the selected lines with a prefix and suffix
\*/
(function(){

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

exports["wrap-lines"] = function(event,operation) {
// Cut just past the preceding line break, or the start of the text
operation.cutStart = $tw.utils.findPrecedingLineBreak(operation.text,operation.selStart);
// Cut to just past the following line break, or to the end of the text
operation.cutEnd = $tw.utils.findFollowingLineBreak(operation.text,operation.selEnd);
// Add the prefix and suffix
operation.replacement = event.paramObject.prefix + "\n" +
operation.text.substring(operation.cutStart,operation.cutEnd) + "\n" +
event.paramObject.suffix + "\n";
operation.newSelStart = operation.cutStart + event.paramObject.prefix.length + 1;
operation.newSelEnd = operation.newSelStart + (operation.cutEnd - operation.cutStart);
};

})();
52 changes: 52 additions & 0 deletions core/modules/editor/operations/text/wrap-selection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*\
title: $:/core/modules/editor/operations/text/wrap-selection.js
type: application/javascript
module-type: texteditoroperation
Text editor operation to wrap the selection with the specified prefix and suffix
\*/
(function(){

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

exports["wrap-selection"] = function(event,operation) {
if(operation.selStart === operation.selEnd) {
// No selection; check if we're within the prefix/suffix
if(operation.text.substring(operation.selStart - event.paramObject.prefix.length,operation.selStart + event.paramObject.suffix.length) === event.paramObject.prefix + event.paramObject.suffix) {
// Remove the prefix and suffix unless they comprise the entire text
if(operation.selStart > event.paramObject.prefix.length || (operation.selEnd + event.paramObject.suffix.length) < operation.text.length ) {
operation.cutStart = operation.selStart - event.paramObject.prefix.length;
operation.cutEnd = operation.selEnd + event.paramObject.suffix.length;
operation.replacement = "";
operation.newSelStart = operation.cutStart;
operation.newSelEnd = operation.newSelStart;
}
} else {
// Wrap the cursor instead
operation.cutStart = operation.selStart;
operation.cutEnd = operation.selEnd;
operation.replacement = event.paramObject.prefix + event.paramObject.suffix;
operation.newSelStart = operation.selStart + event.paramObject.prefix.length;
operation.newSelEnd = operation.newSelStart;
}
} else if(operation.text.substring(operation.selStart,operation.selStart + event.paramObject.prefix.length) === event.paramObject.prefix && operation.text.substring(operation.selEnd - event.paramObject.suffix.length,operation.selEnd) === event.paramObject.suffix) {
// Prefix and suffix are already present, so remove them
operation.cutStart = operation.selStart;
operation.cutEnd = operation.selEnd;
operation.replacement = operation.selection.substring(event.paramObject.prefix.length,operation.selection.length - event.paramObject.suffix.length);
operation.newSelStart = operation.selStart;
operation.newSelEnd = operation.selStart + operation.replacement.length;
} else {
// Add the prefix and suffix
operation.cutStart = operation.selStart;
operation.cutEnd = operation.selEnd;
operation.replacement = event.paramObject.prefix + operation.selection + event.paramObject.suffix;
operation.newSelStart = operation.selStart;
operation.newSelEnd = operation.selStart + operation.replacement.length;
}
};

})();
4 changes: 2 additions & 2 deletions core/modules/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ exports.parseFilter = function(filterString) {
operandRegExp.lastIndex = p;
match = operandRegExp.exec(filterString);
if(!match || match.index !== p) {
throw "Syntax error in filter expression";
throw $tw.language.getString("Error/FilterSyntax");
}
var operation = {
prefix: "",
Expand Down Expand Up @@ -171,7 +171,7 @@ exports.compileFilter = function(filterString) {
filterParseTree = this.parseFilter(filterString);
} catch(e) {
return function(source,widget) {
return ["Filter error: " + e];
return [$tw.language.getString("Error/Filter") + ": " + e];
};
}
// Get the hashmap of filter operator functions
Expand Down
1 change: 1 addition & 0 deletions core/modules/filters/days.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ exports.days = function(source,operator,options) {
};

if(operator.prefix === "!") {
targetTimeStamp = targetTimeStamp - 1000*60*60*24*dayIntervalSign;
source(function(tiddler,title) {
if(tiddler && tiddler.fields[fieldName]) {
if(!isWithinDays($tw.utils.parseDate(tiddler.fields[fieldName]))) {
Expand Down
2 changes: 1 addition & 1 deletion core/modules/filters/has.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ exports.has = function(source,operator,options) {
});
} else {
source(function(tiddler,title) {
if(tiddler && $tw.utils.hop(tiddler.fields,operator.operand) && tiddler.fields[operator.operand] !== "") {
if(tiddler && $tw.utils.hop(tiddler.fields,operator.operand) && !(tiddler.fields[operator.operand] === "" || tiddler.fields[operator.operand].length === 0)) {
results.push(title);
}
});
Expand Down
2 changes: 1 addition & 1 deletion core/modules/filters/is.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ exports.is = function(source,operator,options) {
if(isFilterOperator) {
return isFilterOperator(source,operator.prefix,options);
} else {
return ["Filter Error: Unknown operand for the 'is' filter operator"];
return [$tw.language.getString("Error/IsFilterOperator")];
}
};

Expand Down
279 changes: 279 additions & 0 deletions core/modules/keyboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/*\
title: $:/core/modules/keyboard.js
type: application/javascript
module-type: global
Keyboard handling utilities
\*/
(function(){

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

var namedKeys = {
"cancel": 3,
"help": 6,
"backspace": 8,
"tab": 9,
"clear": 12,
"return": 13,
"enter": 13,
"pause": 19,
"escape": 27,
"space": 32,
"page_up": 33,
"page_down": 34,
"end": 35,
"home": 36,
"left": 37,
"up": 38,
"right": 39,
"down": 40,
"printscreen": 44,
"insert": 45,
"delete": 46,
"0": 48,
"1": 49,
"2": 50,
"3": 51,
"4": 52,
"5": 53,
"6": 54,
"7": 55,
"8": 56,
"9": 57,
"firefoxsemicolon": 59,
"firefoxequals": 61,
"a": 65,
"b": 66,
"c": 67,
"d": 68,
"e": 69,
"f": 70,
"g": 71,
"h": 72,
"i": 73,
"j": 74,
"k": 75,
"l": 76,
"m": 77,
"n": 78,
"o": 79,
"p": 80,
"q": 81,
"r": 82,
"s": 83,
"t": 84,
"u": 85,
"v": 86,
"w": 87,
"x": 88,
"y": 89,
"z": 90,
"numpad0": 96,
"numpad1": 97,
"numpad2": 98,
"numpad3": 99,
"numpad4": 100,
"numpad5": 101,
"numpad6": 102,
"numpad7": 103,
"numpad8": 104,
"numpad9": 105,
"multiply": 106,
"add": 107,
"separator": 108,
"subtract": 109,
"decimal": 110,
"divide": 111,
"f1": 112,
"f2": 113,
"f3": 114,
"f4": 115,
"f5": 116,
"f6": 117,
"f7": 118,
"f8": 119,
"f9": 120,
"f10": 121,
"f11": 122,
"f12": 123,
"f13": 124,
"f14": 125,
"f15": 126,
"f16": 127,
"f17": 128,
"f18": 129,
"f19": 130,
"f20": 131,
"f21": 132,
"f22": 133,
"f23": 134,
"f24": 135,
"firefoxminus": 173,
"semicolon": 186,
"equals": 187,
"comma": 188,
"dash": 189,
"period": 190,
"slash": 191,
"backquote": 192,
"openbracket": 219,
"backslash": 220,
"closebracket": 221,
"quote": 222
};

function KeyboardManager(options) {
var self = this;
options = options || "";
// Save the named key hashmap
this.namedKeys = namedKeys;
// Create a reverse mapping of code to keyname
this.keyNames = [];
$tw.utils.each(namedKeys,function(keyCode,name) {
self.keyNames[keyCode] = name.substr(0,1).toUpperCase() + name.substr(1);
});
// Save the platform-specific name of the "meta" key
this.metaKeyName = $tw.platform.isMac ? "cmd-" : "win-";
}

/*
Return an array of keycodes for the modifier keys ctrl, shift, alt, meta
*/
KeyboardManager.prototype.getModifierKeys = function() {
return [
16, // Shift
17, // Ctrl
18, // Alt
20, // CAPS LOCK
91, // Meta (left)
93, // Meta (right)
224 // Meta (Firefox)
]
};

/*
Parses a key descriptor into the structure:
{
keyCode: numeric keycode
shiftKey: boolean
altKey: boolean
ctrlKey: boolean
metaKey: boolean
}
Key descriptors have the following format:
ctrl+enter
ctrl+shift+alt+A
*/
KeyboardManager.prototype.parseKeyDescriptor = function(keyDescriptor) {
var components = keyDescriptor.split(/\+|\-/),
info = {
keyCode: 0,
shiftKey: false,
altKey: false,
ctrlKey: false,
metaKey: false
};
for(var t=0; t<components.length; t++) {
var s = components[t].toLowerCase(),
c = s.charCodeAt(0);
// Look for modifier keys
if(s === "ctrl") {
info.ctrlKey = true;
} else if(s === "shift") {
info.shiftKey = true;
} else if(s === "alt") {
info.altKey = true;
} else if(s === "meta" || s === "cmd" || s === "win") {
info.metaKey = true;
}
// Replace named keys with their code
if(this.namedKeys[s]) {
info.keyCode = this.namedKeys[s];
}
}
if(info.keyCode) {
return info;
} else {
return null;
}
};

/*
Parse a list of key descriptors into an array of keyInfo objects. The key descriptors can be passed as an array of strings or a space separated string
*/
KeyboardManager.prototype.parseKeyDescriptors = function(keyDescriptors,options) {
var self = this;
options = options || {};
options.stack = options.stack || [];
var wiki = options.wiki || $tw.wiki;
if(typeof keyDescriptors === "string" && keyDescriptors === "") {
return [];
}
if(!$tw.utils.isArray(keyDescriptors)) {
keyDescriptors = keyDescriptors.split(" ");
}
var result = [];
$tw.utils.each(keyDescriptors,function(keyDescriptor) {
// Look for a named shortcut
if(keyDescriptor.substr(0,2) === "((" && keyDescriptor.substr(-2,2) === "))") {
if(options.stack.indexOf(keyDescriptor) === -1) {
options.stack.push(keyDescriptor);
var name = keyDescriptor.substring(2,keyDescriptor.length - 2),
lookupName = function(configName) {
var keyDescriptors = wiki.getTiddlerText("$:/config/" + configName + "/" + name);
if(keyDescriptors) {
result.push.apply(result,self.parseKeyDescriptors(keyDescriptors,options));
}
};
lookupName("shortcuts");
lookupName($tw.platform.isMac ? "shortcuts-mac" : "shortcuts-not-mac");
lookupName($tw.platform.isWindows ? "shortcuts-windows" : "shortcuts-not-windows");
lookupName($tw.platform.isLinux ? "shortcuts-linux" : "shortcuts-not-linux");
}
} else {
result.push(self.parseKeyDescriptor(keyDescriptor));
}
});
return result;
};

KeyboardManager.prototype.getPrintableShortcuts = function(keyInfoArray) {
var self = this,
result = [];
$tw.utils.each(keyInfoArray,function(keyInfo) {
if(keyInfo) {
result.push((keyInfo.ctrlKey ? "ctrl-" : "") +
(keyInfo.shiftKey ? "shift-" : "") +
(keyInfo.altKey ? "alt-" : "") +
(keyInfo.metaKey ? self.metaKeyName : "") +
(self.keyNames[keyInfo.keyCode]));
}
});
return result;
}

KeyboardManager.prototype.checkKeyDescriptor = function(event,keyInfo) {
return keyInfo &&
event.keyCode === keyInfo.keyCode &&
event.shiftKey === keyInfo.shiftKey &&
event.altKey === keyInfo.altKey &&
event.ctrlKey === keyInfo.ctrlKey &&
event.metaKey === keyInfo.metaKey;
};

KeyboardManager.prototype.checkKeyDescriptors = function(event,keyInfoArray) {
for(var t=0; t<keyInfoArray.length; t++) {
if(this.checkKeyDescriptor(event,keyInfoArray[t])) {
return true;
}
}
return false;
};

exports.KeyboardManager = KeyboardManager;

})();
45 changes: 45 additions & 0 deletions core/modules/macros/displayshortcuts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*\
title: $:/core/modules/macros/displayshortcuts.js
type: application/javascript
module-type: macro
Macro to display a list of keyboard shortcuts in human readable form. Notably, it resolves named shortcuts like `((bold))` to the underlying keystrokes.
\*/
(function(){

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

/*
Information about this macro
*/

exports.name = "displayshortcuts";

exports.params = [
{name: "shortcuts"},
{name: "prefix"},
{name: "separator"},
{name: "suffix"}
];

/*
Run the macro
*/
exports.run = function(shortcuts,prefix,separator,suffix) {
var shortcutArray = $tw.keyboardManager.getPrintableShortcuts($tw.keyboardManager.parseKeyDescriptors(shortcuts,{
wiki: this.wiki
}));
if(shortcutArray.length > 0) {
shortcutArray.sort(function(a,b) {
return a.toLowerCase().localeCompare(b.toLowerCase());
})
return prefix + shortcutArray.join(separator) + suffix;
} else {
return "";
}
};

})();
2 changes: 1 addition & 1 deletion core/modules/macros/makedatauri.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: $:/core/modules/macros/makedatauri.js
type: application/javascript
module-type: macro
Macro to convert the content of a tiddler to a data URI
Macro to convert a string of text to a data URI
<<makedatauri text:"Text to be converted" type:"text/vnd.tiddlywiki">>
Expand Down
2 changes: 1 addition & 1 deletion core/modules/parsers/htmlparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var HtmlParser = function(type,text,options) {
tag: "iframe",
attributes: {
src: {type: "string", value: src},
sandbox: {type: "string", value: "sandbox"}
sandbox: {type: "string", value: ""}
}
}];
};
Expand Down
5 changes: 3 additions & 2 deletions core/modules/parsers/wikiparser/rules/extlink.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ exports.types = {inline: true};
exports.init = function(parser) {
this.parser = parser;
// Regexp to match
this.matchRegExp = /~?(?:file|http|https|mailto|ftp|irc|news|data|skype):[^\s<>{}\[\]`|'"\\^~]+(?:\/|\b)/mg;
this.matchRegExp = /~?(?:file|http|https|mailto|ftp|irc|news|data|skype):[^\s<>{}\[\]`|"\\^~]+(?:\/|\b)/mg;
};

exports.parse = function() {
Expand All @@ -42,7 +42,8 @@ exports.parse = function() {
attributes: {
href: {type: "string", value: this.match[0]},
"class": {type: "string", value: "tc-tiddlylink-external"},
target: {type: "string", value: "_blank"}
target: {type: "string", value: "_blank"},
rel: {type: "string", value: "noopener noreferrer"}
},
children: [{
type: "text", text: this.match[0]
Expand Down
1 change: 1 addition & 0 deletions core/modules/parsers/wikiparser/rules/prettyextlink.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ exports.parseLink = function(source,pos) {
}
node.attributes.href = {type: "string", value: URL};
node.attributes.target = {type: "string", value: "_blank"};
node.attributes.rel = {type: "string", value: "noopener noreferrer"};
// Update the end position
node.end = closePos + 2;
return node;
Expand Down
3 changes: 2 additions & 1 deletion core/modules/parsers/wikiparser/rules/prettylink.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ exports.parse = function() {
attributes: {
href: {type: "string", value: link},
"class": {type: "string", value: "tc-tiddlylink-external"},
target: {type: "string", value: "_blank"}
target: {type: "string", value: "_blank"},
rel: {type: "string", value: "noopener noreferrer"}
},
children: [{
type: "text", text: text
Expand Down
1 change: 1 addition & 0 deletions core/modules/parsers/wikiparser/wikiparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Attributes are stored as hashmaps of the following objects:
{type: "string", value: <string>} - literal string
{type: "indirect", textReference: <textReference>} - indirect through a text reference
{type: "macro", macro: <TBD>} - indirect through a macro invocation
\*/
(function(){
Expand Down
2 changes: 1 addition & 1 deletion core/modules/saver-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ SaverHandler.prototype.saveWiki = function(options) {
text = this.wiki.renderTiddler(downloadType,template,options),
callback = function(err) {
if(err) {
alert("Error while saving:\n\n" + err);
alert($tw.language.getString("Error/WhileSaving") + ":\n\n" + err);
} else {
// Clear the task queue if we're saving (rather than downloading)
if(method !== "download") {
Expand Down
1 change: 1 addition & 0 deletions core/modules/savers/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ DownloadSaver.prototype.save = function(text,method,callback,options) {
// Set up the link
var link = document.createElement("a");
link.setAttribute("target","_blank");
link.setAttribute("rel","noopener noreferrer");
if(Blob !== undefined) {
var blob = new Blob([text], {type: "text/html"});
link.setAttribute("href", URL.createObjectURL(blob));
Expand Down
80 changes: 80 additions & 0 deletions core/modules/savers/put.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*\
title: $:/core/modules/savers/put.js
type: application/javascript
module-type: saver
Saves wiki by performing a PUT request to the server
Works with any server which accepts a PUT request
to the current URL, such as a WebDAV server.
\*/
(function(){

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

/*
Select the appropriate saver module and set it up
*/
var PutSaver = function(wiki) {
this.wiki = wiki;
var self = this;
// Async server probe. Until probe finishes, save will fail fast
// See also https://github.com/Jermolene/TiddlyWiki5/issues/2276
var req = new XMLHttpRequest();
req.open("OPTIONS",encodeURI(document.location.protocol + "//" + document.location.hostname + ":" + document.location.port + document.location.pathname));
req.onload = function() {
// Check DAV header http://www.webdav.org/specs/rfc2518.html#rfc.section.9.1
self.serverAcceptsPuts = (this.status === 200 && !!this.getResponseHeader('dav'));
};
req.send();
};

PutSaver.prototype.save = function(text,method,callback) {
if (!this.serverAcceptsPuts) {
return false;
}
var req = new XMLHttpRequest();
// TODO: store/check ETags if supported by server, to protect against overwrites
// Prompt: Do you want to save over this? Y/N
// Merging would be ideal, and may be possible using future generic merge flow
req.onload = function() {
if (this.status === 200 || this.status === 201) {
callback(null); // success
}
else {
callback(this.responseText); // fail
}
};
req.open("PUT", encodeURI(window.location.href));
req.setRequestHeader("Content-Type", "text/html;charset=UTF-8");
req.send(text);
return true;
};

/*
Information about this saver
*/
PutSaver.prototype.info = {
name: "put",
priority: 2000,
capabilities: ["save", "autosave"]
};

/*
Static method that returns true if this saver is capable of working
*/
exports.canSave = function(wiki) {
return /^https?:/.test(location.protocol);
};

/*
Create an instance of this saver
*/
exports.create = function(wiki) {
return new PutSaver(wiki);
};

})();
2 changes: 1 addition & 1 deletion core/modules/savers/twedit.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ TWEditSaver.prototype.save = function(text,method,callback) {
// Error handler
var errorHandler = function(event) {
// Error
callback("Error saving to TWEdit: " + event.target.error.code);
callback($tw.language.getString("Error/SavingToTWEdit") + ": " + event.target.error.code);
};
// Get the file system
window.requestFileSystem(LocalFileSystem.PERSISTENT,0,function(fileSystem) {
Expand Down
4 changes: 2 additions & 2 deletions core/modules/savers/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ UploadSaver.prototype.save = function(text,method,callback) {
// Do the HTTP post
var http = new XMLHttpRequest();
http.open("POST",url,true,username,password);
http.setRequestHeader("Content-Type","multipart/form-data; ;charset=UTF-8; boundary=" + boundary);
http.setRequestHeader("Content-Type","multipart/form-data; charset=UTF-8; boundary=" + boundary);
http.onreadystatechange = function() {
if(http.readyState == 4 && http.status == 200) {
if(http.responseText.substr(0,4) === "0 - ") {
Expand All @@ -67,7 +67,7 @@ UploadSaver.prototype.save = function(text,method,callback) {
try {
http.send(data);
} catch(ex) {
return callback("Error:" + ex);
return callback($tw.language.getString("Error/Caption") + ":" + ex);
}
$tw.notifier.display("$:/language/Notifications/Save/Starting");
return true;
Expand Down
4 changes: 2 additions & 2 deletions core/modules/startup/browser-messaging.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ exports.startup = function() {
if(url) {
loadIFrame(url,function(err,iframeInfo) {
if(err) {
alert("Error loading plugin library: " + url);
alert($tw.language.getString("Error/LoadingPluginLibrary") + ": " + url);
} else {
iframeInfo.domNode.contentWindow.postMessage({
verb: "GET",
Expand All @@ -100,7 +100,7 @@ exports.startup = function() {
if(url && title) {
loadIFrame(url,function(err,iframeInfo) {
if(err) {
alert("Error loading plugin library: " + url);
alert($tw.language.getString("Error/LoadingPluginLibrary") + ": " + url);
} else {
iframeInfo.domNode.contentWindow.postMessage({
verb: "GET",
Expand Down
2 changes: 1 addition & 1 deletion core/modules/startup/rootwidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ exports.startup = function() {
// Install the notification mechanism
$tw.notifier = new $tw.utils.Notifier($tw.wiki);
$tw.rootWidget.addEventListener("tm-notify",function(event) {
$tw.notifier.display(event.param);
$tw.notifier.display(event.param,{variables: event.paramObject});
});
// Install the scroller
$tw.pageScroller = new $tw.utils.PageScroller();
Expand Down
27 changes: 27 additions & 0 deletions core/modules/startup/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,34 @@ var widget = require("$:/core/modules/widgets/widget.js");

exports.startup = function() {
var modules,n,m,f;
// Minimal browser detection
if($tw.browser) {
$tw.browser.isIE = (/msie|trident/i.test(navigator.userAgent));
$tw.browser.isFirefox = !!document.mozFullScreenEnabled;
}
// Platform detection
$tw.platform = {};
if($tw.browser) {
$tw.platform.isMac = /Mac/.test(navigator.platform);
$tw.platform.isWindows = /win/i.test(navigator.platform);
$tw.platform.isLinux = /Linux/i.test(navigator.appVersion);
} else {
switch(require("os").platform()) {
case "darwin":
$tw.platform.isMac = true;
break;
case "win32":
$tw.platform.isWindows = true;
break;
case "freebsd":
$tw.platform.isLinux = true;
break;
case "linux":
$tw.platform.isLinux = true;
break;
}
}
// Initialise version
$tw.version = $tw.utils.extractVersionInfo();
// Set up the performance framework
$tw.perf = new $tw.Performance($tw.wiki.getTiddlerText(PERFORMANCE_INSTRUMENTATION_CONFIG_TITLE,"no") === "yes");
Expand All @@ -50,6 +75,8 @@ exports.startup = function() {
"$:/themes/tiddlywiki/vanilla"
]
});
// Kick off the keyboard manager
$tw.keyboardManager = new $tw.KeyboardManager();
// Clear outstanding tiddler store change events to avoid an unnecessary refresh cycle at startup
$tw.wiki.clearTiddlerEventQueue();
// Create a root widget for attaching event handlers. By using it as the parentWidget for another widget tree, one can reuse the event handlers
Expand Down
52 changes: 34 additions & 18 deletions core/modules/syncer.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,26 @@ Syncer.prototype.readTiddlerInfo = function() {
self.tiddlerInfo[title] = {
revision: tiddler.fields.revision,
adaptorInfo: self.syncadaptor && self.syncadaptor.getTiddlerInfo(tiddler),
changeCount: self.wiki.getChangeCount(title)
changeCount: self.wiki.getChangeCount(title),
hasBeenLazyLoaded: false
};
});
};

/*
Create an tiddlerInfo structure if it doesn't already exist
*/
Syncer.prototype.createTiddlerInfo = function(title) {
if(!$tw.utils.hop(this.tiddlerInfo,title)) {
this.tiddlerInfo[title] = {
revision: null,
adaptorInfo: {},
changeCount: -1,
hasBeenLazyLoaded: false
};
}
};

/*
Checks whether the wiki is dirty (ie the window shouldn't be closed)
*/
Expand Down Expand Up @@ -128,7 +143,8 @@ Syncer.prototype.storeTiddler = function(tiddlerFields) {
this.tiddlerInfo[tiddlerFields.title] = {
revision: tiddlerFields.revision,
adaptorInfo: this.syncadaptor.getTiddlerInfo(tiddler),
changeCount: this.wiki.getChangeCount(tiddlerFields.title)
changeCount: this.wiki.getChangeCount(tiddlerFields.title),
hasBeenLazyLoaded: true
};
};

Expand Down Expand Up @@ -180,7 +196,7 @@ Syncer.prototype.syncFromServer = function() {
},self.pollTimerInterval);
// Check for errors
if(err) {
self.logger.alert("Error retrieving skinny tiddler list:",err);
self.logger.alert($tw.language.getString("Error/RetrievingSkinny") + ":",err);
return;
}
// Process each incoming tiddler
Expand Down Expand Up @@ -238,11 +254,17 @@ Syncer.prototype.syncToServer = function(changes) {
Lazily load a skinny tiddler if we can
*/
Syncer.prototype.handleLazyLoadEvent = function(title) {
// Queue up a sync task to load this tiddler
this.enqueueSyncTask({
type: "load",
title: title
});
// Don't lazy load the same tiddler twice
var info = this.tiddlerInfo[title];
if(!info || !info.hasBeenLazyLoaded) {
this.createTiddlerInfo(title);
this.tiddlerInfo[title].hasBeenLazyLoaded = true;
// Queue up a sync task to load this tiddler
this.enqueueSyncTask({
type: "load",
title: title
});
}
};

/*
Expand All @@ -253,7 +275,7 @@ Syncer.prototype.handleLoginEvent = function() {
this.getStatus(function(err,isLoggedIn,username) {
if(!isLoggedIn) {
$tw.passwordPrompt.createPrompt({
serviceName: "Login to TiddlySpace",
serviceName: $tw.language.getString("LoginToTiddlySpace"),
callback: function(data) {
self.login(data.username,data.password,function(err,isLoggedIn) {
self.syncFromServer();
Expand Down Expand Up @@ -324,13 +346,7 @@ Syncer.prototype.enqueueSyncTask = function(task) {
task.queueTime = now;
task.lastModificationTime = now;
// Fill in some tiddlerInfo if the tiddler is one we haven't seen before
if(!$tw.utils.hop(this.tiddlerInfo,task.title)) {
this.tiddlerInfo[task.title] = {
revision: null,
adaptorInfo: {},
changeCount: -1
};
}
this.createTiddlerInfo(task.title);
// Bail if this is a save and the tiddler is already at the changeCount that the server has
if(task.type === "save" && this.wiki.getChangeCount(task.title) <= this.tiddlerInfo[task.title].changeCount) {
return;
Expand Down Expand Up @@ -387,8 +403,8 @@ Process the task queue, performing the next task if appropriate
*/
Syncer.prototype.processTaskQueue = function() {
var self = this;
// Only process a task if we're not already performing a task. If we are already performing a task then we'll dispatch the next one when it completes
if(this.numTasksInProgress() === 0) {
// Only process a task if the sync adaptor is fully initialised and we're not already performing a task. If we are already performing a task then we'll dispatch the next one when it completes
if(this.syncadaptor.isReady() && this.numTasksInProgress() === 0) {
// Choose the next task to perform
var task = this.chooseNextTask();
// Perform the task if we had one
Expand Down
66 changes: 66 additions & 0 deletions core/modules/utils/dom/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ exports.toggleClass = function(el,className,status) {
}
};

/*
Get the first parent element that has scrollbars or use the body as fallback.
*/
exports.getScrollContainer = function(el) {
var doc = el.ownerDocument;
while(el.parentNode) {
el = el.parentNode;
if(el.scrollTop) {
return el;
}
}
return doc.body;
};

/*
Get the scroll position of the viewport
Returns:
Expand All @@ -76,6 +90,31 @@ exports.getScrollPosition = function() {
}
};

/*
Adjust the height of a textarea to fit its content, preserving scroll position, and return the height
*/
exports.resizeTextAreaToFit = function(domNode,minHeight) {
// Get the scroll container and register the current scroll position
var container = $tw.utils.getScrollContainer(domNode),
scrollTop = container.scrollTop;
// Measure the specified minimum height
domNode.style.height = minHeight;
var measuredHeight = domNode.offsetHeight;
// Set its height to auto so that it snaps to the correct height
domNode.style.height = "auto";
// Calculate the revised height
var newHeight = Math.max(domNode.scrollHeight + domNode.offsetHeight - domNode.clientHeight,measuredHeight);
// Only try to change the height if it has changed
if(newHeight !== domNode.offsetHeight) {
domNode.style.height = newHeight + "px";
// Make sure that the dimensions of the textarea are recalculated
$tw.utils.forceLayout(domNode);
// Set the container to the position we registered at the beginning
container.scrollTop = scrollTop;
}
return newHeight;
};

/*
Gets the bounding rectangle of an element in absolute page coordinates
*/
Expand Down Expand Up @@ -164,5 +203,32 @@ exports.addEventListeners = function(domNode,events) {
});
};

/*
Get the computed styles applied to an element as an array of strings of individual CSS properties
*/
exports.getComputedStyles = function(domNode) {
var textAreaStyles = window.getComputedStyle(domNode,null),
styleDefs = [],
name;
for(var t=0; t<textAreaStyles.length; t++) {
name = textAreaStyles[t];
styleDefs.push(name + ": " + textAreaStyles.getPropertyValue(name) + ";");
}
return styleDefs;
};

/*
Apply a set of styles passed as an array of strings of individual CSS properties
*/
exports.setStyles = function(domNode,styleDefs) {
domNode.style.cssText = styleDefs.join("");
};

/*
Copy the computed styles from a source element to a destination element
*/
exports.copyStyles = function(srcDomNode,dstDomNode) {
$tw.utils.setStyles(dstDomNode,$tw.utils.getComputedStyles(srcDomNode));
};

})();
2 changes: 1 addition & 1 deletion core/modules/utils/dom/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ exports.httpRequest = function(options) {
return;
}
// Something went wrong
options.callback("XMLHttpRequest error code: " + this.status);
options.callback($tw.language.getString("Error/XMLHttpRequest") + ": " + this.status);
}
};
// Make the request
Expand Down
64 changes: 9 additions & 55 deletions core/modules/utils/dom/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: $:/core/modules/utils/dom/keyboard.js
type: application/javascript
module-type: utils
Keyboard utilities
Keyboard utilities; now deprecated. Instead, use $tw.keyboardManager
\*/
(function(){
Expand All @@ -12,60 +12,14 @@ Keyboard utilities
/*global $tw: false */
"use strict";

var namedKeys = {
"backspace": 8,
"tab": 9,
"enter": 13,
"escape": 27
};

/*
Parses a key descriptor into the structure:
{
keyCode: numeric keycode
shiftKey: boolean
altKey: boolean
ctrlKey: boolean
}
Key descriptors have the following format:
ctrl+enter
ctrl+shift+alt+A
*/
exports.parseKeyDescriptor = function(keyDescriptor) {
var components = keyDescriptor.split("+"),
info = {
keyCode: 0,
shiftKey: false,
altKey: false,
ctrlKey: false
};
for(var t=0; t<components.length; t++) {
var s = components[t].toLowerCase();
// Look for modifier keys
if(s === "ctrl") {
info.ctrlKey = true;
} else if(s === "shift") {
info.shiftKey = true;
} else if(s === "alt") {
info.altKey = true;
} else if(s === "meta") {
info.metaKey = true;
}
// Replace named keys with their code
if(namedKeys[s]) {
info.keyCode = namedKeys[s];
["parseKeyDescriptor","checkKeyDescriptor"].forEach(function(method) {
exports[method] = function() {
if($tw.keyboardManager) {
return $tw.keyboardManager[method].apply($tw.keyboardManager,Array.prototype.slice.call(arguments,0));
} else {
return null
}
}
return info;
};

exports.checkKeyDescriptor = function(event,keyInfo) {
var metaKeyStatus = !!keyInfo.metaKey; // Using a temporary variable to keep JSHint happy
return event.keyCode === keyInfo.keyCode &&
event.shiftKey === keyInfo.shiftKey &&
event.altKey === keyInfo.altKey &&
event.ctrlKey === keyInfo.ctrlKey &&
event.metaKey === metaKeyStatus;
};
};
});

})();
3 changes: 2 additions & 1 deletion core/modules/utils/dom/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Modal.prototype.display = function(title,options) {
var link = document.createElement("a");
link.setAttribute("href",tiddler.fields.help);
link.setAttribute("target","_blank");
link.setAttribute("rel","noopener noreferrer");
link.appendChild(document.createTextNode("Help"));
modalFooterHelp.appendChild(link);
modalFooterHelp.style.float = "left";
Expand All @@ -122,7 +123,7 @@ Modal.prototype.display = function(title,options) {
attributes: {
text: {
type: "string",
value: "Close"
value: $tw.language.getString("Buttons/Close/Caption")
}}}
]}],
parentWidget: $tw.rootWidget,
Expand Down
21 changes: 21 additions & 0 deletions core/modules/utils/filesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,25 @@ exports.isDirectoryEmpty = function(dirPath) {
return empty;
};

/*
Recursively delete a tree of empty directories
*/
exports.deleteEmptyDirs = function(dirpath,callback) {
var self = this;
fs.readdir(dirpath,function(err,files) {
if(err) {
return callback(err);
}
if(files.length > 0) {
return callback(null);
}
fs.rmdir(dirpath,function(err) {
if(err) {
return callback(err);
}
self.deleteEmptyDirs(path.dirname(dirpath),callback);
});
});
};

})();
2 changes: 1 addition & 1 deletion core/modules/utils/pluginmaker.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ exports.repackPlugin = function(title,additionalTiddlers,excludeTiddlers) {
try {
jsonPluginTiddler = JSON.parse(pluginTiddler.fields.text);
} catch(e) {
throw "Cannot parse plugin tiddler " + title + "\nError: " + e;
throw "Cannot parse plugin tiddler " + title + "\n" + $tw.language.getString("Error/Caption") + ": " + e;
}
// Get the list of tiddlers
var tiddlers = Object.keys(jsonPluginTiddler.tiddlers);
Expand Down
73 changes: 67 additions & 6 deletions core/modules/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@ Display a warning, in colour if we're on a terminal
*/
exports.warning = function(text) {
console.log($tw.node ? "\x1b[1;33m" + text + "\x1b[0m" : text);
}
};

/*
Repeats a string
*/
exports.repeat = function(str,count) {
var result = "";
for(var t=0;t<count;t++) {
result += str;
}
return result;
};

/*
Trim whitespace from the start and end of a string
Expand All @@ -31,6 +42,39 @@ exports.trim = function(str) {
}
};

/*
Find the line break preceding a given position in a string
Returns position immediately after that line break, or the start of the string
*/
exports.findPrecedingLineBreak = function(text,pos) {
var result = text.lastIndexOf("\n",pos - 1);
if(result === -1) {
result = 0;
} else {
result++;
if(text.charAt(result) === "\r") {
result++;
}
}
return result;
};

/*
Find the line break following a given position in a string
*/
exports.findFollowingLineBreak = function(text,pos) {
// Cut to just past the following line break, or to the end of the text
var result = text.indexOf("\n",pos);
if(result === -1) {
result = text.length;
} else {
if(text.charAt(result) === "\r") {
result++;
}
}
return result;
};

/*
Return the number of keys in an object
*/
Expand Down Expand Up @@ -383,17 +427,18 @@ exports.htmlEncode = function(s) {

// Converts all HTML entities to their character equivalents
exports.entityDecode = function(s) {
var e = s.substr(1,s.length-2); // Strip the & and the ;
var converter = String.fromCodePoint || String.fromCharCode,
e = s.substr(1,s.length-2); // Strip the & and the ;
if(e.charAt(0) === "#") {
if(e.charAt(1) === "x" || e.charAt(1) === "X") {
return String.fromCharCode(parseInt(e.substr(2),16));
return converter(parseInt(e.substr(2),16));
} else {
return String.fromCharCode(parseInt(e.substr(1),10));
return converter(parseInt(e.substr(1),10));
}
} else {
var c = $tw.config.htmlEntities[e];
if(c) {
return String.fromCharCode(c);
return converter(c);
} else {
return s; // Couldn't convert it as an entity, just return it raw
}
Expand Down Expand Up @@ -450,7 +495,7 @@ exports.escapeRegExp = function(s) {

// Checks whether a link target is external, i.e. not a tiddler title
exports.isLinkExternal = function(to) {
var externalRegExp = /(?:file|http|https|mailto|ftp|irc|news|data|skype):[^\s<>{}\[\]`|'"\\^~]+(?:\/|\b)/i;
var externalRegExp = /^(?:file|http|https|mailto|ftp|irc|news|data|skype):[^\s<>{}\[\]`|"\\^~]+(?:\/|\b)$/i;
return externalRegExp.test(to);
};

Expand Down Expand Up @@ -654,4 +699,20 @@ exports.sign = Math.sign || function(x) {
return x > 0 ? 1 : -1;
};

/*
IE does not have an endsWith function
*/
exports.strEndsWith = function(str,ending,position) {
if(str.endsWith) {
return str.endsWith(ending,position);
} else {
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > str.length) {
position = str.length;
}
position -= str.length;
var lastIndex = str.indexOf(ending, position);
return lastIndex !== -1 && lastIndex === position;
}
};

})();
4 changes: 4 additions & 0 deletions core/modules/widgets/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ ButtonWidget.prototype.render = function(parent,nextSibling) {
self.setTiddler();
handled = true;
}
if(self.actions) {
self.invokeActionString(self.actions,self,event);
}
if(handled) {
event.preventDefault();
event.stopPropagation();
Expand Down Expand Up @@ -153,6 +156,7 @@ Compute the internal state of the widget
*/
ButtonWidget.prototype.execute = function() {
// Get attributes
this.actions = this.getAttribute("actions");
this.to = this.getAttribute("to");
this.message = this.getAttribute("message");
this.param = this.getAttribute("param");
Expand Down
117 changes: 59 additions & 58 deletions core/modules/widgets/edit-bitmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@ Edit-bitmap widget
"use strict";

// Default image sizes
var DEFAULT_IMAGE_WIDTH = 300,
DEFAULT_IMAGE_HEIGHT = 185;
var DEFAULT_IMAGE_WIDTH = 600,
DEFAULT_IMAGE_HEIGHT = 370;

// Configuration tiddlers
var LINE_WIDTH_TITLE = "$:/config/BitmapEditor/LineWidth",
LINE_COLOUR_TITLE = "$:/config/BitmapEditor/Colour";
LINE_COLOUR_TITLE = "$:/config/BitmapEditor/Colour",
LINE_OPACITY_TITLE = "$:/config/BitmapEditor/Opacity";

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

var EditBitmapWidget = function(parseTreeNode,options) {
// Initialise the editor operations if they've not been done already
if(!this.editorOperations) {
EditBitmapWidget.prototype.editorOperations = {};
$tw.modules.applyMethods("bitmapeditoroperation",this.editorOperations);
}
this.initialise(parseTreeNode,options);
};

Expand All @@ -42,7 +48,12 @@ EditBitmapWidget.prototype.render = function(parent,nextSibling) {
this.computeAttributes();
// Execute our logic
this.execute();
// Create our element
// Create the wrapper for the toolbar and render its content
this.toolbarNode = this.document.createElement("div");
this.toolbarNode.className = "tc-editor-toolbar";
parent.insertBefore(this.toolbarNode,nextSibling);
this.domNodes.push(this.toolbarNode);
// Create the on-screen canvas
this.canvasDomNode = $tw.utils.domMaker("canvas",{
document: this.document,
"class":"tc-edit-bitmapeditor",
Expand All @@ -60,29 +71,33 @@ EditBitmapWidget.prototype.render = function(parent,nextSibling) {
name: "mouseup", handlerObject: this, handlerMethod: "handleMouseUpEvent"
}]
});
this.widthDomNode = $tw.utils.domMaker("input",{
document: this.document,
"class":"tc-edit-bitmapeditor-width",
eventListeners: [{
name: "change", handlerObject: this, handlerMethod: "handleWidthChangeEvent"
}]
});
this.heightDomNode = $tw.utils.domMaker("input",{
document: this.document,
"class":"tc-edit-bitmapeditor-height",
eventListeners: [{
name: "change", handlerObject: this, handlerMethod: "handleHeightChangeEvent"
}]
});
// Insert the elements into the DOM
// Set the width and height variables
this.setVariable("tv-bitmap-editor-width",this.canvasDomNode.width + "px");
this.setVariable("tv-bitmap-editor-height",this.canvasDomNode.height + "px");
// Render toolbar child widgets
this.renderChildren(this.toolbarNode,null);
// // Insert the elements into the DOM
parent.insertBefore(this.canvasDomNode,nextSibling);
parent.insertBefore(this.widthDomNode,nextSibling);
parent.insertBefore(this.heightDomNode,nextSibling);
this.domNodes.push(this.canvasDomNode,this.widthDomNode,this.heightDomNode);
this.domNodes.push(this.canvasDomNode);
// Load the image into the canvas
if($tw.browser) {
this.loadCanvas();
}
// Add widget message listeners
this.addEventListeners([
{type: "tm-edit-bitmap-operation", handler: "handleEditBitmapOperationMessage"}
]);
};

/*
Handle an edit bitmap operation message from the toolbar
*/
EditBitmapWidget.prototype.handleEditBitmapOperationMessage = function(event) {
// Invoke the handler
var handler = this.editorOperations[event.param];
if(handler) {
handler.call(this,event);
}
};

/*
Expand All @@ -91,13 +106,28 @@ Compute the internal state of the widget
EditBitmapWidget.prototype.execute = function() {
// Get our parameters
this.editTitle = this.getAttribute("tiddler",this.getVariable("currentTiddler"));
// Make the child widgets
this.makeChildWidgets();
};

/*
Note that the bitmap editor intentionally doesn't try to refresh itself because it would be confusing to have the image changing spontaneously while editting it
Just refresh the toolbar
*/
EditBitmapWidget.prototype.refresh = function(changedTiddlers) {
return false;
return this.refreshChildren(changedTiddlers);
};

/*
Set the bitmap size variables and refresh the toolbar
*/
EditBitmapWidget.prototype.refreshToolbar = function() {
// Set the width and height variables
this.setVariable("tv-bitmap-editor-width",this.canvasDomNode.width + "px");
this.setVariable("tv-bitmap-editor-height",this.canvasDomNode.height + "px");
// Refresh each of our child widgets
$tw.utils.each(this.children,function(childWidget) {
childWidget.refreshSelf();
});
};

EditBitmapWidget.prototype.loadCanvas = function() {
Expand All @@ -112,7 +142,7 @@ EditBitmapWidget.prototype.loadCanvas = function() {
self.currCanvas = self.document.createElement("canvas");
self.initCanvas(self.currCanvas,currImage.width,currImage.height,currImage);
// Set the width and height input boxes
self.updateSize();
self.refreshToolbar();
};
currImage.onerror = function() {
// Set the on-screen canvas size and clear it
Expand All @@ -121,7 +151,7 @@ EditBitmapWidget.prototype.loadCanvas = function() {
self.currCanvas = self.document.createElement("canvas");
self.initCanvas(self.currCanvas,DEFAULT_IMAGE_WIDTH,DEFAULT_IMAGE_HEIGHT);
// Set the width and height input boxes
self.updateSize();
self.refreshToolbar();
};
// Get the current bitmap into an image object
currImage.src = "data:" + tiddler.fields.type + ";base64," + tiddler.fields.text;
Expand All @@ -139,14 +169,6 @@ EditBitmapWidget.prototype.initCanvas = function(canvas,width,height,image) {
}
};

/*
** Update the input boxes with the actual size of the canvas
*/
EditBitmapWidget.prototype.updateSize = function() {
this.widthDomNode.value = this.currCanvas.width;
this.heightDomNode.value = this.currCanvas.height;
};

/*
** Change the size of the canvas, preserving the current image
*/
Expand All @@ -167,28 +189,6 @@ EditBitmapWidget.prototype.changeCanvasSize = function(newWidth,newHeight) {
ctx.drawImage(this.currCanvas,0,0);
};

EditBitmapWidget.prototype.handleWidthChangeEvent = function(event) {
// Get the new width
var newWidth = parseInt(this.widthDomNode.value,10);
// Update if necessary
if(newWidth > 0 && newWidth !== this.currCanvas.width) {
this.changeCanvasSize(newWidth,this.currCanvas.height);
}
// Update the input controls
this.updateSize();
};

EditBitmapWidget.prototype.handleHeightChangeEvent = function(event) {
// Get the new width
var newHeight = parseInt(this.heightDomNode.value,10);
// Update if necessary
if(newHeight > 0 && newHeight !== this.currCanvas.height) {
this.changeCanvasSize(this.currCanvas.width,newHeight);
}
// Update the input controls
this.updateSize();
};

EditBitmapWidget.prototype.handleTouchStartEvent = function(event) {
this.brushDown = true;
this.strokeStart(event.touches[0].clientX,event.touches[0].clientY);
Expand Down Expand Up @@ -264,8 +264,9 @@ EditBitmapWidget.prototype.strokeMove = function(x,y) {
// Redraw the previous image
ctx.drawImage(this.currCanvas,0,0);
// Render the stroke
ctx.globalAlpha = parseFloat(this.wiki.getTiddlerText(LINE_OPACITY_TITLE,"1.0"));
ctx.strokeStyle = this.wiki.getTiddlerText(LINE_COLOUR_TITLE,"#ff0");
ctx.lineWidth = parseInt(this.wiki.getTiddlerText(LINE_WIDTH_TITLE,"3"),10);
ctx.lineWidth = parseFloat(this.wiki.getTiddlerText(LINE_WIDTH_TITLE,"3"));
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.beginPath();
Expand All @@ -292,7 +293,7 @@ EditBitmapWidget.prototype.saveChanges = function() {
var tiddler = this.wiki.getTiddler(this.editTitle);
if(tiddler) {
// data URIs look like "data:<type>;base64,<text>"
var dataURL = this.canvasDomNode.toDataURL(tiddler.fields.type,1.0),
var dataURL = this.canvasDomNode.toDataURL(tiddler.fields.type),
posColon = dataURL.indexOf(":"),
posSemiColon = dataURL.indexOf(";"),
posComma = dataURL.indexOf(","),
Expand Down
139 changes: 139 additions & 0 deletions core/modules/widgets/edit-shortcut.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*\
title: $:/core/modules/widgets/edit-shortcut.js
type: application/javascript
module-type: widget
Widget to display an editable keyboard shortcut
\*/
(function(){

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

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

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

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

/*
Render this widget into the DOM
*/
EditShortcutWidget.prototype.render = function(parent,nextSibling) {
this.parentDomNode = parent;
this.computeAttributes();
this.execute();
this.inputNode = this.document.createElement("input");
// Assign classes
if(this.shortcutClass) {
this.inputNode.className = this.shortcutClass;
}
// Assign other attributes
if(this.shortcutStyle) {
this.inputNode.setAttribute("style",this.shortcutStyle);
}
if(this.shortcutTooltip) {
this.inputNode.setAttribute("title",this.shortcutTooltip);
}
if(this.shortcutPlaceholder) {
this.inputNode.setAttribute("placeholder",this.shortcutPlaceholder);
}
if(this.shortcutAriaLabel) {
this.inputNode.setAttribute("aria-label",this.shortcutAriaLabel);
}
// Assign the current shortcut
this.updateInputNode();
// Add event handlers
$tw.utils.addEventListeners(this.inputNode,[
{name: "keydown", handlerObject: this, handlerMethod: "handleKeydownEvent"}
]);
// Link into the DOM
parent.insertBefore(this.inputNode,nextSibling);
this.domNodes.push(this.inputNode);
};

/*
Compute the internal state of the widget
*/
EditShortcutWidget.prototype.execute = function() {
this.shortcutTiddler = this.getAttribute("tiddler");
this.shortcutField = this.getAttribute("field");
this.shortcutIndex = this.getAttribute("index");
this.shortcutPlaceholder = this.getAttribute("placeholder");
this.shortcutDefault = this.getAttribute("default","");
this.shortcutClass = this.getAttribute("class");
this.shortcutStyle = this.getAttribute("style");
this.shortcutTooltip = this.getAttribute("tooltip");
this.shortcutAriaLabel = this.getAttribute("aria-label");
};

/*
Update the value of the input node
*/
EditShortcutWidget.prototype.updateInputNode = function() {
if(this.shortcutField) {
var tiddler = this.wiki.getTiddler(this.shortcutTiddler);
if(tiddler && $tw.utils.hop(tiddler.fields,this.shortcutField)) {
this.inputNode.value = tiddler.getFieldString(this.shortcutField);
} else {
this.inputNode.value = this.shortcutDefault;
}
} else if(this.shortcutIndex) {
this.inputNode.value = this.wiki.extractTiddlerDataItem(this.shortcutTiddler,this.shortcutIndex,this.shortcutDefault);
} else {
this.inputNode.value = this.wiki.getTiddlerText(this.shortcutTiddler,this.shortcutDefault);
}
};

/*
Handle a dom "keydown" event
*/
EditShortcutWidget.prototype.handleKeydownEvent = function(event) {
// Ignore shift, ctrl, meta, alt
if(event.keyCode && $tw.keyboardManager.getModifierKeys().indexOf(event.keyCode) === -1) {
// Get the shortcut text representation
var value = $tw.keyboardManager.getPrintableShortcuts([{
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey,
metaKey: event.metaKey,
keyCode: event.keyCode
}]);
if(value.length > 0) {
this.wiki.setText(this.shortcutTiddler,this.shortcutField,this.shortcutIndex,value[0]);
}
// Ignore the keydown if it was already handled
event.preventDefault();
event.stopPropagation();
return true;
} else {
return false;
}
};

/*
Selectively refreshes the widget if needed. Returns true if the widget needed re-rendering
*/
EditShortcutWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes.placeholder || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.style || changedAttributes.tooltip || changedAttributes["aria-label"]) {
this.refreshSelf();
return true;
} else if(changedTiddlers[this.shortcutTiddler]) {
this.updateInputNode();
return true;
} else {
return false;
}
};

exports["edit-shortcut"] = EditShortcutWidget;

})();
278 changes: 4 additions & 274 deletions core/modules/widgets/edit-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,280 +12,10 @@ Edit-text widget
/*global $tw: false */
"use strict";

var DEFAULT_MIN_TEXT_AREA_HEIGHT = "100px"; // Minimum height of textareas in pixels
var editTextWidgetFactory = require("$:/core/modules/editor/factory.js").editTextWidgetFactory,
FramedEngine = require("$:/core/modules/editor/engines/framed.js").FramedEngine,
SimpleEngine = require("$:/core/modules/editor/engines/simple.js").SimpleEngine;

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

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

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

/*
Render this widget into the DOM
*/
EditTextWidget.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();
// Create our element
var editInfo = this.getEditInfo(),
tag = this.editTag;
if($tw.config.htmlUnsafeElements.indexOf(tag) !== -1) {
tag = "input";
}
var domNode = this.document.createElement(tag);
if(this.editType) {
domNode.setAttribute("type",this.editType);
}
if(editInfo.value === "" && this.editPlaceholder) {
domNode.setAttribute("placeholder",this.editPlaceholder);
}
if(this.editSize) {
domNode.setAttribute("size",this.editSize);
}
if(this.editRows) {
domNode.setAttribute("rows",this.editRows);
}
// Assign classes
if(this.editClass) {
domNode.className = this.editClass;
}
// Set the text
if(this.editTag === "textarea") {
domNode.appendChild(this.document.createTextNode(editInfo.value));
} else {
domNode.value = editInfo.value;
}
// Add an input event handler
$tw.utils.addEventListeners(domNode,[
{name: "focus", handlerObject: this, handlerMethod: "handleFocusEvent"},
{name: "input", handlerObject: this, handlerMethod: "handleInputEvent"}
]);
// Insert the element into the DOM
parent.insertBefore(domNode,nextSibling);
this.domNodes.push(domNode);
if(this.postRender) {
this.postRender();
}
// Fix height
this.fixHeight();
// Focus field
if(this.editFocus === "true") {
if(domNode.focus && domNode.select) {
domNode.focus();
domNode.select();
}
}
};

/*
Get the tiddler being edited and current value
*/
EditTextWidget.prototype.getEditInfo = function() {
// Get the edit value
var self = this,
value,
update;
if(this.editIndex) {
value = this.wiki.extractTiddlerDataItem(this.editTitle,this.editIndex,this.editDefault);
update = function(value) {
var data = self.wiki.getTiddlerData(self.editTitle,{});
if(data[self.editIndex] !== value) {
data[self.editIndex] = value;
self.wiki.setTiddlerData(self.editTitle,data);
}
};
} else {
// Get the current tiddler and the field name
var tiddler = this.wiki.getTiddler(this.editTitle);
if(tiddler) {
// If we've got a tiddler, the value to display is the field string value
value = tiddler.getFieldString(this.editField);
} else {
// Otherwise, we need to construct a default value for the editor
switch(this.editField) {
case "text":
value = "Type the text for the tiddler '" + this.editTitle + "'";
break;
case "title":
value = this.editTitle;
break;
default:
value = "";
break;
}
if(this.editDefault !== undefined) {
value = this.editDefault;
}
}
update = function(value) {
var tiddler = self.wiki.getTiddler(self.editTitle),
updateFields = {
title: self.editTitle
};
updateFields[self.editField] = value;
self.wiki.addTiddler(new $tw.Tiddler(self.wiki.getCreationFields(),tiddler,updateFields,self.wiki.getModificationFields()));
};
}
return {value: value, update: update};
};

/*
Compute the internal state of the widget
*/
EditTextWidget.prototype.execute = function() {
// Get our parameters
this.editTitle = this.getAttribute("tiddler",this.getVariable("currentTiddler"));
this.editField = this.getAttribute("field","text");
this.editIndex = this.getAttribute("index");
this.editDefault = this.getAttribute("default");
this.editClass = this.getAttribute("class");
this.editPlaceholder = this.getAttribute("placeholder");
this.editSize = this.getAttribute("size");
this.editRows = this.getAttribute("rows");
this.editAutoHeight = this.getAttribute("autoHeight","yes") === "yes";
this.editMinHeight = this.getAttribute("minHeight",DEFAULT_MIN_TEXT_AREA_HEIGHT);
this.editFocusPopup = this.getAttribute("focusPopup");
this.editFocus = this.getAttribute("focus");
// Get the editor element tag and type
var tag,type;
if(this.editField === "text") {
tag = "textarea";
} else {
tag = "input";
var fieldModule = $tw.Tiddler.fieldModules[this.editField];
if(fieldModule && fieldModule.editTag) {
tag = fieldModule.editTag;
}
if(fieldModule && fieldModule.editType) {
type = fieldModule.editType;
}
type = type || "text";
}
// Get the rest of our parameters
this.editTag = this.getAttribute("tag",tag);
this.editType = this.getAttribute("type",type);
};

/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
EditTextWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
// Completely rerender if any of our attributes have changed
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows) {
this.refreshSelf();
return true;
} else if(changedTiddlers[this.editTitle]) {
this.updateEditor(this.getEditInfo().value);
return true;
}
// Fix the height anyway in case there has been a reflow
this.fixHeight();
return false;
};

/*
Update the editor with new text. This method is separate from updateEditorDomNode()
so that subclasses can override updateEditor() and still use updateEditorDomNode()
*/
EditTextWidget.prototype.updateEditor = function(text) {
this.updateEditorDomNode(text);
};

/*
Update the editor dom node with new text
*/
EditTextWidget.prototype.updateEditorDomNode = function(text) {
// Replace the edit value if the tiddler we're editing has changed
var domNode = this.domNodes[0];
if(!domNode.isTiddlyWikiFakeDom) {
if(this.document.activeElement !== domNode) {
domNode.value = text;
}
// Fix the height if needed
this.fixHeight();
}
};

/*
Get the first parent element that has scrollbars or use the body as fallback.
*/
EditTextWidget.prototype.getScrollContainer = function(el) {
while(el.parentNode) {
el = el.parentNode;
if(el.scrollTop) {
return el;
}
}
return this.document.body;
};

/*
Fix the height of textareas to fit their content
*/
EditTextWidget.prototype.fixHeight = function() {
var domNode = this.domNodes[0];
if(this.editAutoHeight && domNode && !domNode.isTiddlyWikiFakeDom && this.editTag === "textarea") {
// Resize the textarea to fit its content, preserving scroll position
// Get the scroll container and register the current scroll position
var container = this.getScrollContainer(domNode),
scrollTop = container.scrollTop;
// Measure the specified minimum height
domNode.style.height = this.editMinHeight;
var minHeight = domNode.offsetHeight;
// Set its height to auto so that it snaps to the correct height
domNode.style.height = "auto";
// Calculate the revised height
var newHeight = Math.max(domNode.scrollHeight + domNode.offsetHeight - domNode.clientHeight,minHeight);
// Only try to change the height if it has changed
if(newHeight !== domNode.offsetHeight) {
domNode.style.height = newHeight + "px";
// Make sure that the dimensions of the textarea are recalculated
$tw.utils.forceLayout(domNode);
// Set the container to the position we registered at the beginning
container.scrollTop = scrollTop;
}
}
};

/*
Handle a dom "input" event
*/
EditTextWidget.prototype.handleInputEvent = function(event) {
this.saveChanges(this.domNodes[0].value);
this.fixHeight();
return true;
};

EditTextWidget.prototype.handleFocusEvent = function(event) {
if(this.editFocusPopup) {
$tw.popup.triggerPopup({
domNode: this.domNodes[0],
title: this.editFocusPopup,
wiki: this.wiki,
force: true
});
}
return true;
};

EditTextWidget.prototype.saveChanges = function(text) {
var editInfo = this.getEditInfo();
if(text !== editInfo.value) {
editInfo.update(text);
}
};

exports["edit-text"] = EditTextWidget;
exports["edit-text"] = editTextWidgetFactory(FramedEngine,SimpleEngine);

})();
3 changes: 2 additions & 1 deletion core/modules/widgets/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ EditWidget.prototype.execute = function() {
index: {type: "string", value: this.editIndex},
"class": {type: "string", value: this.editClass},
"placeholder": {type: "string", value: this.editPlaceholder}
}
},
children: this.parseTreeNode.children
}]);
};

Expand Down
11 changes: 9 additions & 2 deletions core/modules/widgets/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ Render this widget into the DOM
EntityWidget.prototype.render = function(parent,nextSibling) {
this.parentDomNode = parent;
this.execute();
var textNode = this.document.createTextNode($tw.utils.entityDecode(this.parseTreeNode.entity));
var entityString = this.getAttribute("entity",this.parseTreeNode.entity || ""),
textNode = this.document.createTextNode($tw.utils.entityDecode(entityString));
parent.insertBefore(textNode,nextSibling);
this.domNodes.push(textNode);
};
Expand All @@ -44,7 +45,13 @@ EntityWidget.prototype.execute = function() {
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
EntityWidget.prototype.refresh = function(changedTiddlers) {
return false;
var changedAttributes = this.computeAttributes();
if(changedAttributes.entity) {
this.refreshSelf();
return true;
} else {
return false;
}
};

exports.entity = EntityWidget;
Expand Down
3 changes: 3 additions & 0 deletions core/modules/widgets/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ ImageWidget.prototype.render = function(parent,nextSibling) {
src = _canonical_uri;
break;
}
} else {
// Just trigger loading of the tiddler
this.wiki.getTiddlerText(this.imageSource);
}
}
}
Expand Down
10 changes: 7 additions & 3 deletions core/modules/widgets/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ KeyboardWidget.prototype.render = function(parent,nextSibling) {
domNode.className = classes.join(" ");
// Add a keyboard event handler
domNode.addEventListener("keydown",function (event) {
if($tw.utils.checkKeyDescriptor(event,self.keyInfo)) {
self.invokeActions(this,event);
if($tw.keyboardManager.checkKeyDescriptors(event,self.keyInfoArray)) {
self.invokeActions(self,event);
if(self.actions) {
self.invokeActionString(self.actions,self,event);
}
self.dispatchMessage(event);
event.preventDefault();
event.stopPropagation();
Expand All @@ -65,10 +68,11 @@ Compute the internal state of the widget
*/
KeyboardWidget.prototype.execute = function() {
// Get attributes
this.actions = this.getAttribute("actions");
this.message = this.getAttribute("message");
this.param = this.getAttribute("param");
this.key = this.getAttribute("key");
this.keyInfo = $tw.utils.parseKeyDescriptor(this.key);
this.keyInfoArray = $tw.keyboardManager.parseKeyDescriptors(this.key);
this["class"] = this.getAttribute("class");
// Make child widgets
this.makeChildWidgets();
Expand Down
9 changes: 6 additions & 3 deletions core/modules/widgets/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Link widget
"use strict";

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

var LinkWidget = function(parseTreeNode,options) {
this.initialise(parseTreeNode,options);
Expand All @@ -35,9 +36,10 @@ LinkWidget.prototype.render = function(parent,nextSibling) {
this.execute();
// Get the value of the tv-wikilinks configuration macro
var wikiLinksMacro = this.getVariable("tv-wikilinks"),
useWikiLinks = wikiLinksMacro ? (wikiLinksMacro.trim() !== "no") : true;
useWikiLinks = wikiLinksMacro ? (wikiLinksMacro.trim() !== "no") : true,
missingLinksEnabled = !(this.hideMissingLinks && this.isMissing && !this.isShadow);
// Render the link if required
if(useWikiLinks) {
if(useWikiLinks && missingLinksEnabled) {
this.renderLink(parent,nextSibling);
} else {
// Just insert the link text
Expand Down Expand Up @@ -216,6 +218,7 @@ LinkWidget.prototype.execute = function() {
// Determine the link characteristics
this.isMissing = !this.wiki.tiddlerExists(this.to);
this.isShadow = this.wiki.isShadowTiddler(this.to);
this.hideMissingLinks = ($tw.wiki.getTiddlerText(MISSING_LINK_CONFIG_TITLE,"yes") === "no");
// Make the child widgets
this.makeChildWidgets();
};
Expand All @@ -225,7 +228,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
LinkWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.to || changedTiddlers[this.to] || changedAttributes["aria-label"] || changedAttributes.tooltip) {
if(changedAttributes.to || changedTiddlers[this.to] || changedAttributes["aria-label"] || changedAttributes.tooltip || changedTiddlers[MISSING_LINK_CONFIG_TITLE]) {
this.refreshSelf();
return true;
}
Expand Down
4 changes: 4 additions & 0 deletions core/modules/widgets/linkcatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ LinkCatcherWidget.prototype.execute = function() {
this.catchMessage = this.getAttribute("message");
this.catchSet = this.getAttribute("set");
this.catchSetTo = this.getAttribute("setTo");
this.catchActions = this.getAttribute("actions");
// Construct the child widgets
this.makeChildWidgets();
};
Expand Down Expand Up @@ -80,6 +81,9 @@ LinkCatcherWidget.prototype.handleNavigateEvent = function(event) {
var tiddler = this.wiki.getTiddler(this.catchSet);
this.wiki.addTiddler(new $tw.Tiddler(tiddler,{title: this.catchSet, text: this.catchSetTo}));
}
if(this.catchActions) {
this.invokeActionString(this.catchActions,this);
}
return false;
};

Expand Down
2 changes: 1 addition & 1 deletion core/modules/widgets/navigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ NavigatorWidget.prototype.handlePerformImportEvent = function(event) {
importData = this.wiki.getTiddlerDataCached(event.param,{tiddlers: {}}),
importReport = [];
// Add the tiddlers to the store
importReport.push($tw.language.getString("Import/Imported") + "\n");
importReport.push($tw.language.getString("Import/Imported/Hint") + "\n");
$tw.utils.each(importData.tiddlers,function(tiddlerFields) {
var title = tiddlerFields.title;
if(title && importTiddler && importTiddler.fields["selection-" + title] !== "unchecked") {
Expand Down
6 changes: 6 additions & 0 deletions core/modules/widgets/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,18 @@ SelectWidget.prototype.render = function(parent,nextSibling) {
Handle a change event
*/
SelectWidget.prototype.handleChangeEvent = function(event) {
// Get the new value and assign it to the tiddler
if(this.selectMultiple == false) {
var value = this.getSelectDomNode().value;
} else {
var value = this.getSelectValues()
value = $tw.utils.stringifyList(value);
}
this.wiki.setText(this.selectTitle,this.selectField,this.selectIndex,value);
// Trigger actions
if(this.selectActions) {
this.invokeActionString(this.selectActions,this,event);
}
};

/*
Expand Down Expand Up @@ -132,6 +137,7 @@ Compute the internal state of the widget
*/
SelectWidget.prototype.execute = function() {
// Get our parameters
this.selectActions = this.getAttribute("actions");
this.selectTitle = this.getAttribute("tiddler",this.getVariable("currentTiddler"));
this.selectField = this.getAttribute("field","text");
this.selectIndex = this.getAttribute("index");
Expand Down
2 changes: 1 addition & 1 deletion core/modules/widgets/transclude.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ TranscludeWidget.prototype.execute = function() {
parseTreeNodes = [{type: "element", tag: "span", attributes: {
"class": {type: "string", value: "tc-error"}
}, children: [
{type: "text", text: "Recursive transclusion error in transclude widget"}
{type: "text", text: $tw.language.getString("Error/RecursiveTransclusion")}
]}];
}
}
Expand Down
7 changes: 7 additions & 0 deletions core/modules/widgets/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ ViewWidget.prototype.execute = function() {
case "htmlwikified":
this.text = this.getValueAsHtmlWikified();
break;
case "plainwikified":
this.text = this.getValueAsPlainWikified();
break;
case "htmlencodedplainwikified":
this.text = this.getValueAsHtmlEncodedPlainWikified();
break;
Expand Down Expand Up @@ -135,6 +138,10 @@ ViewWidget.prototype.getValueAsHtmlWikified = function() {
return this.wiki.renderText("text/html","text/vnd.tiddlywiki",this.getValueAsText(),{parentWidget: this});
};

ViewWidget.prototype.getValueAsPlainWikified = function() {
return this.wiki.renderText("text/plain","text/vnd.tiddlywiki",this.getValueAsText(),{parentWidget: this});
};

ViewWidget.prototype.getValueAsHtmlEncodedPlainWikified = function() {
return $tw.utils.htmlEncode(this.wiki.renderText("text/plain","text/vnd.tiddlywiki",this.getValueAsText(),{parentWidget: this}));
};
Expand Down
17 changes: 17 additions & 0 deletions core/modules/widgets/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,23 @@ Widget.prototype.invokeActions = function(triggeringWidget,event) {
return handled;
};

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

Widget.prototype.allowActionPropagation = function() {
return true;
Expand Down
154 changes: 154 additions & 0 deletions core/modules/widgets/wikify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*\
title: $:/core/modules/widgets/wikify.js
type: application/javascript
module-type: widget
Widget to wikify text into a variable
\*/
(function(){

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

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

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

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

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

/*
Compute the internal state of the widget
*/
WikifyWidget.prototype.execute = function() {
// Get our parameters
this.wikifyName = this.getAttribute("name");
this.wikifyText = this.getAttribute("text");
this.wikifyType = this.getAttribute("type");
this.wikifyMode = this.getAttribute("mode","block");
this.wikifyOutput = this.getAttribute("output","text");
// Create the parse tree
this.wikifyParser = this.wiki.parseText(this.wikifyType,this.wikifyText,{
parseAsInline: this.wikifyMode === "inline"
});
// Create the widget tree
this.wikifyWidgetNode = this.wiki.makeWidget(this.wikifyParser,{
document: $tw.fakeDocument,
parentWidget: this
});
// Render the widget tree to the container
this.wikifyContainer = $tw.fakeDocument.createElement("div");
this.wikifyWidgetNode.render(this.wikifyContainer,null);
this.wikifyResult = this.getResult();
// Set context variable
this.setVariable(this.wikifyName,this.wikifyResult);
// Construct the child widgets
this.makeChildWidgets();
};

/*
Return the result string
*/
WikifyWidget.prototype.getResult = function() {
var result;
switch(this.wikifyOutput) {
case "text":
result = this.wikifyContainer.textContent;
break;
case "html":
result = this.wikifyContainer.innerHTML;
break;
case "parsetree":
result = JSON.stringify(this.wikifyParser.tree,0,$tw.config.preferences.jsonSpaces);
break;
case "widgettree":
result = JSON.stringify(this.getWidgetTree(),0,$tw.config.preferences.jsonSpaces);
break;
}
return result;
};

/*
Return a string of the widget tree
*/
WikifyWidget.prototype.getWidgetTree = function() {
var copyNode = function(widgetNode,resultNode) {
var type = widgetNode.parseTreeNode.type;
resultNode.type = type;
switch(type) {
case "element":
resultNode.tag = widgetNode.parseTreeNode.tag;
break;
case "text":
resultNode.text = widgetNode.parseTreeNode.text;
break;
}
if(Object.keys(widgetNode.attributes || {}).length > 0) {
resultNode.attributes = {};
$tw.utils.each(widgetNode.attributes,function(attr,attrName) {
resultNode.attributes[attrName] = widgetNode.getAttribute(attrName);
});
}
if(Object.keys(widgetNode.children || {}).length > 0) {
resultNode.children = [];
$tw.utils.each(widgetNode.children,function(widgetChildNode) {
var node = {};
resultNode.children.push(node);
copyNode(widgetChildNode,node);
});
}
},
results = {};
copyNode(this.wikifyWidgetNode,results);
return results;
};

/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
WikifyWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
// Refresh ourselves entirely if any of our attributes have changed
if(changedAttributes.name || changedAttributes.text || changedAttributes.type || changedAttributes.mode || changedAttributes.output) {
this.refreshSelf();
return true;
} else {
// Refresh the widget tree
if(this.wikifyWidgetNode.refresh(changedTiddlers)) {
// Check if there was any change
var result = this.getResult();
if(result !== this.wikifyResult) {
// If so, save the change
this.wikifyResult = result;
this.setVariable(this.wikifyName,this.wikifyResult);
// Refresh each of our child widgets
$tw.utils.each(this.children,function(childWidget) {
childWidget.refreshSelf();
});
return true;
}
}
// Just refresh the children
return this.refreshChildren(changedTiddlers);
}
};

exports.wikify = WikifyWidget;

})();
13 changes: 7 additions & 6 deletions core/modules/wiki.js
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,7 @@ Options include:
_canonical_uri: optional string of the canonical URI of this content
*/
exports.parseText = function(type,text,options) {
text = text || "";
options = options || {};
// Select a parser
var Parser = $tw.Wiki.parsers[type];
Expand Down Expand Up @@ -1164,19 +1165,19 @@ exports.findDraft = function(targetTitle) {
}

/*
Check whether the specified draft tiddler has been modified
Check whether the specified draft tiddler has been modified.
If the original tiddler doesn't exist, create a vanilla tiddler variable,
to check if additional fields have been added.
*/
exports.isDraftModified = function(title) {
var tiddler = this.getTiddler(title);
if(!tiddler.isDraft()) {
return false;
}
var ignoredFields = ["created", "modified", "title", "draft.title", "draft.of"],
origTiddler = this.getTiddler(tiddler.fields["draft.of"]);
if(!origTiddler) {
return tiddler.fields.text !== "";
}
return tiddler.fields["draft.title"] !== tiddler.fields["draft.of"] || !tiddler.isEqual(origTiddler,ignoredFields);
origTiddler = this.getTiddler(tiddler.fields["draft.of"]) || new $tw.Tiddler({text:"", tags:[]}),
titleModified = tiddler.fields["draft.title"] !== tiddler.fields["draft.of"];
return titleModified || !tiddler.isEqual(origTiddler,ignoredFields);
};

/*
Expand Down
7 changes: 7 additions & 0 deletions core/templates/raw-static-tiddler.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
title: $:/core/templates/raw-static-tiddler

<!--

This template is used for saving tiddlers as static HTML

--><$view field="text" format="plainwikified" />
1 change: 1 addition & 0 deletions core/templates/static.area.tid
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
title: $:/core/templates/static.area

<$reveal type="nomatch" state="$:/isEncrypted" text="yes">
{{{ [all[shadows+tiddlers]tag[$:/tags/RawStaticContent]!has[draft.of]] ||$:/core/templates/raw-static-tiddler}}}
{{$:/core/templates/static.content||$:/core/templates/html-tiddler}}
</$reveal>
<$reveal type="match" state="$:/isEncrypted" text="yes">
Expand Down
1 change: 1 addition & 0 deletions core/templates/static.template.html.tid
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type: text/vnd.tiddlywiki-html
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="format-detection" content="telephone=no">
<link id="faviconLink" rel="shortcut icon" href="favicon.ico">
<title>{{$:/core/wiki/title}}</title>
Expand Down
1 change: 1 addition & 0 deletions core/templates/static.tiddler.html.tid
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ title: $:/core/templates/static.tiddler.html
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="format-detection" content="telephone=no">
<link id="faviconLink" rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" href="static.css">
Expand Down
1 change: 1 addition & 0 deletions core/templates/tiddlywiki5.html.tid
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ title: $:/core/templates/tiddlywiki5.html
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="format-detection" content="telephone=no" />
<meta name="copyright" content="{{$:/core/copyright.txt}}" />
<link id="faviconLink" rel="shortcut icon" href="favicon.ico">
Expand Down
24 changes: 1 addition & 23 deletions core/ui/AdvancedSearch/Filter.tid
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,13 @@ tags: $:/tags/AdvancedSearch
caption: {{$:/language/Search/Filter/Caption}}

\define lingo-base() $:/language/Search/
<$linkcatcher to="$:/temp/advancedsearch">

<<lingo Filter/Hint>>

<div class="tc-search tc-advanced-search">
<$edit-text tiddler="$:/temp/advancedsearch" type="search" tag="input"/>
<$button popup=<<qualify "$:/state/filterDropdown">> class="tc-btn-invisible">
{{$:/core/images/down-arrow}}
</$button>
<$reveal state="$:/temp/advancedsearch" type="nomatch" text="">
<$button class="tc-btn-invisible">
<$action-setfield $tiddler="$:/temp/advancedsearch" $field="text" $value=""/>
{{$:/core/images/close-button}}
</$button>
<$macrocall $name="exportButton" exportFilter={{$:/temp/advancedsearch}} lingoBase="$:/language/Buttons/ExportTiddlers/"/>
</$reveal>
</div>

<div class="tc-block-dropdown-wrapper">
<$reveal state=<<qualify "$:/state/filterDropdown">> type="nomatch" text="" default="">
<div class="tc-block-dropdown tc-edit-type-dropdown">
<$list filter="[all[shadows+tiddlers]tag[$:/tags/Filter]]"><$link to={{!!filter}}><$transclude field="description"/></$link>
</$list>
</div>
</$reveal>
<$list filter="[all[shadows+tiddlers]tag[$:/tags/AdvancedSearch/FilterButton]!has[draft.of]]"><$transclude/></$list>
</div>

</$linkcatcher>

<$reveal state="$:/temp/advancedsearch" type="nomatch" text="">
<$set name="resultCount" value="""<$count filter={{$:/temp/advancedsearch}}/>""">
<div class="tc-search-results">
Expand Down
9 changes: 9 additions & 0 deletions core/ui/AdvancedSearch/FilterButtons/clear.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: $:/core/ui/AdvancedSearch/Filter/FilterButtons/clear
tags: $:/tags/AdvancedSearch/FilterButton

<$reveal state="$:/temp/advancedsearch" type="nomatch" text="">
<$button class="tc-btn-invisible">
<$action-setfield $tiddler="$:/temp/advancedsearch" $field="text" $value=""/>
{{$:/core/images/close-button}}
</$button>
</$reveal>
26 changes: 26 additions & 0 deletions core/ui/AdvancedSearch/FilterButtons/delete.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
title: $:/core/ui/AdvancedSearch/Filter/FilterButtons/delete
tags: $:/tags/AdvancedSearch/FilterButton

<$reveal state="$:/temp/advancedsearch" type="nomatch" text="">
<$button popup=<<qualify "$:/state/filterDeleteDropdown">> class="tc-btn-invisible">
{{$:/core/images/delete-button}}
</$button>
</$reveal>

<$reveal state=<<qualify "$:/state/filterDeleteDropdown">> type="popup" position="belowleft" animate="yes">
<div class="tc-block-dropdown-wrapper">
<div class="tc-block-dropdown tc-edit-type-dropdown">
<div class="tc-dropdown-item-plain">
<$set name="resultCount" value="""<$count filter={{$:/temp/advancedsearch}}/>""">
Are you sure you wish to delete <<resultCount>> tiddler(s)?
</$set>
</div>
<div class="tc-dropdown-item-plain">
<$button class="tc-btn">
<$action-deletetiddler $filter={{$:/temp/advancedsearch}}/>
Delete these tiddlers
</$button>
</div>
</div>
</div>
</$reveal>
19 changes: 19 additions & 0 deletions core/ui/AdvancedSearch/FilterButtons/dropdown.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
title: $:/core/ui/AdvancedSearch/Filter/FilterButtons/dropdown
tags: $:/tags/AdvancedSearch/FilterButton

<span class="tc-popup-keep">
<$button popup=<<qualify "$:/state/filterDropdown">> class="tc-btn-invisible">
{{$:/core/images/down-arrow}}
</$button>
</span>

<$reveal state=<<qualify "$:/state/filterDropdown">> type="popup" position="belowleft" animate="yes">
<$linkcatcher to="$:/temp/advancedsearch">
<div class="tc-block-dropdown-wrapper">
<div class="tc-block-dropdown tc-edit-type-dropdown">
<$list filter="[all[shadows+tiddlers]tag[$:/tags/Filter]]"><$link to={{!!filter}}><$transclude field="description"/></$link>
</$list>
</div>
</div>
</$linkcatcher>
</$reveal>
6 changes: 6 additions & 0 deletions core/ui/AdvancedSearch/FilterButtons/export.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
title: $:/core/ui/AdvancedSearch/Filter/FilterButtons/export
tags: $:/tags/AdvancedSearch/FilterButton

<$reveal state="$:/temp/advancedsearch" type="nomatch" text="">
<$macrocall $name="exportButton" exportFilter={{$:/temp/advancedsearch}} lingoBase="$:/language/Buttons/ExportTiddlers/"/>
</$reveal>
2 changes: 1 addition & 1 deletion core/ui/AlertTemplate.tid
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: $:/core/ui/AlertTemplate
<$button class="tc-btn-invisible"><$action-deletetiddler $tiddler=<<currentTiddler>>/>{{$:/core/images/delete-button}}</$button>
</div>
<div class="tc-alert-subtitle">
<$view field="component"/> - <$view field="modified" format="date" template="0hh:0mm:0ss DD MM YYYY"/> <$reveal type="nomatch" state="!!count" text=""><span class="tc-alert-highlight">(count: <$view field="count"/>)</span></$reveal>
<$view field="component"/> - <$view field="modified" format="date" template="0hh:0mm:0ss DD MM YYYY"/> <$reveal type="nomatch" state="!!count" text=""><span class="tc-alert-highlight">({{$:/language/Count}}: <$view field="count"/>)</span></$reveal>
</div>
<div class="tc-alert-body">

Expand Down
2 changes: 1 addition & 1 deletion core/ui/ControlPanel/Basics.tid
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ caption: {{$:/language/ControlPanel/Basics/Caption}}
|<$link to="$:/SiteSubtitle"><<lingo Subtitle/Prompt>></$link> |<$edit-text tiddler="$:/SiteSubtitle" default="" tag="input"/> |
|<$link to="$:/status/UserName"><<lingo Username/Prompt>></$link> |<$edit-text tiddler="$:/status/UserName" default="" tag="input"/> |
|<$link to="$:/config/AnimationDuration"><<lingo AnimDuration/Prompt>></$link> |<$edit-text tiddler="$:/config/AnimationDuration" default="" tag="input"/> |
|<$link to="$:/DefaultTiddlers"><<lingo DefaultTiddlers/Prompt>></$link> |<<lingo DefaultTiddlers/TopHint>><br> <$edit-text tag="textarea" tiddler="$:/DefaultTiddlers"/><br>//<<lingo DefaultTiddlers/BottomHint>>// |
|<$link to="$:/DefaultTiddlers"><<lingo DefaultTiddlers/Prompt>></$link> |<<lingo DefaultTiddlers/TopHint>><br> <$edit tag="textarea" tiddler="$:/DefaultTiddlers" class="tc-edit-texteditor"/><br>//<<lingo DefaultTiddlers/BottomHint>>// |
|<$link to="$:/config/NewJournal/Title"><<lingo NewJournal/Title/Prompt>></$link> |<$edit-text tiddler="$:/config/NewJournal/Title" default="" tag="input"/> |
|<$link to="$:/config/NewJournal/Tags"><<lingo NewJournal/Tags/Prompt>></$link> |<$edit-text tiddler="$:/config/NewJournal/Tags" default="" tag="input"/> |
|<<lingo Language/Prompt>> |{{$:/snippets/minilanguageswitcher}} |
Expand Down
140 changes: 140 additions & 0 deletions core/ui/ControlPanel/KeyboardShortcuts.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
title: $:/core/ui/ControlPanel/KeyboardShortcuts
tags: $:/tags/ControlPanel
caption: {{$:/language/ControlPanel/KeyboardShortcuts/Caption}}

\define lingo-base() $:/language/ControlPanel/KeyboardShortcuts/

\define new-shortcut(title)
<div class="tc-dropdown-item-plain">
<$edit-shortcut tiddler="$title$" placeholder={{$:/language/ControlPanel/KeyboardShortcuts/Add/Prompt}} style="width:auto;"/> <$button>
<<lingo Add/Caption>>
<$action-listops
$tiddler="$(shortcutTitle)$"
$field="text"
$subfilter="[{$title$}]"
/>
<$action-deletetiddler
$tiddler="$title$"
/>
</$button>
</div>
\end

\define shortcut-list-item(caption)
<td>
</td>
<td style="text-align:right;font-size:0.7em;">
<<lingo Platform/$caption$>>
</td>
<td>
<div style="position:relative;">
<$button popup=<<qualify "$:/state/dropdown/$(shortcutTitle)$">> class="tc-btn-invisible">
{{$:/core/images/edit-button}}
</$button>
<$macrocall $name="displayshortcuts" $output="text/html" shortcuts={{$(shortcutTitle)$}} prefix="<kbd>" separator="</kbd> <kbd>" suffix="</kbd>"/>

<$reveal state=<<qualify "$:/state/dropdown/$(shortcutTitle)$">> type="popup" position="below" animate="yes">
<div class="tc-block-dropdown-wrapper">
<div class="tc-block-dropdown tc-edit-type-dropdown tc-popup-keep">
<$list filter="[list[$(shortcutTitle)$!!text]sort[title]]" variable="shortcut" emptyMessage="""
<div class="tc-dropdown-item-plain">
//<<lingo NoShortcuts/Caption>>//
</div>
""">
<div class="tc-dropdown-item-plain">
<$button class="tc-btn-invisible" tooltip=<<lingo Remove/Hint>>>
<$action-listops
$tiddler="$(shortcutTitle)$"
$field="text"
$subfilter="+[remove<shortcut>]"
/>
&times;
</$button>
<kbd>
<$macrocall $name="displayshortcuts" $output="text/html" shortcuts=<<shortcut>>/>
</kbd>
</div>
</$list>
<hr/>
<$macrocall $name="new-shortcut" title=<<qualify "$:/state/new-shortcut/$(shortcutTitle)$">>/>
</div>
</div>
</$reveal>
</div>
</td>
\end

\define shortcut-list(caption,prefix)
<tr>
<$list filter="[all[tiddlers+shadows][$prefix$$(shortcutName)$]]" variable="shortcutTitle">
<<shortcut-list-item "$caption$">>
</$list>
</tr>
\end

\define shortcut-editor()
<<shortcut-list "All" "$:/config/shortcuts/">>
<<shortcut-list "Mac" "$:/config/shortcuts-mac/">>
<<shortcut-list "NonMac" "$:/config/shortcuts-not-mac/">>
<<shortcut-list "Linux" "$:/config/shortcuts-linux/">>
<<shortcut-list "NonLinux" "$:/config/shortcuts-not-linux/">>
<<shortcut-list "Windows" "$:/config/shortcuts-windows/">>
<<shortcut-list "NonWindows" "$:/config/shortcuts-not-windows/">>
\end

\define shortcut-preview()
<$macrocall $name="displayshortcuts" $output="text/html" shortcuts={{$(shortcutPrefix)$$(shortcutName)$}} prefix="<kbd>" separator="</kbd> <kbd>" suffix="</kbd>"/>
\end

\define shortcut-item-inner()
<tr>
<td>
<$reveal type="nomatch" state=<<dropdownStateTitle>> text="open">
<$button class="tc-btn-invisible">
<$action-setfield
$tiddler=<<dropdownStateTitle>>
$value="open"
/>
{{$:/core/images/right-arrow}}
</$button>
</$reveal>
<$reveal type="match" state=<<dropdownStateTitle>> text="open">
<$button class="tc-btn-invisible">
<$action-setfield
$tiddler=<<dropdownStateTitle>>
$value="close"
/>
{{$:/core/images/down-arrow}}
</$button>
</$reveal>
''<$text text=<<shortcutName>>/>''
</td>
<td>
<$transclude tiddler="$:/config/ShortcutInfo/$(shortcutName)$"/>
</td>
<td>
<$list filter="$:/config/shortcuts/ $:/config/shortcuts-mac/ $:/config/shortcuts-not-mac/ $:/config/shortcuts-linux/ $:/config/shortcuts-not-linux/ $:/config/shortcuts-windows/ $:/config/shortcuts-not-windows/" variable="shortcutPrefix">
<<shortcut-preview>>
</$list>
</td>
</tr>
<$set name="dropdownState" value={{$(dropdownStateTitle)$}}>
<$list filter="[<dropdownState>prefix[open]]" variable="listItem">
<<shortcut-editor>>
</$list>
</$set>
\end

\define shortcut-item()
<$set name="dropdownStateTitle" value=<<qualify "$:/state/dropdown/keyboardshortcut/$(shortcutName)$">>>
<<shortcut-item-inner>>
</$set>
\end

<table>
<tbody>
<$list filter="[all[shadows+tiddlers]removeprefix[$:/config/ShortcutInfo/]]" variable="shortcutName">
<<shortcut-item>>
</$list>
</tbody>
</table>
10 changes: 4 additions & 6 deletions core/ui/ControlPanel/Modals/AddPlugins.tid
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
title: $:/core/ui/ControlPanel/Modals/AddPlugins
subtitle: {{$:/core/images/download-button}} {{$:/language/ControlPanel/Plugins/Add/Caption}}

\define lingo-base() $:/language/ControlPanel/Plugins/

\define install-plugin-button()
<$button>
<$action-sendmessage $message="tm-load-plugin-from-library" url={{!!url}} title={{$(assetInfo)$!!original-title}}/>
<$list filter="[<assetInfo>get[original-title]get[version]]" variable="installedVersion" emptyMessage="""{{$:/language/ControlPanel/Plugins/Install}}""">
{{$:/language/ControlPanel/Plugins/Reinstall}}
<$list filter="[<assetInfo>get[original-title]get[version]]" variable="installedVersion" emptyMessage="""{{$:/language/ControlPanel/Plugins/Install/Caption}}""">
{{$:/language/ControlPanel/Plugins/Reinstall/Caption}}
</$list>
</$button>
\end
Expand Down Expand Up @@ -48,9 +46,9 @@ $:/state/add-plugin-info/$(connectionTiddler)$/$(assetInfo)$
<$reveal type="match" text="yes" state=<<popup-state>>>
<div class="tc-plugin-info-dropdown">
<div class="tc-plugin-info-dropdown-message">
<$list filter="[<assetInfo>get[original-title]get[version]]" variable="installedVersion" emptyMessage="""This plugin is not currently installed""">
<$list filter="[<assetInfo>get[original-title]get[version]]" variable="installedVersion" emptyMessage="""{{$:/language/ControlPanel/Plugins/NotInstalled/Hint}}""">
<em>
This plugin is already installed at version <$text text=<<installedVersion>>/>
{{$:/language/ControlPanel/Plugins/AlreadyInstalled/Hint}}
</em>
</$list>
</div>
Expand Down
2 changes: 1 addition & 1 deletion core/ui/ControlPanel/Plugins.tid
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ $:/config/Plugins/Disabled/$(currentTiddler)$
<$macrocall $name="tabs" state=<<tabs-state-macro>> tabsList={{!!list}} default="readme" template="$:/core/ui/PluginInfo"/>
</$reveal>
<$reveal type="match" text="" state="!!list">
No information provided
<<lingo NoInformation/Hint>>
</$reveal>
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions core/ui/ControlPanel/Settings/EditorToolbar.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: $:/core/ui/ControlPanel/Settings/EditorToolbar
tags: $:/tags/ControlPanel/Settings
caption: {{$:/language/ControlPanel/Settings/EditorToolbar/Caption}}

\define lingo-base() $:/language/ControlPanel/Settings/EditorToolbar/
<<lingo Hint>>

<$checkbox tiddler="$:/config/TextEditor/EnableToolbar" field="text" checked="yes" unchecked="no" default="yes"> <$link to="$:/config/TextEditor/EnableToolbar"><<lingo Description>></$link> </$checkbox>

9 changes: 9 additions & 0 deletions core/ui/ControlPanel/Settings/MissingLinks.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: $:/core/ui/ControlPanel/Settings/MissingLinks
tags: $:/tags/ControlPanel/Settings
caption: {{$:/language/ControlPanel/Settings/MissingLinks/Caption}}

\define lingo-base() $:/language/ControlPanel/Settings/MissingLinks/
<<lingo Hint>>

<$checkbox tiddler="$:/config/MissingLinks" field="text" checked="yes" unchecked="no" default="yes"> <$link to="$:/config/MissingLinks"><<lingo Description>></$link> </$checkbox>

21 changes: 21 additions & 0 deletions core/ui/ControlPanel/Toolbars/EditorToolbar.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
title: $:/core/ui/ControlPanel/Toolbars/EditorToolbar
tags: $:/tags/ControlPanel/Toolbars
caption: {{$:/language/ControlPanel/Toolbars/EditorToolbar/Caption}}

\define lingo-base() $:/language/TiddlerInfo/

\define config-title()
$:/config/EditorToolbarButtons/Visibility/$(listItem)$
\end

\define toolbar-button()
<$checkbox tiddler=<<config-title>> field="text" checked="show" unchecked="hide" default="show"> <$transclude tiddler={{$(listItem)$!!icon}}/> <$transclude tiddler=<<listItem>> field="caption"/> -- <i class="tc-muted"><$transclude tiddler=<<listItem>> field="description"/></i></$checkbox>
\end

{{$:/language/ControlPanel/Toolbars/EditorToolbar/Hint}}

<$list filter="[all[shadows+tiddlers]tag[$:/tags/EditorToolbar]!has[draft.of]]" variable="listItem">

<<toolbar-button>>

</$list>
4 changes: 2 additions & 2 deletions core/ui/EditTemplate.tid
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ tc-tiddler-frame tc-tiddler-edit-frame $(missingTiddlerClass)$ $(shadowTiddlerCl
\end
<div class=<<frame-classes>>>
<$set name="storyTiddler" value=<<currentTiddler>>>
<$keyboard key={{$:/config/shortcuts/cancel-edit-tiddler}} message="tm-cancel-tiddler">
<$keyboard key={{$:/config/shortcuts/save-tiddler}} message="tm-save-tiddler">
<$keyboard key="((cancel-edit-tiddler))" message="tm-cancel-tiddler">
<$keyboard key="((save-tiddler))" message="tm-save-tiddler">
<$list filter="[all[shadows+tiddlers]tag[$:/tags/EditTemplate]!has[draft.of]]" variable="listItem">
<$transclude tiddler=<<listItem>>/>
</$list>
Expand Down
9 changes: 9 additions & 0 deletions core/ui/EditTemplate/Preview/output.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: $:/core/ui/EditTemplate/body/preview/output
tags: $:/tags/EditPreview
caption: {{$:/language/EditTemplate/Body/Preview/Type/Output}}

<$set name="tv-tiddler-preview" value="yes">

<$transclude />

</$set>
30 changes: 30 additions & 0 deletions core/ui/EditTemplate/body-editor.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
title: $:/core/ui/EditTemplate/body/editor

<$edit

field="text"
class="tc-edit-texteditor"
placeholder={{$:/language/EditTemplate/Body/Placeholder}}

><$set

name="targetTiddler"
value=<<currentTiddler>>

><$list

filter="[all[shadows+tiddlers]tag[$:/tags/EditorToolbar]!has[draft.of]]"

><$reveal

type="nomatch"
state=<<config-visibility-title>>
text="hide"
class="tc-text-editor-toolbar-item-wrapper"

><$transclude

tiddler="$:/core/ui/EditTemplate/body/toolbar/button"
mode="inline"

/></$reveal></$list></$set></$edit>
107 changes: 107 additions & 0 deletions core/ui/EditTemplate/body-toolbar-button.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
title: $:/core/ui/EditTemplate/body/toolbar/button

\define toolbar-button-icon()
<$list

filter="[all[current]!has[custom-icon]]"
variable="no-custom-icon"

><$transclude

tiddler={{!!icon}}

/></$list>
\end

\define toolbar-button-tooltip()
{{!!description}}<$macrocall $name="displayshortcuts" $output="text/plain" shortcuts={{!!shortcuts}} prefix="` - [" separator="] [" suffix="]`"/>
\end

\define toolbar-button()
<$list

filter={{!!condition}}
variable="list-condition"

><$wikify

name="tooltip-text"
text=<<toolbar-button-tooltip>>
mode="inline"
output="text"

><$list

filter="[all[current]!has[dropdown]]"
variable="no-dropdown"

><$button

class="tc-btn-invisible $(buttonClasses)$"
tooltip=<<tooltip-text>>

><span

data-tw-keyboard-shortcut={{!!shortcuts}}

/><<toolbar-button-icon>><$transclude

tiddler=<<currentTiddler>>
field="text"

/></$button></$list><$list

filter="[all[current]has[dropdown]]"
variable="dropdown"

><$set

name="dropdown-state"
value=<<qualify "$:/state/EditorToolbarDropdown">>

><$button

popup=<<dropdown-state>>
class="tc-popup-keep tc-btn-invisible $(buttonClasses)$"
selectedClass="tc-selected"
tooltip=<<tooltip-text>>

><span

data-tw-keyboard-shortcut={{!!shortcuts}}

/><<toolbar-button-icon>><$transclude

tiddler=<<currentTiddler>>
field="text"

/></$button><$reveal

state=<<dropdown-state>>
type="popup"
position="below"
animate="yes"
tag="span"

><div

class="tc-drop-down tc-popup-keep"

><$transclude

tiddler={{!!dropdown}}
mode="block"

/></div></$reveal></$set></$list></$wikify></$list>
\end

\define toolbar-button-outer()
<$set

name="buttonClasses"
value={{!!button-classes}}

><<toolbar-button>></$set>
\end

<<toolbar-button-outer>>
20 changes: 10 additions & 10 deletions core/ui/EditTemplate/body.tid
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ title: $:/core/ui/EditTemplate/body
tags: $:/tags/EditTemplate

\define lingo-base() $:/language/EditTemplate/Body/
\define config-visibility-title()
$:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$
\end
<$list filter="[is[current]has[_canonical_uri]]">

<div class="tc-message-box">
Expand All @@ -20,19 +23,17 @@ tags: $:/tags/EditTemplate

<$reveal state="$:/state/showeditpreview" type="match" text="yes">

<em class="tc-edit"><<lingo Hint>></em> <$button type="set" set="$:/state/showeditpreview" setTo="no"><<lingo Preview/Button/Hide>></$button>

<div class="tc-tiddler-preview">

<$transclude tiddler="$:/core/ui/EditTemplate/body/editor" mode="inline"/>

<div class="tc-tiddler-preview-preview">
<$set name="tv-tiddler-preview" value="yes">

<$transclude />
<$transclude tiddler={{$:/state/editpreviewtype}} mode="inline">

</$set>
</div>
<$transclude tiddler="$:/core/ui/EditTemplate/body/preview/output" mode="inline"/>

<div class="tc-tiddler-preview-edit">
<$edit field="text" class="tc-edit-texteditor" placeholder={{$:/language/EditTemplate/Body/Placeholder}}/>
</$transclude>

</div>

Expand All @@ -42,8 +43,7 @@ tags: $:/tags/EditTemplate

<$reveal state="$:/state/showeditpreview" type="nomatch" text="yes">

<em class="tc-edit"><<lingo Hint>></em> <$button type="set" set="$:/state/showeditpreview" setTo="yes"><<lingo Preview/Button/Show>></$button>
<$edit field="text" class="tc-edit-texteditor" placeholder={{$:/language/EditTemplate/Body/Placeholder}}/>
<$transclude tiddler="$:/core/ui/EditTemplate/body/editor" mode="inline"/>

</$reveal>

Expand Down
19 changes: 15 additions & 4 deletions core/ui/EditTemplate/tags.tid
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@ tags: $:/tags/EditTemplate
\define lingo-base() $:/language/EditTemplate/
\define tag-styles()
background-color:$(backgroundColor)$;
fill:$(foregroundColor)$;
color:$(foregroundColor)$;
\end
\define tag-body-inner(colour,fallbackTarget,colourA,colourB)
<$vars foregroundColor=<<contrastcolour target:"""$colour$""" fallbackTarget:"""$fallbackTarget$""" colourA:"""$colourA$""" colourB:"""$colourB$""">> backgroundColor="""$colour$""">
<span style=<<tag-styles>> class="tc-tag-label">
<$view field="title" format="text" />
<$button message="tm-remove-tag" param={{!!title}} class="tc-btn-invisible tc-remove-tag-button">&times;</$button>
</span>
</$vars>
\end
\define tag-body(colour,palette)
<$macrocall $name="tag-body-inner" colour="""$colour$""" fallbackTarget={{$palette$##tag-background}} colourA={{$palette$##foreground}} colourB={{$palette$##background}}/>
\end
<div class="tc-edit-tags">
<$fieldmangler>
<$list filter="[all[current]tags[]sort[title]]" storyview="pop"><$set name="backgroundColor" value={{!!color}}><span style=<<tag-styles>> class="tc-tag-label">
<$view field="title" format="text" />
<$button message="tm-remove-tag" param={{!!title}} class="tc-btn-invisible tc-remove-tag-button">&times;</$button></span>
</$set>
<$list filter="[all[current]tags[]sort[title]]" storyview="pop">
<$macrocall $name="tag-body" colour={{!!color}} palette={{$:/palette}}/>
</$list>

<div class="tc-edit-add-tag">
Expand Down
16 changes: 15 additions & 1 deletion core/ui/EditTemplate/title.tid
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
title: $:/core/ui/EditTemplate/title
tags: $:/tags/EditTemplate

<$edit-text field="draft.title" class="tc-titlebar tc-edit-texteditor" focus="true"/>
<$vars pattern="""[\|\[\]{}]""" bad-chars="""`| [ ] { }`""">

<$list filter="[is[current]regexp:draft.title<pattern>]" variable="listItem">

<div class="tc-message-box">

{{$:/language/EditTemplate/Title/BadCharacterWarning}}

</div>

</$list>

</$vars>

<$edit-text field="draft.title" class="tc-titlebar tc-edit-texteditor" focus="true"/>
3 changes: 1 addition & 2 deletions core/ui/EditToolbar/save.tid
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ tags: $:/tags/EditToolbar
caption: {{$:/core/images/done-button}} {{$:/language/Buttons/Save/Caption}}
description: {{$:/language/Buttons/Save/Hint}}

<$fieldmangler>
<$button tooltip={{$:/language/Buttons/Save/Hint}} aria-label={{$:/language/Buttons/Save/Caption}} class=<<tv-config-toolbar-class>>>
<$fieldmangler><$button tooltip={{$:/language/Buttons/Save/Hint}} aria-label={{$:/language/Buttons/Save/Caption}} class=<<tv-config-toolbar-class>>>
<$action-sendmessage $message="tm-add-tag" $param={{$:/temp/NewTagName}}/>
<$action-deletetiddler $tiddler="$:/temp/NewTagName"/>
<$action-sendmessage $message="tm-add-field" $name={{$:/temp/newfieldname}} $value={{$:/temp/newfieldvalue}}/>
Expand Down
14 changes: 14 additions & 0 deletions core/ui/EditorToolbar/bold.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
title: $:/core/ui/EditorToolbar/bold
tags: $:/tags/EditorToolbar
icon: $:/core/images/bold
caption: {{$:/language/Buttons/Bold/Caption}}
description: {{$:/language/Buttons/Bold/Hint}}
condition: [<targetTiddler>!has[type]] [<targetTiddler>type[text/vnd.tiddlywiki]]
shortcuts: ((bold))

<$action-sendmessage
$message="tm-edit-text-operation"
$param="wrap-selection"
prefix="''"
suffix="''"
/>
21 changes: 21 additions & 0 deletions core/ui/EditorToolbar/clear-dropdown.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
title: $:/core/ui/EditorToolbar/clear-dropdown

''{{$:/language/Buttons/Clear/Hint}}''

<div class="tc-colour-chooser">

<$macrocall $name="colour-picker" actions="""

<$action-sendmessage
$message="tm-edit-bitmap-operation"
$param="clear"
colour=<<colour-picker-value>>
/>

<$action-deletetiddler
$tiddler=<<dropdown-state>>
/>

"""/>

</div>
8 changes: 8 additions & 0 deletions core/ui/EditorToolbar/clear.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
title: $:/core/ui/EditorToolbar/clear
tags: $:/tags/EditorToolbar
icon: $:/core/images/erase
caption: {{$:/language/Buttons/Clear/Caption}}
description: {{$:/language/Buttons/Clear/Hint}}
condition: [<targetTiddler>is[image]]
dropdown: $:/core/ui/EditorToolbar/clear-dropdown

8 changes: 8 additions & 0 deletions core/ui/EditorToolbar/editor-height-dropdown.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
title: $:/core/ui/EditorToolbar/editor-height-dropdown

\define lingo-base() $:/language/Buttons/EditorHeight/
''<<lingo Hint>>''

<$radio tiddler="$:/config/TextEditor/EditorHeight/Mode" value="auto"> {{$:/core/images/auto-height}} <<lingo Caption/Auto>></$radio>

<$radio tiddler="$:/config/TextEditor/EditorHeight/Mode" value="fixed"> {{$:/core/images/fixed-height}} <<lingo Caption/Fixed>> <$edit-text tag="input" tiddler="$:/config/TextEditor/EditorHeight/Height" default="100px"/></$radio>
15 changes: 15 additions & 0 deletions core/ui/EditorToolbar/editor-height.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
title: $:/core/ui/EditorToolbar/editor-height
tags: $:/tags/EditorToolbar
icon: $:/core/images/fixed-height
custom-icon: yes
caption: {{$:/language/Buttons/EditorHeight/Caption}}
description: {{$:/language/Buttons/EditorHeight/Hint}}
condition: [<targetTiddler>!is[image]]
dropdown: $:/core/ui/EditorToolbar/editor-height-dropdown

<$reveal tag="span" state="$:/config/TextEditor/EditorHeight/Mode" type="match" text="fixed">
{{$:/core/images/fixed-height}}
</$reveal>
<$reveal tag="span" state="$:/config/TextEditor/EditorHeight/Mode" type="match" text="auto">
{{$:/core/images/auto-height}}
</$reveal>
Loading