1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-10-03 01:10:45 +00:00
TiddlyWiki5/core/modules/widgets/navigator.js
jed e86d4d29d5 Fix th-renaming-tiddler (see #4023) (#4037)
* Remove the th-renaming-tiddler hook from navigatior.js

The hook is invoked in the renameTiddler function which would cause the same hook to be called twice, and the version in the navigator widget didn't have the correct inputs and return value according to the documentation.

* Make it so that the th-renaming-tiddler hook isn't called twice

by removing it from the navigator.js file
2019-07-24 18:12:09 +01:00

610 lines
20 KiB
JavaScript
Executable File

/*\
title: $:/core/modules/widgets/navigator.js
type: application/javascript
module-type: widget
Navigator widget
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var IMPORT_TITLE = "$:/Import";
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var NavigatorWidget = function(parseTreeNode,options) {
this.initialise(parseTreeNode,options);
this.addEventListeners([
{type: "tm-navigate", handler: "handleNavigateEvent"},
{type: "tm-edit-tiddler", handler: "handleEditTiddlerEvent"},
{type: "tm-delete-tiddler", handler: "handleDeleteTiddlerEvent"},
{type: "tm-save-tiddler", handler: "handleSaveTiddlerEvent"},
{type: "tm-cancel-tiddler", handler: "handleCancelTiddlerEvent"},
{type: "tm-close-tiddler", handler: "handleCloseTiddlerEvent"},
{type: "tm-close-all-tiddlers", handler: "handleCloseAllTiddlersEvent"},
{type: "tm-close-other-tiddlers", handler: "handleCloseOtherTiddlersEvent"},
{type: "tm-new-tiddler", handler: "handleNewTiddlerEvent"},
{type: "tm-import-tiddlers", handler: "handleImportTiddlersEvent"},
{type: "tm-perform-import", handler: "handlePerformImportEvent"},
{type: "tm-fold-tiddler", handler: "handleFoldTiddlerEvent"},
{type: "tm-fold-other-tiddlers", handler: "handleFoldOtherTiddlersEvent"},
{type: "tm-fold-all-tiddlers", handler: "handleFoldAllTiddlersEvent"},
{type: "tm-unfold-all-tiddlers", handler: "handleUnfoldAllTiddlersEvent"},
{type: "tm-rename-tiddler", handler: "handleRenameTiddlerEvent"}
]);
};
/*
Inherit from the base widget class
*/
NavigatorWidget.prototype = new Widget();
/*
Render this widget into the DOM
*/
NavigatorWidget.prototype.render = function(parent,nextSibling) {
this.parentDomNode = parent;
this.computeAttributes();
this.execute();
this.renderChildren(parent,nextSibling);
};
/*
Compute the internal state of the widget
*/
NavigatorWidget.prototype.execute = function() {
// Get our parameters
this.storyTitle = this.getAttribute("story");
this.historyTitle = this.getAttribute("history");
this.setVariable("tv-story-list",this.storyTitle);
this.setVariable("tv-history-list",this.historyTitle);
// Construct the child widgets
this.makeChildWidgets();
};
/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
NavigatorWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.story || changedAttributes.history) {
this.refreshSelf();
return true;
} else {
return this.refreshChildren(changedTiddlers);
}
};
NavigatorWidget.prototype.getStoryList = function() {
return this.storyTitle ? this.wiki.getTiddlerList(this.storyTitle) : null;
};
NavigatorWidget.prototype.saveStoryList = function(storyList) {
var storyTiddler = this.wiki.getTiddler(this.storyTitle);
this.wiki.addTiddler(new $tw.Tiddler(
{title: this.storyTitle},
storyTiddler,
{list: storyList}
));
};
NavigatorWidget.prototype.removeTitleFromStory = function(storyList,title) {
var p = storyList.indexOf(title);
while(p !== -1) {
storyList.splice(p,1);
p = storyList.indexOf(title);
}
};
NavigatorWidget.prototype.replaceFirstTitleInStory = function(storyList,oldTitle,newTitle) {
var pos = storyList.indexOf(oldTitle);
if(pos !== -1) {
storyList[pos] = newTitle;
do {
pos = storyList.indexOf(oldTitle,pos + 1);
if(pos !== -1) {
storyList.splice(pos,1);
}
} while(pos !== -1);
} else {
storyList.splice(0,0,newTitle);
}
};
NavigatorWidget.prototype.addToStory = function(title,fromTitle) {
this.wiki.addToStory(title,fromTitle,this.storyTitle,{openLinkFromInsideRiver: this.getAttribute("openLinkFromInsideRiver","top"),openLinkFromOutsideRiver: this.getAttribute("openLinkFromOutsideRiver","top")});
};
/*
Add a new record to the top of the history stack
title: a title string or an array of title strings
fromPageRect: page coordinates of the origin of the navigation
*/
NavigatorWidget.prototype.addToHistory = function(title,fromPageRect) {
this.wiki.addToHistory(title,fromPageRect,this.historyTitle);
};
/*
Handle a tm-navigate event
*/
NavigatorWidget.prototype.handleNavigateEvent = function(event) {
event = $tw.hooks.invokeHook("th-navigating",event);
if(event.navigateTo) {
this.addToStory(event.navigateTo,event.navigateFromTitle);
if(!event.navigateSuppressNavigation) {
this.addToHistory(event.navigateTo,event.navigateFromClientRect);
}
}
return false;
};
// Close a specified tiddler
NavigatorWidget.prototype.handleCloseTiddlerEvent = function(event) {
var title = event.param || event.tiddlerTitle,
storyList = this.getStoryList();
// Look for tiddlers with this title to close
this.removeTitleFromStory(storyList,title);
this.saveStoryList(storyList);
return false;
};
// Close all tiddlers
NavigatorWidget.prototype.handleCloseAllTiddlersEvent = function(event) {
this.saveStoryList([]);
return false;
};
// Close other tiddlers
NavigatorWidget.prototype.handleCloseOtherTiddlersEvent = function(event) {
var title = event.param || event.tiddlerTitle;
this.saveStoryList([title]);
return false;
};
// Place a tiddler in edit mode
NavigatorWidget.prototype.handleEditTiddlerEvent = function(event) {
var editTiddler = $tw.hooks.invokeHook("th-editing-tiddler",event);
if(!editTiddler) {
return false;
}
var self = this;
function isUnmodifiedShadow(title) {
return self.wiki.isShadowTiddler(title) && !self.wiki.tiddlerExists(title);
}
function confirmEditShadow(title) {
return confirm($tw.language.getString(
"ConfirmEditShadowTiddler",
{variables:
{title: title}
}
));
}
var title = event.param || event.tiddlerTitle;
if(isUnmodifiedShadow(title) && !confirmEditShadow(title)) {
return false;
}
// Replace the specified tiddler with a draft in edit mode
var draftTiddler = this.makeDraftTiddler(title);
// Update the story and history if required
if(!event.paramObject || event.paramObject.suppressNavigation !== "yes") {
var draftTitle = draftTiddler.fields.title,
storyList = this.getStoryList();
this.removeTitleFromStory(storyList,draftTitle);
this.replaceFirstTitleInStory(storyList,title,draftTitle);
this.addToHistory(draftTitle,event.navigateFromClientRect);
this.saveStoryList(storyList);
return false;
}
};
// Delete a tiddler
NavigatorWidget.prototype.handleDeleteTiddlerEvent = function(event) {
// Get the tiddler we're deleting
var title = event.param || event.tiddlerTitle,
tiddler = this.wiki.getTiddler(title),
storyList = this.getStoryList(),
originalTitle = tiddler ? tiddler.fields["draft.of"] : "",
originalTiddler = originalTitle ? this.wiki.getTiddler(originalTitle) : undefined,
confirmationTitle;
if(!tiddler) {
return false;
}
// Check if the tiddler we're deleting is in draft mode
if(originalTitle) {
// If so, we'll prompt for confirmation referencing the original tiddler
confirmationTitle = originalTitle;
} else {
// If not a draft, then prompt for confirmation referencing the specified tiddler
confirmationTitle = title;
}
// Seek confirmation
if((this.wiki.getTiddler(originalTitle) || (tiddler.fields.text || "") !== "") && !confirm($tw.language.getString(
"ConfirmDeleteTiddler",
{variables:
{title: confirmationTitle}
}
))) {
return false;
}
// Delete the original tiddler
if(originalTitle) {
if(originalTiddler) {
$tw.hooks.invokeHook("th-deleting-tiddler",originalTiddler);
}
this.wiki.deleteTiddler(originalTitle);
this.removeTitleFromStory(storyList,originalTitle);
}
// Invoke the hook function and delete this tiddler
$tw.hooks.invokeHook("th-deleting-tiddler",tiddler);
this.wiki.deleteTiddler(title);
// Remove the closed tiddler from the story
this.removeTitleFromStory(storyList,title);
this.saveStoryList(storyList);
// Trigger an autosave
$tw.rootWidget.dispatchEvent({type: "tm-auto-save-wiki"});
return false;
};
/*
Create/reuse the draft tiddler for a given title
*/
NavigatorWidget.prototype.makeDraftTiddler = function(targetTitle) {
// See if there is already a draft tiddler for this tiddler
var draftTitle = this.wiki.findDraft(targetTitle);
if(draftTitle) {
return this.wiki.getTiddler(draftTitle);
}
// Get the current value of the tiddler we're editing
var tiddler = this.wiki.getTiddler(targetTitle);
// Save the initial value of the draft tiddler
draftTitle = this.generateDraftTitle(targetTitle);
var draftTiddler = new $tw.Tiddler(
tiddler,
{
title: draftTitle,
"draft.title": targetTitle,
"draft.of": targetTitle
},
this.wiki.getModificationFields()
);
this.wiki.addTiddler(draftTiddler);
return draftTiddler;
};
/*
Generate a title for the draft of a given tiddler
*/
NavigatorWidget.prototype.generateDraftTitle = function(title) {
return this.wiki.generateDraftTitle(title);
};
// Take a tiddler out of edit mode, saving the changes
NavigatorWidget.prototype.handleSaveTiddlerEvent = function(event) {
var title = event.param || event.tiddlerTitle,
tiddler = this.wiki.getTiddler(title),
storyList = this.getStoryList();
// Replace the original tiddler with the draft
if(tiddler) {
var draftTitle = (tiddler.fields["draft.title"] || "").trim(),
draftOf = (tiddler.fields["draft.of"] || "").trim();
if(draftTitle) {
var isRename = draftOf !== draftTitle,
isConfirmed = true;
if(isRename && this.wiki.tiddlerExists(draftTitle)) {
isConfirmed = confirm($tw.language.getString(
"ConfirmOverwriteTiddler",
{variables:
{title: draftTitle}
}
));
}
if(isConfirmed) {
// Create the new tiddler and pass it through the th-saving-tiddler hook
var newTiddler = new $tw.Tiddler(this.wiki.getCreationFields(),tiddler,{
title: draftTitle,
"draft.title": undefined,
"draft.of": undefined
},this.wiki.getModificationFields());
newTiddler = $tw.hooks.invokeHook("th-saving-tiddler",newTiddler);
this.wiki.addTiddler(newTiddler);
// If enabled, relink references to renamed tiddler
var shouldRelink = this.getAttribute("relinkOnRename","no").toLowerCase().trim() === "yes";
if(isRename && shouldRelink && this.wiki.tiddlerExists(draftOf)) {
console.log("Relinking '" + draftOf + "' to '" + draftTitle + "'");
this.wiki.relinkTiddler(draftOf,draftTitle);
}
// Remove the draft tiddler
this.wiki.deleteTiddler(title);
// Remove the original tiddler if we're renaming it
if(isRename) {
this.wiki.deleteTiddler(draftOf);
}
// #2381 always remove new title & old
this.removeTitleFromStory(storyList,draftTitle);
this.removeTitleFromStory(storyList,draftOf);
if(!event.paramObject || event.paramObject.suppressNavigation !== "yes") {
// Replace the draft in the story with the original
this.replaceFirstTitleInStory(storyList,title,draftTitle);
this.addToHistory(draftTitle,event.navigateFromClientRect);
if(draftTitle !== this.storyTitle) {
this.saveStoryList(storyList);
}
}
// Trigger an autosave
$tw.rootWidget.dispatchEvent({type: "tm-auto-save-wiki"});
}
}
}
return false;
};
// Take a tiddler out of edit mode without saving the changes
NavigatorWidget.prototype.handleCancelTiddlerEvent = function(event) {
event = $tw.hooks.invokeHook("th-cancelling-tiddler", event);
// Flip the specified tiddler from draft back to the original
var draftTitle = event.param || event.tiddlerTitle,
draftTiddler = this.wiki.getTiddler(draftTitle),
originalTitle = draftTiddler && draftTiddler.fields["draft.of"];
if(draftTiddler && originalTitle) {
// Ask for confirmation if the tiddler text has changed
var isConfirmed = true,
originalTiddler = this.wiki.getTiddler(originalTitle),
storyList = this.getStoryList();
if(this.wiki.isDraftModified(draftTitle)) {
isConfirmed = confirm($tw.language.getString(
"ConfirmCancelTiddler",
{variables:
{title: draftTitle}
}
));
}
// Remove the draft tiddler
if(isConfirmed) {
this.wiki.deleteTiddler(draftTitle);
if(!event.paramObject || event.paramObject.suppressNavigation !== "yes") {
if(originalTiddler) {
this.replaceFirstTitleInStory(storyList,draftTitle,originalTitle);
this.addToHistory(originalTitle,event.navigateFromClientRect);
} else {
this.removeTitleFromStory(storyList,draftTitle);
}
this.saveStoryList(storyList);
}
}
}
return false;
};
// Create a new draft tiddler
// event.param can either be the title of a template tiddler, or a hashmap of fields.
//
// The title of the newly created tiddler follows these rules:
// * If a hashmap was used and a title field was specified, use that title
// * If a hashmap was used without a title field, use a default title, if necessary making it unique with a numeric suffix
// * If a template tiddler was used, use the title of the template, if necessary making it unique with a numeric suffix
//
// If a draft of the target tiddler already exists then it is reused
NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
event = $tw.hooks.invokeHook("th-new-tiddler", event);
// Get the story details
var storyList = this.getStoryList(),
templateTiddler, additionalFields, title, draftTitle, existingTiddler;
// Get the template tiddler (if any)
if(typeof event.param === "string") {
// Get the template tiddler
templateTiddler = this.wiki.getTiddler(event.param);
// Generate a new title
title = this.wiki.generateNewTitle(event.param || $tw.language.getString("DefaultNewTiddlerTitle"));
}
// Get the specified additional fields
if(typeof event.paramObject === "object") {
additionalFields = event.paramObject;
}
if(typeof event.param === "object") { // Backwards compatibility with 5.1.3
additionalFields = event.param;
}
if(additionalFields && additionalFields.title) {
title = additionalFields.title;
}
// Make a copy of the additional fields excluding any blank ones
var filteredAdditionalFields = $tw.utils.extend({},additionalFields);
Object.keys(filteredAdditionalFields).forEach(function(fieldName) {
if(filteredAdditionalFields[fieldName] === "") {
delete filteredAdditionalFields[fieldName];
}
});
// Generate a title if we don't have one
title = title || this.wiki.generateNewTitle($tw.language.getString("DefaultNewTiddlerTitle"));
// Find any existing draft for this tiddler
draftTitle = this.wiki.findDraft(title);
// Pull in any existing tiddler
if(draftTitle) {
existingTiddler = this.wiki.getTiddler(draftTitle);
} else {
draftTitle = this.generateDraftTitle(title);
existingTiddler = this.wiki.getTiddler(title);
}
// Merge the tags
var mergedTags = [];
if(existingTiddler && existingTiddler.fields.tags) {
$tw.utils.pushTop(mergedTags,existingTiddler.fields.tags);
}
if(additionalFields && additionalFields.tags) {
// Merge tags
mergedTags = $tw.utils.pushTop(mergedTags,$tw.utils.parseStringArray(additionalFields.tags));
}
if(templateTiddler && templateTiddler.fields.tags) {
// Merge tags
mergedTags = $tw.utils.pushTop(mergedTags,templateTiddler.fields.tags);
}
// Save the draft tiddler
var draftTiddler = new $tw.Tiddler({
text: "",
"draft.title": title
},
templateTiddler,
additionalFields,
this.wiki.getCreationFields(),
existingTiddler,
filteredAdditionalFields,
{
title: draftTitle,
"draft.of": title,
tags: mergedTags
},this.wiki.getModificationFields());
this.wiki.addTiddler(draftTiddler);
// Update the story to insert the new draft at the top and remove any existing tiddler
if(storyList.indexOf(draftTitle) === -1) {
var slot = storyList.indexOf(event.navigateFromTitle);
if(slot === -1) {
slot = this.getAttribute("openLinkFromOutsideRiver","top") === "bottom" ? storyList.length - 1 : slot;
}
storyList.splice(slot + 1,0,draftTitle);
}
if(storyList.indexOf(title) !== -1) {
storyList.splice(storyList.indexOf(title),1);
}
this.saveStoryList(storyList);
// Add a new record to the top of the history stack
this.addToHistory(draftTitle);
return false;
};
// Import JSON tiddlers into a pending import tiddler
NavigatorWidget.prototype.handleImportTiddlersEvent = function(event) {
// Get the tiddlers
var tiddlers = [];
try {
tiddlers = JSON.parse(event.param);
} catch(e) {
}
// Get the current $:/Import tiddler
var importTiddler = this.wiki.getTiddler(IMPORT_TITLE),
importData = this.wiki.getTiddlerData(IMPORT_TITLE,{}),
newFields = new Object({
title: IMPORT_TITLE,
type: "application/json",
"plugin-type": "import",
"status": "pending"
}),
incomingTiddlers = [];
// Process each tiddler
importData.tiddlers = importData.tiddlers || {};
$tw.utils.each(tiddlers,function(tiddlerFields) {
tiddlerFields.title = $tw.utils.trim(tiddlerFields.title);
var title = tiddlerFields.title;
if(title) {
incomingTiddlers.push(title);
importData.tiddlers[title] = tiddlerFields;
}
});
// Give the active upgrader modules a chance to process the incoming tiddlers
var messages = this.wiki.invokeUpgraders(incomingTiddlers,importData.tiddlers);
$tw.utils.each(messages,function(message,title) {
newFields["message-" + title] = message;
});
// Deselect any suppressed tiddlers
$tw.utils.each(importData.tiddlers,function(tiddler,title) {
if($tw.utils.count(tiddler) === 0) {
newFields["selection-" + title] = "unchecked";
}
});
// Save the $:/Import tiddler
newFields.text = JSON.stringify(importData,null,$tw.config.preferences.jsonSpaces);
this.wiki.addTiddler(new $tw.Tiddler(importTiddler,newFields));
// Update the story and history details
if(this.getVariable("tv-auto-open-on-import") !== "no") {
var storyList = this.getStoryList(),
history = [];
// Add it to the story
if(storyList.indexOf(IMPORT_TITLE) === -1) {
storyList.unshift(IMPORT_TITLE);
}
// And to history
history.push(IMPORT_TITLE);
// Save the updated story and history
this.saveStoryList(storyList);
this.addToHistory(history);
}
return false;
};
//
NavigatorWidget.prototype.handlePerformImportEvent = function(event) {
var self = this,
importTiddler = this.wiki.getTiddler(event.param),
importData = this.wiki.getTiddlerDataCached(event.param,{tiddlers: {}}),
importReport = [];
// Add the tiddlers to the store
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") {
var tiddler = new $tw.Tiddler(tiddlerFields);
tiddler = $tw.hooks.invokeHook("th-importing-tiddler",tiddler);
self.wiki.addTiddler(tiddler);
importReport.push("# [[" + tiddlerFields.title + "]]");
}
});
// Replace the $:/Import tiddler with an import report
this.wiki.addTiddler(new $tw.Tiddler({
title: event.param,
text: importReport.join("\n"),
"status": "complete"
}));
// Navigate to the $:/Import tiddler
this.addToHistory([event.param]);
// Trigger an autosave
$tw.rootWidget.dispatchEvent({type: "tm-auto-save-wiki"});
};
NavigatorWidget.prototype.handleFoldTiddlerEvent = function(event) {
var paramObject = event.paramObject || {};
if(paramObject.foldedState) {
var foldedState = this.wiki.getTiddlerText(paramObject.foldedState,"show") === "show" ? "hide" : "show";
this.wiki.setText(paramObject.foldedState,"text",null,foldedState);
}
};
NavigatorWidget.prototype.handleFoldOtherTiddlersEvent = function(event) {
var self = this,
paramObject = event.paramObject || {},
prefix = paramObject.foldedStatePrefix;
$tw.utils.each(this.getStoryList(),function(title) {
self.wiki.setText(prefix + title,"text",null,event.param === title ? "show" : "hide");
});
};
NavigatorWidget.prototype.handleFoldAllTiddlersEvent = function(event) {
var self = this,
paramObject = event.paramObject || {},
prefix = paramObject.foldedStatePrefix || "$:/state/folded/";
$tw.utils.each(this.getStoryList(),function(title) {
self.wiki.setText(prefix + title,"text",null,"hide");
});
};
NavigatorWidget.prototype.handleUnfoldAllTiddlersEvent = function(event) {
var self = this,
paramObject = event.paramObject || {},
prefix = paramObject.foldedStatePrefix;
$tw.utils.each(this.getStoryList(),function(title) {
self.wiki.setText(prefix + title,"text",null,"show");
});
};
NavigatorWidget.prototype.handleRenameTiddlerEvent = function(event) {
var paramObject = event.paramObject || {},
from = paramObject.from || event.tiddlerTitle,
to = paramObject.to;
$tw.wiki.renameTiddler(from,to);
};
exports.navigator = NavigatorWidget;
})();