/*\ title: $:/core/modules/wiki.js type: application/javascript module-type: wikimethod Extension methods for the $tw.Wiki object Adds the following properties to the wiki object: * `eventListeners` is a hashmap by type of arrays of listener functions * `changedTiddlers` is a hashmap describing changes to named tiddlers since wiki change events were last dispatched. Each entry is a hashmap containing two fields: modified: true/false deleted: true/false * `changeCount` is a hashmap by tiddler title containing a numerical index that starts at zero and is incremented each time a tiddler is created changed or deleted * `caches` is a hashmap by tiddler title containing a further hashmap of named cache objects. Caches are automatically cleared when a tiddler is modified or deleted * `globalCache` is a hashmap by cache name of cache objects that are cleared whenever any tiddler change occurs \*/ (function(){ /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; var widget = require("$:/core/modules/widgets/widget.js"); var USER_NAME_TITLE = "$:/status/UserName", TIMESTAMP_DISABLE_TITLE = "$:/config/TimestampDisable"; /* Add available indexers to this wiki */ exports.addIndexersToWiki = function() { var self = this; $tw.utils.each($tw.modules.applyMethods("indexer"),function(Indexer,name) { self.addIndexer(new Indexer(self),name); }); }; /* Get the value of a text reference. Text references can have any of these forms: !! !! - specifies a field of the current tiddlers ## */ exports.getTextReference = function(textRef,defaultText,currTiddlerTitle) { var tr = $tw.utils.parseTextReference(textRef), title = tr.title || currTiddlerTitle; if(tr.field) { var tiddler = this.getTiddler(title); if(tr.field === "title") { // Special case so we can return the title of a non-existent tiddler return title || defaultText; } else if(tiddler && $tw.utils.hop(tiddler.fields,tr.field)) { return tiddler.getFieldString(tr.field); } else { return defaultText; } } else if(tr.index) { return this.extractTiddlerDataItem(title,tr.index,defaultText); } else { return this.getTiddlerText(title,defaultText); } }; exports.setTextReference = function(textRef,value,currTiddlerTitle) { var tr = $tw.utils.parseTextReference(textRef), title = tr.title || currTiddlerTitle; this.setText(title,tr.field,tr.index,value); }; exports.setText = function(title,field,index,value,options) { options = options || {}; var creationFields = options.suppressTimestamp ? {} : this.getCreationFields(), modificationFields = options.suppressTimestamp ? {} : this.getModificationFields(); // Check if it is a reference to a tiddler field if(index) { var data = this.getTiddlerData(title,Object.create(null)); if(value !== undefined) { data[index] = value; } else { delete data[index]; } this.setTiddlerData(title,data,{},{suppressTimestamp: options.suppressTimestamp}); } else { var tiddler = this.getTiddler(title), fields = {title: title}; fields[field || "text"] = value; this.addTiddler(new $tw.Tiddler(creationFields,tiddler,fields,modificationFields)); } }; exports.deleteTextReference = function(textRef,currTiddlerTitle) { var tr = $tw.utils.parseTextReference(textRef), title,tiddler,fields; // Check if it is a reference to a tiddler if(tr.title && !tr.field) { this.deleteTiddler(tr.title); // Else check for a field reference } else if(tr.field) { title = tr.title || currTiddlerTitle; tiddler = this.getTiddler(title); if(tiddler && $tw.utils.hop(tiddler.fields,tr.field)) { fields = Object.create(null); fields[tr.field] = undefined; this.addTiddler(new $tw.Tiddler(tiddler,fields,this.getModificationFields())); } } }; exports.addEventListener = function(type,listener) { this.eventListeners = this.eventListeners || {}; this.eventListeners[type] = this.eventListeners[type] || []; this.eventListeners[type].push(listener); }; exports.removeEventListener = function(type,listener) { var listeners = this.eventListeners[type]; if(listeners) { var p = listeners.indexOf(listener); if(p !== -1) { listeners.splice(p,1); } } }; exports.dispatchEvent = function(type /*, args */) { var args = Array.prototype.slice.call(arguments,1), listeners = this.eventListeners[type]; if(listeners) { for(var p=0; p 0) { self.dispatchEvent("change",changes); } }); this.eventsTriggered = true; } }; exports.getSizeOfTiddlerEventQueue = function() { return $tw.utils.count(this.changedTiddlers); }; exports.clearTiddlerEventQueue = function() { this.changedTiddlers = Object.create(null); this.changeCount = Object.create(null); }; exports.getChangeCount = function(title) { this.changeCount = this.changeCount || Object.create(null); if($tw.utils.hop(this.changeCount,title)) { return this.changeCount[title]; } else { return 0; } }; /* Generate an unused title from the specified base options.prefix must be a string */ exports.generateNewTitle = function(baseTitle,options) { options = options || {}; var c = 0, title = baseTitle, template = options.template, prefix = (typeof(options.prefix) === "string") ? options.prefix : " "; if (template) { // "count" is important to avoid an endless loop in while(...)!! template = (/\$count:?(\d+)?\$/i.test(template)) ? template : template + "$count$"; title = $tw.utils.formatTitleString(template,{"base":baseTitle,"separator":prefix,"counter":c}); while(this.tiddlerExists(title) || this.isShadowTiddler(title) || this.findDraft(title)) { title = $tw.utils.formatTitleString(template,{"base":baseTitle,"separator":prefix,"counter":(++c)}); } } else { while(this.tiddlerExists(title) || this.isShadowTiddler(title) || this.findDraft(title)) { title = baseTitle + prefix + (++c); } } return title; }; exports.isSystemTiddler = function(title) { return title && title.indexOf("$:/") === 0; }; exports.isTemporaryTiddler = function(title) { return title && title.indexOf("$:/temp/") === 0; }; exports.isVolatileTiddler = function(title) { return title && title.indexOf("$:/temp/volatile/") === 0; }; exports.isImageTiddler = function(title) { var tiddler = this.getTiddler(title); if(tiddler) { var contentTypeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/vnd.tiddlywiki"]; return !!contentTypeInfo && contentTypeInfo.flags.indexOf("image") !== -1; } else { return null; } }; exports.isBinaryTiddler = function(title) { var tiddler = this.getTiddler(title); if(tiddler) { var contentTypeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/vnd.tiddlywiki"]; return !!contentTypeInfo && contentTypeInfo.encoding === "base64"; } else { return null; } }; /* Like addTiddler() except it will silently reject any plugin tiddlers that are older than the currently loaded version. Returns true if the tiddler was imported */ exports.importTiddler = function(tiddler) { var existingTiddler = this.getTiddler(tiddler.fields.title); // Check if we're dealing with a plugin if(tiddler && tiddler.hasField("plugin-type") && tiddler.hasField("version") && existingTiddler && existingTiddler.hasField("plugin-type") && existingTiddler.hasField("version")) { // Reject the incoming plugin if it is older if(!$tw.utils.checkVersions(tiddler.fields.version,existingTiddler.fields.version)) { return false; } } // Fall through to adding the tiddler this.addTiddler(tiddler); return true; }; /* Return a hashmap of the fields that should be set when a tiddler is created */ exports.getCreationFields = function() { if(this.getTiddlerText(TIMESTAMP_DISABLE_TITLE,"").toLowerCase() !== "yes") { var fields = { created: new Date() }, creator = this.getTiddlerText(USER_NAME_TITLE); if(creator) { fields.creator = creator; } return fields; } else { return {}; } }; /* Return a hashmap of the fields that should be set when a tiddler is modified */ exports.getModificationFields = function() { if(this.getTiddlerText(TIMESTAMP_DISABLE_TITLE,"").toLowerCase() !== "yes") { var fields = Object.create(null), modifier = this.getTiddlerText(USER_NAME_TITLE); fields.modified = new Date(); if(modifier) { fields.modifier = modifier; } return fields; } else { return {}; } }; /* Return a sorted array of tiddler titles. Options include: sortField: field to sort by excludeTag: tag to exclude includeSystem: whether to include system tiddlers (defaults to false) */ exports.getTiddlers = function(options) { options = options || Object.create(null); var self = this, sortField = options.sortField || "title", tiddlers = [], t, titles = []; this.each(function(tiddler,title) { if(options.includeSystem || !self.isSystemTiddler(title)) { if(!options.excludeTag || !tiddler.hasTag(options.excludeTag)) { tiddlers.push(tiddler); } } }); tiddlers.sort(function(a,b) { var aa = a.fields[sortField].toLowerCase() || "", bb = b.fields[sortField].toLowerCase() || ""; if(aa < bb) { return -1; } else { if(aa > bb) { return 1; } else { return 0; } } }); for(t=0; t= 2 ? arguments[arg++] : {}, callback = arguments[arg++], titles = this.getTiddlers(options), t, tiddler; for(t=0; t= 0) { ++newPos; } } // If a new position is specified, let's move it if (newPos !== -1) { // get its current Pos, and make sure // sure that it's _actually_ in the list // and that it would _actually_ move // (#4275) We don't bother calling // indexOf unless we have a new // position to work with var currPos = titles.indexOf(title); if(currPos >= 0 && newPos !== currPos) { // move it! titles.splice(currPos,1); if(newPos >= currPos) { newPos--; } titles.splice(newPos,0,title); } } } } } var list = this.getTiddlerList(listTitle); if(!array || array.length === 0) { return []; } else { var titles = [], t, title; // First place any entries that are present in the list for(t=0; t0 && fieldIndex