mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-12-30 12:00:28 +00:00
13c4e028b1
Previously we were using a message `tw-auto-save-wiki` to trigger an autosave. The message was generated by certain UI actions such as saving a tiddler. The trouble was that the message was being processed before the wiki change event for the accompanying change had had a chance to percolate. The end result was that the dirty indicator was staying lit when using autosave. The new approach abandons the autosave message and instead triggers the autosave in the wiki change event when a relevant change occurs. One happy side effect of these changes is that the dirty indicator now works as expected with the client server edition - ie, when typing in a draft tiddler the dirty indicator will flash briefly, and then clear when the sync mechanism has completed saving the draft.
486 lines
15 KiB
JavaScript
Executable File
486 lines
15 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: "tw-navigate", handler: "handleNavigateEvent"},
|
|
{type: "tw-edit-tiddler", handler: "handleEditTiddlerEvent"},
|
|
{type: "tw-delete-tiddler", handler: "handleDeleteTiddlerEvent"},
|
|
{type: "tw-save-tiddler", handler: "handleSaveTiddlerEvent"},
|
|
{type: "tw-cancel-tiddler", handler: "handleCancelTiddlerEvent"},
|
|
{type: "tw-close-tiddler", handler: "handleCloseTiddlerEvent"},
|
|
{type: "tw-close-all-tiddlers", handler: "handleCloseAllTiddlersEvent"},
|
|
{type: "tw-close-other-tiddlers", handler: "handleCloseOtherTiddlersEvent"},
|
|
{type: "tw-new-tiddler", handler: "handleNewTiddlerEvent"},
|
|
{type: "tw-import-tiddlers", handler: "handleImportTiddlersEvent"},
|
|
{type: "tw-perform-import", handler: "handlePerformImportEvent"}
|
|
]);
|
|
};
|
|
|
|
/*
|
|
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");
|
|
// 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.findTitleInStory = function(storyList,title,defaultIndex) {
|
|
var p = storyList.indexOf(title);
|
|
return p === -1 ? defaultIndex : p;
|
|
};
|
|
|
|
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) {
|
|
var storyList = this.getStoryList();
|
|
if(storyList) {
|
|
// See if the tiddler is already there
|
|
var slot = this.findTitleInStory(storyList,title,-1);
|
|
// If not we need to add it
|
|
if(slot === -1) {
|
|
// First we try to find the position of the story element we navigated from
|
|
slot = this.findTitleInStory(storyList,fromTitle,-1) + 1;
|
|
// Add the tiddler
|
|
storyList.splice(slot,0,title);
|
|
// Save the story
|
|
this.saveStoryList(storyList);
|
|
}
|
|
}
|
|
};
|
|
|
|
/*
|
|
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 tw-navigate event
|
|
*/
|
|
NavigatorWidget.prototype.handleNavigateEvent = function(event) {
|
|
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 self = this;
|
|
function isUnmodifiedShadow(title) {
|
|
// jshint eqnull:true
|
|
var tiddler = self.wiki.getTiddler(title);
|
|
return (
|
|
self.wiki.isShadowTiddler(title) &&
|
|
tiddler.fields.modified == null
|
|
);
|
|
}
|
|
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),
|
|
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, confirmationTitle;
|
|
// Check if the tiddler we're deleting is in draft mode
|
|
if(tiddler.hasField("draft.title")) {
|
|
// If so, we'll prompt for confirmation referencing the original tiddler
|
|
originalTitle = tiddler.fields["draft.of"];
|
|
confirmationTitle = originalTitle;
|
|
} else {
|
|
// If not a draft, then prompt for confirmation referencing the specified tiddler
|
|
originalTitle = null;
|
|
confirmationTitle = title;
|
|
}
|
|
// Seek confirmation
|
|
if(!confirm($tw.language.getString(
|
|
"ConfirmDeleteTiddler",
|
|
{variables:
|
|
{title: confirmationTitle}
|
|
}
|
|
))) {
|
|
return false;
|
|
}
|
|
// Delete the original tiddler
|
|
if(originalTitle) {
|
|
this.wiki.deleteTiddler(originalTitle);
|
|
this.removeTitleFromStory(storyList,originalTitle);
|
|
}
|
|
// Delete this tiddler
|
|
this.wiki.deleteTiddler(title);
|
|
// Remove the closed tiddler from the story
|
|
this.removeTitleFromStory(storyList,title);
|
|
this.saveStoryList(storyList);
|
|
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 drafts = [];
|
|
this.wiki.forEachTiddler({includeSystem: true},function(title,tiddler) {
|
|
if(tiddler.fields["draft.title"] && tiddler.fields["draft.of"] === targetTitle) {
|
|
drafts.push(tiddler);
|
|
}
|
|
});
|
|
if(drafts.length > 0) {
|
|
return drafts[0];
|
|
}
|
|
// Get the current value of the tiddler we're editing
|
|
var tiddler = this.wiki.getTiddler(targetTitle),
|
|
draftTitle = this.generateDraftTitle(targetTitle);
|
|
// Save the initial value of the draft tiddler
|
|
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) {
|
|
var c = 0;
|
|
do {
|
|
var draftTitle = "Draft " + (c ? (c + 1) + " " : "") + "of '" + title + "'";
|
|
c++;
|
|
} while(this.wiki.tiddlerExists(draftTitle));
|
|
return draftTitle;
|
|
};
|
|
|
|
// 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(!isRename && !this.wiki.isDraftModified(title)) {
|
|
event.type = "tw-cancel-tiddler";
|
|
this.dispatchEvent(event);
|
|
} else if(isConfirmed) {
|
|
// Save the draft tiddler as the real tiddler
|
|
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getCreationFields(),tiddler,{
|
|
title: draftTitle,
|
|
"draft.title": undefined,
|
|
"draft.of": undefined
|
|
},this.wiki.getModificationFields()));
|
|
// Remove the draft tiddler
|
|
this.wiki.deleteTiddler(title);
|
|
// Remove the original tiddler if we're renaming it
|
|
if(isRename) {
|
|
this.wiki.deleteTiddler(draftOf);
|
|
}
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Take a tiddler out of edit mode without saving the changes
|
|
NavigatorWidget.prototype.handleCancelTiddlerEvent = function(event) {
|
|
// Flip the specified tiddler from draft back to the original
|
|
var draftTitle = event.param || event.tiddlerTitle,
|
|
draftTiddler = this.wiki.getTiddler(draftTitle),
|
|
originalTitle = draftTiddler.fields["draft.of"],
|
|
storyList = this.getStoryList();
|
|
if(draftTiddler && originalTitle) {
|
|
// Ask for confirmation if the tiddler text has changed
|
|
var isConfirmed = true;
|
|
if(this.wiki.getTiddlerText(draftTitle) !== this.wiki.getTiddlerText(originalTitle)) {
|
|
isConfirmed = confirm($tw.language.getString(
|
|
"ConfirmCancelTiddler",
|
|
{variables:
|
|
{title: draftTitle}
|
|
}
|
|
));
|
|
}
|
|
// Remove the draft tiddler
|
|
if(isConfirmed) {
|
|
this.wiki.deleteTiddler(draftTitle);
|
|
this.replaceFirstTitleInStory(storyList,draftTitle,originalTitle);
|
|
this.addToHistory(originalTitle,event.navigateFromClientRect);
|
|
this.saveStoryList(storyList);
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Create a new draft tiddler
|
|
NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
|
|
// Get the story details
|
|
var storyList = this.getStoryList();
|
|
// Get the template tiddler if there is one
|
|
var templateTiddler = this.wiki.getTiddler(event.param);
|
|
// Title the new tiddler
|
|
var title = this.wiki.generateNewTitle((templateTiddler && templateTiddler.fields.title) || "New Tiddler");
|
|
// Create the draft tiddler
|
|
var draftTitle = this.generateDraftTitle(title),
|
|
draftTiddler = new $tw.Tiddler({
|
|
text: ""
|
|
},templateTiddler,
|
|
this.wiki.getCreationFields(),
|
|
{
|
|
title: draftTitle,
|
|
"draft.title": title,
|
|
"draft.of": title
|
|
},this.wiki.getModificationFields());
|
|
this.wiki.addTiddler(draftTiddler);
|
|
// Update the story to insert the new draft at the top
|
|
var slot = storyList.indexOf(event.navigateFromTitle);
|
|
storyList.splice(slot + 1,0,draftTitle);
|
|
// Save the updated story
|
|
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) {
|
|
var self = this;
|
|
// 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) {
|
|
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("tw-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.getTiddlerData(event.param,{tiddlers: {}}),
|
|
importReport = [];
|
|
// Add the tiddlers to the store
|
|
importReport.push("The following tiddlers were imported:\n");
|
|
$tw.utils.each(importData.tiddlers,function(tiddlerFields) {
|
|
var title = tiddlerFields.title;
|
|
if(title && importTiddler && importTiddler.fields["selection-" + title] !== "unchecked") {
|
|
self.wiki.addTiddler(new $tw.Tiddler(tiddlerFields));
|
|
importReport.push("# [[" + tiddlerFields.title + "]]");
|
|
}
|
|
});
|
|
// Replace the $:/Import tiddler with an import report
|
|
this.wiki.addTiddler(new $tw.Tiddler({
|
|
title: IMPORT_TITLE,
|
|
text: importReport.join("\n"),
|
|
"status": "complete"
|
|
}));
|
|
// Navigate to the $:/Import tiddler
|
|
this.addToHistory([IMPORT_TITLE]);
|
|
};
|
|
|
|
exports.navigator = NavigatorWidget;
|
|
|
|
})();
|