1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-27 03:57:21 +00:00

Add automatic saving and warning on exit with unsaved changes

We re-use some of the existing syncer mechanism. It was already keeping
track of changes to tiddlers in the store when working with a tiddler
syncadaptor. Now it also tracks changes when there is no syncadaptor,
allowing us to provide a warning if there are unsaved changes.
This commit is contained in:
Jermolene 2014-02-06 21:36:30 +00:00
parent 8e8e31fb9f
commit a01bbd4b9c
19 changed files with 216 additions and 174 deletions

View File

@ -56,6 +56,15 @@ exports.startup = function() {
$tw.wiki.addTiddler({title: storyTitle, text: "", list: story},$tw.wiki.getModificationFields());
// Host-specific startup
if($tw.browser) {
// Set up our beforeunload handler
window.addEventListener("beforeunload",function(event) {
var confirmationMessage = null;
if($tw.syncer.isDirty()) {
confirmationMessage = "You have unsaved changes in TiddlyWiki";
event.returnValue = confirmationMessage; // Gecko
}
return confirmationMessage; // Webkit, Safari, Chrome etc.
});
// Install the popup manager
$tw.popup = new $tw.utils.Popup({
rootElement: document.body
@ -86,22 +95,21 @@ exports.startup = function() {
$tw.pageScroller.handleEvent(event);
});
// Install the save action handler
$tw.wiki.initSavers();
$tw.rootWidget.addEventListener("tw-save-wiki",function(event) {
$tw.wiki.saveWiki({
$tw.syncer.saveWiki({
template: event.param,
downloadType: "text/plain"
});
});
$tw.rootWidget.addEventListener("tw-auto-save-wiki",function(event) {
$tw.wiki.saveWiki({
$tw.syncer.saveWiki({
method: "autosave",
template: event.param,
downloadType: "text/plain"
});
});
$tw.rootWidget.addEventListener("tw-download-file",function(event) {
$tw.wiki.saveWiki({
$tw.syncer.saveWiki({
method: "download",
template: event.param,
downloadType: "text/plain"

View File

@ -3,7 +3,7 @@ title: $:/core/modules/syncer.js
type: application/javascript
module-type: global
The syncer transfers content to and from data sources using syncadaptor modules.
The syncer tracks changes to the store. If a syncadaptor is used then individual tiddlers are synchronised through it. If there is no syncadaptor then the entire wiki is saved via saver modules.
\*/
(function(){
@ -28,46 +28,16 @@ function Syncer(options) {
self.syncadaptor = new module.adaptorClass(self);
}
});
// Only do anything if we've got a syncadaptor
if(this.syncadaptor) {
this.init();
// Initialise our savers
if($tw.browser) {
this.initSavers();
}
}
/*
Error handling
*/
Syncer.prototype.showError = function(error) {
this.log("Error: " + error);
};
/*
Constants
*/
Syncer.prototype.titleIsLoggedIn = "$:/status/IsLoggedIn";
Syncer.prototype.titleUserName = "$:/status/UserName";
Syncer.prototype.taskTimerInterval = 1 * 1000; // Interval for sync timer
Syncer.prototype.throttleInterval = 1 * 1000; // Defer saving tiddlers if they've changed in the last 1s...
Syncer.prototype.fallbackInterval = 10 * 1000; // Unless the task is older than 10s
Syncer.prototype.pollTimerInterval = 60 * 1000; // Interval for polling for changes from the adaptor
/*
Initialise the syncer
*/
Syncer.prototype.init = function() {
var self = this;
// Hashmap by title of {revision:,changeCount:,adaptorInfo:}
this.tiddlerInfo = {};
// Compile the dirty tiddler filter
this.filterFn = this.wiki.compileFilter(this.wiki.getTiddlerText(this.titleSyncFilter));
// Record information for known tiddlers
this.wiki.forEachTiddler({includeSystem: true},function(title,tiddler) {
self.tiddlerInfo[title] = {
revision: tiddler.fields["revision"],
adaptorInfo: self.syncadaptor.getTiddlerInfo(tiddler),
changeCount: self.wiki.getChangeCount(title)
}
});
this.readTiddlerInfo();
// Tasks are {type: "load"/"save"/"delete", title:, queueTime:, lastModificationTime:}
this.taskQueue = {}; // Hashmap of tasks to be performed
this.taskQueue = {}; // Hashmap of tasks yet to be performed
this.taskInProgress = {}; // Hash of tasks in progress
this.taskTimerId = null; // Timer for task dispatch
this.pollTimerId = null; // Timer for polling server
@ -76,9 +46,11 @@ Syncer.prototype.init = function() {
self.syncToServer(changes);
});
// Listen out for lazyLoad events
this.wiki.addEventListener("lazyLoad",function(title) {
self.handleLazyLoadEvent(title);
});
if(this.syncadaptor) {
this.wiki.addEventListener("lazyLoad",function(title) {
self.handleLazyLoadEvent(title);
});
}
// Listen out for login/logout/refresh events in the browser
if($tw.browser) {
document.addEventListener("tw-login",function(event) {
@ -98,6 +70,120 @@ Syncer.prototype.init = function() {
self.syncFromServer();
}
});
}
/*
Error handling
*/
Syncer.prototype.showError = function(error) {
this.log("Error: " + error);
};
/*
Constants
*/
Syncer.prototype.titleIsLoggedIn = "$:/status/IsLoggedIn";
Syncer.prototype.titleUserName = "$:/status/UserName";
Syncer.prototype.titleSyncFilter = "$:/config/SyncFilter";
Syncer.prototype.titleAutoSave = "$:/config/AutoSave";
Syncer.prototype.taskTimerInterval = 1 * 1000; // Interval for sync timer
Syncer.prototype.throttleInterval = 1 * 1000; // Defer saving tiddlers if they've changed in the last 1s...
Syncer.prototype.fallbackInterval = 10 * 1000; // Unless the task is older than 10s
Syncer.prototype.pollTimerInterval = 60 * 1000; // Interval for polling for changes from the adaptor
/*
Read (or re-read) the latest tiddler info from the store
*/
Syncer.prototype.readTiddlerInfo = function() {
// Hashmap by title of {revision:,changeCount:,adaptorInfo:}
this.tiddlerInfo = {};
// Record information for known tiddlers
var self = this;
this.wiki.forEachTiddler({includeSystem: true},function(title,tiddler) {
self.tiddlerInfo[title] = {
revision: tiddler.fields["revision"],
adaptorInfo: self.syncadaptor && self.syncadaptor.getTiddlerInfo(tiddler),
changeCount: self.wiki.getChangeCount(title)
}
});
};
/*
Select the appropriate saver modules and set them up
*/
Syncer.prototype.initSavers = function(moduleType) {
moduleType = moduleType || "saver";
// Instantiate the available savers
this.savers = [];
var self = this;
$tw.modules.forEachModuleOfType(moduleType,function(title,module) {
if(module.canSave(self)) {
self.savers.push(module.create(self.wiki));
}
});
// Sort the savers into priority order
this.savers.sort(function(a,b) {
if(a.info.priority < b.info.priority) {
return -1;
} else {
if(a.info.priority > b.info.priority) {
return +1;
} else {
return 0;
}
}
});
};
/*
Save the wiki contents. Options are:
method: "save" or "download"
template: the tiddler containing the template to save
downloadType: the content type for the saved file
*/
Syncer.prototype.saveWiki = function(options) {
options = options || {};
var method = options.method || "save",
template = options.template || "$:/core/save/all",
downloadType = options.downloadType || "text/plain",
text = this.wiki.renderTiddler(downloadType,template),
callback = function(err) {
if(err) {
alert("Error while saving:\n\n" + err);
} else {
$tw.notifier.display("$:/messages/Saved");
if(options.callback) {
options.callback();
}
}
};
// Ignore autosave if we've got a syncadaptor or autosave is disabled
if(method === "autosave") {
if(this.syncadaptor || this.wiki.getTiddlerText(this.titleAutoSave,"yes") !== "yes") {
return false;
}
}
// Call the highest priority saver that supports this method
for(var t=this.savers.length-1; t>=0; t--) {
var saver = this.savers[t];
if(saver.info.capabilities.indexOf(method) !== -1 && saver.save(text,method,callback)) {
this.log("Saving wiki with method",method,"through saver",saver.info.name);
// Clear the task queue if we're saving (rather than downloading)
if(method !== "download") {
this.readTiddlerInfo();
this.taskQueue = {};
}
return true;
}
}
return false;
};
/*
Checks whether the wiki is dirty (ie the window shouldn't be closed)
*/
Syncer.prototype.isDirty = function() {
return (this.numTasksInQueue() > 0) || (this.numTasksInProgress() > 0);
};
/*
@ -118,7 +204,7 @@ Syncer.prototype.storeTiddler = function(tiddlerFields) {
Syncer.prototype.getStatus = function(callback) {
var self = this;
// Check if the adaptor supports getStatus()
if(this.syncadaptor.getStatus) {
if(this.syncadaptor && this.syncadaptor.getStatus) {
// Mark us as not logged in
this.wiki.addTiddler({title: this.titleIsLoggedIn,text: "no"});
// Get login status
@ -130,7 +216,7 @@ Syncer.prototype.getStatus = function(callback) {
// Set the various status tiddlers
self.wiki.addTiddler({title: self.titleIsLoggedIn,text: isLoggedIn ? "yes" : "no"});
if(isLoggedIn) {
self.wiki.addTiddler({title: self.titleUserName,text: username});
self.wiki.addTiddler({title: self.titleUserName,text: username || ""});
} else {
self.wiki.deleteTiddler(self.titleUserName);
}
@ -148,7 +234,7 @@ Syncer.prototype.getStatus = function(callback) {
Synchronise from the server by reading the skinny tiddler list and queuing up loads for any tiddlers that we don't already have up to date
*/
Syncer.prototype.syncFromServer = function() {
if(this.syncadaptor.getSkinnyTiddlers) {
if(this.syncadaptor && this.syncadaptor.getSkinnyTiddlers) {
this.log("Retrieving skinny tiddler list");
var self = this;
if(this.pollTimerId) {
@ -198,10 +284,11 @@ Synchronise a set of changes to the server
*/
Syncer.prototype.syncToServer = function(changes) {
var self = this,
now = new Date();
now = new Date(),
filteredChanges = this.filterFn.call(this.wiki,changes);
$tw.utils.each(changes,function(change,title,object) {
// Ignore the change if it is a shadow tiddler
if((change.deleted && $tw.utils.hop(self.tiddlerInfo,title)) || (!change.deleted && self.wiki.tiddlerExists(title))) {
if((change.deleted && $tw.utils.hop(self.tiddlerInfo,title)) || (!change.deleted && filteredChanges.indexOf(title) !== -1)) {
// Queue a task to sync this tiddler
self.enqueueSyncTask({
type: change.deleted ? "delete" : "save",
@ -215,6 +302,7 @@ Syncer.prototype.syncToServer = function(changes) {
Lazily load a skinny tiddler if we can
*/
Syncer.prototype.handleLazyLoadEvent = function(title) {
console.log("Lazy loading",title)
// Queue up a sync task to load this tiddler
this.enqueueSyncTask({
type: "load",
@ -314,7 +402,7 @@ Syncer.prototype.enqueueSyncTask = function(task) {
}
// Check if this tiddler is already in the queue
if($tw.utils.hop(this.taskQueue,task.title)) {
this.log("Re-queueing up sync task with type:",task.type,"title:",task.title);
// this.log("Re-queueing up sync task with type:",task.type,"title:",task.title);
var existingTask = this.taskQueue[task.title];
// If so, just update the last modification time
existingTask.lastModificationTime = task.lastModificationTime;
@ -323,12 +411,14 @@ Syncer.prototype.enqueueSyncTask = function(task) {
existingTask.type = task.type;
}
} else {
this.log("Queuing up sync task with type:",task.type,"title:",task.title);
// this.log("Queuing up sync task with type:",task.type,"title:",task.title);
// If it is not in the queue, insert it
this.taskQueue[task.title] = task;
}
// Process the queue
$tw.utils.nextTick(function() {self.processTaskQueue.call(self);});
if(this.syncadaptor) {
$tw.utils.nextTick(function() {self.processTaskQueue.call(self);});
}
};
/*

View File

@ -52,6 +52,7 @@ NavigatorWidget.prototype.execute = function() {
// Get our parameters
this.storyTitle = this.getAttribute("story");
this.historyTitle = this.getAttribute("history");
this.autosave = this.getAttribute("autosave","yes");
// Construct the child widgets
this.makeChildWidgets();
};
@ -61,7 +62,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
NavigatorWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.story || changedAttributes.history) {
if(changedAttributes.story || changedAttributes.history || changedAttributes.autosave) {
this.refreshSelf();
return true;
} else {
@ -290,7 +291,9 @@ NavigatorWidget.prototype.handleSaveTiddlerEvent = function(event) {
this.saveStoryList(storyList);
}
// Send a notification event
this.dispatchEvent({type: "tw-auto-save-wiki"});
if(this.autosave === "yes") {
this.dispatchEvent({type: "tw-auto-save-wiki"});
}
}
}
}

View File

@ -877,62 +877,6 @@ exports.renderTiddler = function(outputType,title,options) {
return outputType === "text/html" ? container.innerHTML : container.textContent;
};
/*
Select the appropriate saver modules and set them up
*/
exports.initSavers = function(moduleType) {
moduleType = moduleType || "saver";
// Instantiate the available savers
this.savers = [];
var self = this;
$tw.modules.forEachModuleOfType(moduleType,function(title,module) {
if(module.canSave(self)) {
self.savers.push(module.create(self));
}
});
// Sort the savers into priority order
this.savers.sort(function(a,b) {
if(a.info.priority < b.info.priority) {
return -1;
} else {
if(a.info.priority > b.info.priority) {
return +1;
} else {
return 0;
}
}
});
};
/*
Save the wiki contents. Options are:
method: "save" or "download"
template: the tiddler containing the template to save
downloadType: the content type for the saved file
*/
exports.saveWiki = function(options) {
options = options || {};
var method = options.method || "save",
template = options.template || "$:/core/save/all",
downloadType = options.downloadType || "text/plain",
text = this.renderTiddler(downloadType,template),
callback = function(err) {
if(err) {
alert("Error while saving:\n\n" + err);
} else {
$tw.notifier.display("$:/messages/Saved");
}
};
// Call the highest priority saver that supports this method
for(var t=this.savers.length-1; t>=0; t--) {
var saver = this.savers[t];
if(saver.info.capabilities.indexOf(method) !== -1 && saver.save(text,method,callback)) {
return true;
}
}
return false;
};
/*
Return an array of tiddler titles that match a search string
text: The text string to search for

View File

@ -12,6 +12,11 @@ http://$(userName)$.tiddlyspot.com/backup/
</$set>
</$reveal>
\end
! Saving
|[[Autosave|$:/config/AutoSave]] |{{$:/snippets/autosavestatus}} |
! TiddlySpot
|[[Wiki name|$:/UploadName]] |<$edit-text tiddler="$:/UploadName" default="" tag="input"/> |

View File

@ -0,0 +1,10 @@
title: $:/snippets/autosavestatus
<$reveal type="match" state="$:/config/AutoSave" text="yes">
Autosave is currently enabled
<$linkcatcher to="$:/config/AutoSave"><$link to="no">Disable</$link></$linkcatcher>
</$reveal>
<$reveal type="nomatch" state="$:/config/AutoSave" text="yes">
Autosave is currently disabled
<$linkcatcher to="$:/config/AutoSave"><$link to="yes">Enable</$link></$linkcatcher>
</$reveal>

View File

@ -0,0 +1,3 @@
title: $:/config/AutoSave
yes

View File

@ -0,0 +1,3 @@
title: $:/config/SyncFilter
[is[tiddler]] -[[$:/HistoryList]] -[[$:/StoryList]] -[[$:/isEncrypted]] -[prefix[$:/status]] -[prefix[$:/state]] -[prefix[$:/temp]]

View File

@ -0,0 +1,11 @@
created: 20140206211715540
modified: 20140206212334833
tags: features
title: AutoSave
type: text/vnd.tiddlywiki
If there is a SaverModule available that supports it, TiddlyWiki will automatically trigger a save of the current document on clicking {{$:/core/images/done-button}} ''tick'' after editing a tiddler.
You should see a yellow notification at the top right of the window to confirm that an automatic save has taken place.
Automatic saving can be enabled or disabled through the ''Saving'' tab of the [[control panel|$:/ControlPanel]]. Behind the scenes, it is controlled through the configuration tiddler [[$:/config/AutoSave]], which must have the value ''yes'' to enable automatic saving.

View File

@ -9,11 +9,6 @@
"tiddlywiki/vanilla",
"tiddlywiki/snowwhite"
],
"doNotSave": [
"$:/HistoryList",
"$:/status/IsLoggedIn",
"$:/status/UserName"
],
"includeWikis": [
"../tw5.com"
]

View File

@ -6,8 +6,6 @@
"tiddlywiki/vanilla",
"tiddlywiki/snowwhite"
],
"doNotSave": [
],
"includeWikis": [
]
}

View File

@ -6,12 +6,6 @@
"tiddlywiki/vanilla",
"tiddlywiki/snowwhite"
],
"doNotSave": [
"$:/StoryList",
"$:/HistoryList",
"$:/status/IsLoggedIn",
"$:/status/UserName"
],
"includeWikis": [
]
}

View File

@ -6,8 +6,6 @@
"tiddlywiki/vanilla",
"tiddlywiki/snowwhite"
],
"doNotSave": [
],
"includeWikis": [
]
}

View File

@ -6,8 +6,6 @@
"tiddlywiki/vanilla",
"tiddlywiki/snowwhite"
],
"doNotSave": [
],
"includeWikis": [
]
}

View File

@ -1,5 +1,5 @@
created: 20131129090249275
modified: 20140126130155435
modified: 20140206212022007
tags: introduction
title: GettingStarted
type: text/vnd.tiddlywiki

View File

@ -1,5 +1,5 @@
created: 20140127143652456
modified: 20140127151028534
modified: 20140206191028534
tags: releasenote
title: Release 5.0.8-beta
type: text/vnd.tiddlywiki
@ -18,7 +18,8 @@ These are changes that might affect users upgrading from previous betas.
!! Improvements
*
* Added [[automatic saving|AutoSave]] on editing a tiddler
* Added a warning when attempting to close the window with unsaved changes
!! Bug Fixes

View File

@ -1,5 +1,5 @@
created: 20130825214200000
modified: 20140128194546203
modified: 20140206174804112
tags: dev
title: TiddlyWikiFolders
type: text/vnd.tiddlywiki
@ -21,7 +21,6 @@ Only the ''tiddlywiki.info'' file is required, the ''tiddlers'' and ''plugins''
The `tiddlywiki.info` file in a wiki folder contains a JSON object comprising the following fields:
* ''plugins'' - an array of plugin names to be included in the wiki
* ''doNotSave'' - an array of tiddler titles that should not be saved by the FileSystemAdaptorPlugin
* ''includeWikis'' - an array of relative paths to external wiki folders to be included in the wiki
For example:
@ -32,11 +31,6 @@ For example:
"tiddlywiki/tiddlyweb",
"tiddlywiki/filesystem"
],
"doNotSave": [
"$:/HistoryList",
"$:/status/IsLoggedIn",
"$:/status/UserName"
],
"includeWikis": [
"../tw5.com"
]

View File

@ -1,5 +1,5 @@
created: 20130823203800000
modified: 20140124204012349
modified: 20140206174012349
tags: planning
title: RoadMap
type: text/vnd.tiddlywiki
@ -12,10 +12,6 @@ During the beta TiddlyWiki will be practical for cautious everyday use but as we
The following additional features are planned or under consideration for implementation during the beta:
* Features affecting user data integrity
** Warning when attempting to close window without saving
** Use browser local storage to preserve changes in case browser crashes before saving/sync
** Automatic saving for saver modules that can support it
* Features required for large scale adoption
** Multilanguage support
** Proper use of ARIA roles

View File

@ -140,10 +140,6 @@ FileSystemAdaptor.prototype.saveTiddler = function(tiddler,callback) {
if(err) {
return callback(err);
}
if($tw.boot.wikiInfo.doNotSave && $tw.boot.wikiInfo.doNotSave.indexOf(tiddler.fields.title) !== -1) {
// Don't save the tiddler if it is on the blacklist
return callback(null,{},0);
}
if(self.watchers[fileInfo.filepath]) {
self.watchers[fileInfo.filepath].close();
delete self.watchers[fileInfo.filepath];
@ -197,34 +193,29 @@ FileSystemAdaptor.prototype.deleteTiddler = function(title,callback) {
fileInfo = $tw.boot.files[title];
// Only delete the tiddler if we have writable information for the file
if(fileInfo) {
if($tw.boot.wikiInfo.doNotSave && $tw.boot.wikiInfo.doNotSave.indexOf(title) !== -1) {
// Don't delete the tiddler if it is on the blacklist
callback(null);
} else {
if(this.watchers[fileInfo.filepath]) {
this.watchers[fileInfo.filepath].close();
delete this.watchers[fileInfo.filepath];
}
delete this.pending[fileInfo.filepath];
// Delete the file
fs.unlink(fileInfo.filepath,function(err) {
if(err) {
return callback(err);
}
self.log("Deleted file",fileInfo.filepath);
// Delete the metafile if present
if(fileInfo.hasMetaFile) {
fs.unlink(fileInfo.filepath + ".meta",function(err) {
if(err) {
return callback(err);
}
callback(null);
});
} else {
callback(null);
}
});
if(this.watchers[fileInfo.filepath]) {
this.watchers[fileInfo.filepath].close();
delete this.watchers[fileInfo.filepath];
}
delete this.pending[fileInfo.filepath];
// Delete the file
fs.unlink(fileInfo.filepath,function(err) {
if(err) {
return callback(err);
}
self.log("Deleted file",fileInfo.filepath);
// Delete the metafile if present
if(fileInfo.hasMetaFile) {
fs.unlink(fileInfo.filepath + ".meta",function(err) {
if(err) {
return callback(err);
}
callback(null);
});
} else {
callback(null);
}
});
} else {
callback(null);
}