/***
|Description|checks and reports updates of installed extensions on startup, introduces a macro/backstage button to explore, install and update extensions|
|Version |0.6.2|
|Author |Yakov Litvin|
|Source |https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsExplorerPlugin.js|
|License |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
!!!Installation & configuration
Installation of the plugin is as usual: import the tiddler or copy and tag it with {{{systemConfig}}}; reload TW.
{{FpG{not yet (will be useful once collections are introduced): In some cases, you may want to customize AvailableExtensions: {{PoGc{when/why? how?}}} }}}
!!!What EEP does, how to use it
Once you install this plugin, on startup, it will try to check if installed extensions have any updates available and report if it finds any. An update of a particular extension is looked up by the url in the Source slice (see this tiddler for example). EEP will recognize an "update" if it finds the content by that url, and that content has a Version slice and the version is higher than the installed one (like: 0.4.2 is higher than 0.3.9; 0.0.1 is also higher than none).
It also adds "explore extensions" in the backstage (and the {{{<<extensionsExplorer>>}}} macro with the same interface) that shows some extensions available for installation and the list of installed plugins with buttons to check for updates.
Note: With some TW savers/servers, loading an extension may fail if its author hasn't enabled CORS on the server pointed by Source.
!!!For extension authors: how to prepare extensions and repositories
To make EEP find updates for your extensions, you have to
# put it somewhere in the internet:
** the server should have CORS enabled (~GitHub is fine);
** the extension should be in either form: "plain text" (.js or .txt file extension) or a tiddler in a TW (.html extension);
# ensure that the extension has a Source slice with a url that points to itself (i.e. where to look for the latest version):
** for plain text, one can use a direct url, like: https://raw.githubusercontent.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/master/ShowUnsavedPlugin.js;
** for ~GitHub, one can also use the url of the UI page (i.e. navigate to it via ~GitHub UI and copy the address): https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js;
** for a tiddler inside a TW, use a permalink, like: https://TiddlyTools.com/Classic/#NestedSlidersPlugin (note that the Source slice in this plugin is in fact outdated: http://www.TiddlyTools.com/#NestedSlidersPlugin – you should avoid that as this will break the updating flow);
** for a tiddler inside a TW on ~GitHub, use ~GitHub Pages (this is in fact how ~TiddlyTools is served, they just use a custom domain; an example of an "ordinary" url: https://yakovl.github.io/TiddlyWiki_ExtraFilters/#ExtraFiltersPlugin);
** for your dev flow, it may be useful to put the plugin to ~GitHub as a .js file and load it into the demo TW via [[TiddlerInFilePlugin|https://github.com/YakovL/TiddlyWiki_TiddlerInFilePlugin]]. An example of such setup can be found [[here|https://github.com/YakovL/TiddlyWiki_FromPlaceToPlacePlugin]].
To make your extension explorable, you can do one of the following:
* simply announce [[in the community|https://groups.google.com/group/TiddlyWikiClassic]] or elsewhere
* [single extension, PR to .. collection]
* [multiple extensions, own collection, PR to ..]
{{PoGc{collections: not yet}}}
***/
//{{{
// Returns the slice value if it is present or defaultText otherwise
//
Tiddler.prototype.getSlice = Tiddler.prototype.getSlice || function(sliceName, defaultText) {
let re = TiddlyWiki.prototype.slicesRE, m
re.lastIndex = 0
while(m = re.exec(this.text)) {
if(m[2]) {
if(m[2] == sliceName) return m[3]
} else {
if(m[5] == sliceName) return m[6]
}
}
return defaultText
}
const centralSourcesListName = "AvailableExtensions"
config.macros.extensionsExplorer = {
lingo: {
// TODO: review order, looks somewhat chaotic
// TODO: probably add listeners of custom events, displayMessage there
installButtonLabel: "install",
installButtonPrompt: "get and install this extension",
getFailedToLoadMsg: name => "failed to load " + name,
getSucceededToLoadMsg: name => `loaded ${name}, about to import and install...`,
noSourceUrlAvailable: "no source url",
getEvalSuccessMsg: name => `Successfully installed ${name} (reload is not necessary)`,
getEvalFailMsg: (name, error) => `${name} failed with error: ${error}`,
getImportSuccessMsg: (title, versionString, isUpdated) => isUpdated ?
`Updated ${title}${versionString ? " to " + versionString : ""}` :
`Imported ${title}${versionString ? " v" + versionString : ""}`,
updateButtonCheckLabel: "check",
updateButtonCheckPrompt: "check for updates",
updateButtonUpdateLabel: "update",
updateButtonUpdatePrompt: "install available update",
getUpdateAvailableMsg: name => `update of ${name} is available!`,
getUpdateAvailableAndVersionsMsg: (existingTiddler, newTiddler) => {
const getVersionString = config.macros.extensionsExplorer.getVersionString
return `update of ${existingTiddler.title} is available ` +
"(current version: " + getVersionString(existingTiddler) +
", available version: " + getVersionString(newTiddler) + ")"
},
updateNotAvailable: "update is not available",
getUpdateConfirmMsg: (title, loadedVersion, presentVersion) => {
const loadedVersionString = loadedVersion ? formatVersion(loadedVersion) : ""
const presentVersionString = presentVersion ? formatVersion(presentVersion) : ""
return `Would you like to update ${title}` +
` (new version: ${loadedVersionString || "unknown"}, ` +
`current version: ${presentVersionString || "unknown"})?`
},
centralSourcesListAnnotation: "The JSON here describes extensions so that ExtensionsExplorerPlugin can install them"
},
// helpers specific to tiddler format
guessExtensionType: function(tiddler) {
if(tiddler.tags.contains('systemConfig') ||
tiddler.getSlice('Type', '').toLowerCase() == 'plugin' ||
/Plugin$/.exec(tiddler.title)
)
return 'plugin'
},
// We use the server.host field a bit different than the core does (see importing):
// we keep #TiddlerName part which won't hurt except for the plugin https://github.com/TiddlyWiki/tiddlywiki/blob/master/plugins/Sync.js (which we kinda substitute anyway),
// we also don't set server.type and server.page.revision fields yet (unlike import); see also server.workspace, wikiformat fields.
sourceUrlField: 'server.host',
getSourceUrl: function(tiddler) {
return tiddler.fields[this.sourceUrlField] || tiddler.getSlice('Source')
//# try also the field set by import (figure the name by experiment)
},
setSourceUrl: function(tiddler, url) {
//# simple implementation, not sure if setValue should be used instead
tiddler.fields[this.sourceUrlField] = url
},
getDescription: tiddler => tiddler.getSlice('Description', ''),
getVersionString: tiddler => tiddler.getSlice('Version', ''),
getVersion: function(tiddler) {
const versionString = this.getVersionString(tiddler)
//# should use a helper from core instead
const parts = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(versionString)
return parts ? {
major: parseInt(parts[1]),
minor: parseInt(parts[2]),
revision: parseInt(parts[3] || '0')
} : {}
},
// helpers to get stuff from external repos
//# start from hardcoding 1 (.oO data sctructures needed
// for getAvailableExtensions and various user scenarios),
// then several (TW/JSON, local/remote)
availableRepositories: [],
getAvailableRepositories: function() {
return this.availableRepositories
},
// fallback used when AvailableExtensions is empty
defaultAvailableExtensions: [
{
url: 'https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsCollection.txt',
description: 'A central extensions collection for ExtensionsExplorerPlugin meant to both gather collections of existing extensions and help new authors make their work more explorable',
type: 'collection'
},
{
// js file @ github - worked /# simplify url to be inserted?
name: 'ShowUnsavedPlugin',
sourceType: 'txt',
url: 'https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js',
description: 'highlights saving button (bold red by default) and the document title (adds a leading "*") when there are unsaved changes',
type: 'plugin',
text: ''
},
{
url: 'https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/DarkModePlugin.js',
description: 'This plugin introduces "dark mode" (changes styles) and switching it by the {{{darkMode}}} macro and operating system settings'
},
{
// in TW @ remote (CORS-enabled) – worked
name: 'FieldsEditorPlugin',
sourceType: 'tw',
url: 'https://yakovl.github.io/VisualTW2/VisualTW2.html#FieldsEditorPlugin',
description: 'adds controls (create/edit/rename/delete) to the "fields" toolbar dropdown',
type: 'plugin'
},
{
// txt file @ remote without CORS – worked with _
url: 'http://yakovlitvin.pro/TW/pre-releases/Spreadsheets.html#HandsontablePlugin',
description: 'a test plugin on a site without CORS'
},
{
url: 'https://github.com/tobibeer/TiddlyWikiPlugins/blob/master/plugins/ListFiltrPlugin.js'
}
],
guessNameByUrl: function(extension) {
if(!extension.url) return undefined
const urlParts = extension.url.split('#')
// site.domain/path/tw.html#TiddlerName or site.domain/path/#TiddlerName
if(urlParts.length > 1 && /(\.html|\/)$/.exec(urlParts[0])) return urlParts[1]
// <url part>/TiddlerName.txt or <url part>/TiddlerName.js
const textPathMatch = /\/(\w+)\.(js|txt)$/.exec(urlParts[0])
return textPathMatch ? textPathMatch[1] : undefined
},
collectionTag: 'systemExtensionsCollection',
parseCollection: function(text) {
/* expected format:
< additional info, like |Source|...| and other metadata >
//{{{
< extensions as JSON >
//}}}
*/
const match = /(\/\/{{{)\s+((?:.|\n)+)\s+(\/\/}}})$/.exec(text)
if(match) try {
return JSON.parse(match[2])
} catch (e) {
console.log(`problems with parsing ${centralSourcesListName}:`, e)
return null
}
},
// checks .centralSourcesListName, .defaultAvailableExtensions, collections
getAvailableExtensions: function() {
const listText = store.getTiddlerText(centralSourcesListName)
const availableExtensions = this.parseCollection(listText)
|| this.defaultAvailableExtensions
const otherCollections = store.filterTiddlers("[tag[" + this.collectionTag + "]]")
for(const collectionTiddler of otherCollections) {
const extensions = this.parseCollection(collectionTiddler.text)
// for now, just merge
if(extensions) for(const extension of extensions) {
availableExtensions.push(extension)
}
}
//# move name normalizing to the reading method
// once we move the list of available extensions from hardcode
for(const extension of availableExtensions) {
extension.name = extension.name || this.guessNameByUrl(extension)
}
return availableExtensions
},
// map by url of extension tiddlers
// (those loaded from txt may be detected by !tiddler.creator)
availableUpdatesCache: {},
cacheAvailableUpdate: function(sourceUrl, tiddler) {
this.availableUpdatesCache[sourceUrl] = { tiddler: tiddler }
},
// github urls like https://github.com/tobibeer/TiddlyWikiPlugins/blob/master/plugins/FiltrPlugin.js
// are urls of user interface; to get raw code, we use the official githubusercontent.com service
// also, we change the old urls https://raw.github.com/tobibeer/TiddlyWikiPlugins/master/plugins/FiltrPlugin.js
getUrlOfRawIfGithub: function(url) {
const ghUrlRE = /^https:\/\/github\.com\/(\w+?)\/(\w+?)\/blob\/(.+)$/
const oldGhRawUrlRE = /^https:\/\/raw.github.com\/(\w+?)\/(\w+?)\/(.+)$/
//# test
const match = ghUrlRE.exec(url) || oldGhRawUrlRE.exec(url)
if(match) return 'https://raw.githubusercontent.com/' + match[1] + // username
'/' + match[2] + // repository name
'/' + match[3] // path
return url
},
twsCache: {}, // map of strings
/*
@param sourceType: 'tw' | string | fasly (default = 'txt') -
of the tiddler source (a TW or a text file)
@param url: string - either url of the text file or url#TiddlerName
for a TW (TiddlerName defines the title of the tiddler to load)
@param title: string - is assigned to the loaded tiddler
@param callback: tiddler | null => void
support second param of callback? (error/xhr)
*/
loadExternalTiddler: function(sourceType, url, title, callback, useCache) {
sourceType = sourceType || this.guessSourceType(url)
//# if sourceType is uknown, we can load file and guess afterwards
if(sourceType == 'tw') {
const tiddlerName = url.split('#')[1] || title
const requestUrl = url.split('#')[0]
const cache = this.twsCache
const onTwLoad = function(success, params, responseText, url, xhr) {
//# pass more info? outside: warn?
if(!success) return callback(null)
if(!useCache) cache[requestUrl] = responseText
const externalTW = new TiddlyWiki()
const result = externalTW.importTiddlyWiki(responseText)
//# pass more info? outside: warn?
if(!result) return callback(null)
const tiddler = externalTW.fetchTiddler(tiddlerName)
tiddler.title = title
callback(tiddler)
// above is a simple "from scratch" implementation
//# should we reuse existing core code? (see import)
// currently, this only loads and passes tiddler,
// actual import is done in _
const context = {
adaptor: {},
complete: function() {}
}
// FileAdaptor.loadTiddlyWikiSuccess(context, );
//# import, see ...
//# tiddler.title = title;
//# callback(tiddler);
}
if(useCache && cache[requestUrl])
onTwLoad(true, null, cache[requestUrl])
else
httpReq('GET', requestUrl, onTwLoad)
} else {
url = this.getUrlOfRawIfGithub(url)
httpReq('GET', url, function(success, params, responseText, url, xhr) {
//# pass more info? outside: warn?
if(!success) return callback(null)
const tiddler = new Tiddler(title)
tiddler.text = responseText
tiddler.generatedByTextOnly = true
callback(tiddler)
})
}
},
getInstalledExtensions: function() {
//# instead of returning tiddlers, create extension objects,
// those should have ~isInstalled, ~isEnabled, ~hasUpdates flags
// (and change refresh accordingly)
return store.filterTiddlers(`[tag[systemConfig]] ` +
`[tag[${this.collectionTag}]] [[${centralSourcesListName}]]`)
//# implement others: themes, transclusions
},
// for each installed extension, check for update and reports (now: displays message)
init: function() {
//# set delegated handlers of install, update buttons
const extensionTiddlers = this.getInstalledExtensions()
if(!config.options.chkSkipExtensionsUpdatesCheckOnStartup)
for(const eTiddler of extensionTiddlers) {
const url = this.getSourceUrl(eTiddler)
if(!url) continue
this.checkForUpdate(url, eTiddler, result => {
console.log(`checkForUpdate for ${url},`, eTiddler, 'result is:', result)
if(result.tiddler && !result.noUpdateMessage) {
displayMessage(this.lingo.getUpdateAvailableAndVersionsMsg(eTiddler, result.tiddler))
}
//# either report each one at once,
// (see onUpdateCheckResponse)
// create summary and report,
// (use availableUpdates)
// create summary and just show "+4" or alike (better something diminishing),
// or even update (some of) ext-s silently
//# start with creating summary
})
}
const taskName = "explorePlugins"
config.backstageTasks.push(taskName)
config.tasks[taskName] = {
text: "explore extensions",
tooltip: "see if there's any updates or install new ones",
content: '<<extensionsExplorer>>',
}
},
handler: function(place, macroName, params, wikifier, paramString) {
const tableHeaderMarkup = "|name|description|version||h"
// name is supposted to be a link to the repo; 3d row – for "install" button
wikify(tableHeaderMarkup, place)
const table = place.lastChild
jQuery(table).attr({ refresh: 'macro', macroName: macroName })
.addClass('extensionsExplorer').append('<tbody>')
this.refresh(table)
},
// grabs list of available extensions and shows with buttons to install;
// for each installed plugin, shows a button to check update or "no url" message,
refresh: function(table) {
const $tbody = jQuery(table).find('tbody')
.empty()
// safe method (no wikification, innerHTML etc)
const appendRow = function(cells) {
const row = document.createElement('tr')
const nameCell = createTiddlyElement(row, 'td')
if(cells.url)
createExternalLink(nameCell, cells.url, cells.name)
else
createTiddlyLink(nameCell, cells.name, true)
createTiddlyElement(row, 'td', null, null, cells.description)
createTiddlyElement(row, 'td', null, null, cells.version)
const actionsCell = createTiddlyElement(row, 'td')
for(const e of cells.actionElements)
actionsCell.appendChild(e)
$tbody.append(row)
}
//# when implemented: load list of available extensions (now hardcoded)
const installedExtensionsTiddlers = this.getInstalledExtensions()
.sort((e1, e2) => {
const up1 = this.availableUpdatesCache[this.getSourceUrl(e1)]
const up2 = this.availableUpdatesCache[this.getSourceUrl(e2)]
return up1 && up2 ? 0 :
up1 && !up2 ? -1 :
up2 && !up1 ? +1 :
!this.getSourceUrl(e1) ? +1 :
!this.getSourceUrl(e2) ? -1 : 0
})
// show extensions available to install
const availableExtensions = this.getAvailableExtensions()
for(const extension of availableExtensions) {
// skip installed
if(installedExtensionsTiddlers.some(tid => tid.title === extension.name
&& this.getSourceUrl(tid) === extension.url)) continue
if(!extension.name && extension.sourceType == 'tw')
extension.name = extension.url.split('#')[1]
appendRow({
name: extension.name,
url: extension.url,
description: extension.description,
version: extension.version,
actionElements: [
createTiddlyButton(null,
this.lingo.installButtonLabel,
this.lingo.installButtonPrompt,
() => this.grabAndInstall(extension) )
]
})
}
//# add link to open, update on the place of install – if installed
// show installed ones.. # or only those having updates?
$tbody.append(jQuery(`<tr><td colspan="4" style="text-align: center;">Installed</td></tr>`))
for(const extensionTiddler of installedExtensionsTiddlers) {
//# limit the width of the Description column/whole table
const updateUrl = this.getSourceUrl(extensionTiddler)
//# check also list of extensions to install
const onUpdateCheckResponse = (result, isAlreadyReported) => {
if(!result.tiddler) {
displayMessage(this.lingo.updateNotAvailable)
//# use result.error
return
}
const versionOfLoaded = this.getVersion(result.tiddler)
const versionOfPresent = this.getVersion(extensionTiddler)
if(compareVersions(versionOfLoaded, versionOfPresent) >= 0) {
displayMessage(this.lingo.updateNotAvailable)
//# use result.error
return
}
debugger;
if(!isAlreadyReported) displayMessage(this.lingo.getUpdateAvailableMsg(extensionTiddler.title), updateUrl)
//# later: better than confirm? option for silent?
if(confirm(this.lingo.getUpdateConfirmMsg(
extensionTiddler.title,
versionOfLoaded, versionOfPresent))
) {
this.updateExtension(result.tiddler, updateUrl)
// displayMessage(this.lingo.getImportedUpdateMsg(
// result.tiddler.title,
// this.getVersionString(result.tiddler)
// ))
}
}
const checkUpdateButton = createTiddlyButton(null,
this.lingo.updateButtonCheckLabel,
this.lingo.updateButtonCheckPrompt,
() => this.checkForUpdate(updateUrl, extensionTiddler,
onUpdateCheckResponse))
const cachedUpdate = this.availableUpdatesCache[updateUrl]
const installUpdateButton = createTiddlyButton(null,
this.lingo.updateButtonUpdateLabel,
this.lingo.updateButtonUpdatePrompt,
() => onUpdateCheckResponse(cachedUpdate, true))
appendRow({
name: extensionTiddler.title,
description: this.getDescription(extensionTiddler),
version: this.getVersionString(extensionTiddler),
actionElements: [
!updateUrl ? document.createTextNode(this.lingo.noSourceUrlAvailable) :
cachedUpdate ? installUpdateButton :
checkUpdateButton
]
})
}
},
grabAndInstall: function(extension) {
//# initial goal: add displayMessage on .install success
//# move ~user interaction~ (displayMessage) into callbacks? merge with checkForUpdate?
if(!extension) return
if(extension.text) {
//# is this ever called? do we cache extension.text on checking update? what about version?
const extensionTiddler = new Tiddler(extension.name)
extensionTiddler.text = extension.text
extensionTiddler.generatedByTextOnly = true
//# share 3 ↑ lines as ~internalize helper (with loadExternalTiddler)
this.install(extensionTiddler, extension.type, extension.url)
return
}
this.loadExternalTiddler(
extension.sourceType,
extension.url,
extension.name,
tiddler => {
if(!tiddler) {
displayMessage(this.lingo.getFailedToLoadMsg(extension.name))
return
}
displayMessage(this.lingo.getSucceededToLoadMsg(tiddler.title))
this.install(tiddler, extension.type ||
this.guessExtensionType(tiddler), extension.url)
}
)
},
// evaluate if a plugin, import
//# simple unsafe version, no dependency handling, registering as installed,
// _install-only-once check_, result reporting, refreshing/notifying, ..
install: function(extensionTiddler, extensionType, sourceUrl) {
if(!extensionTiddler) return
//# displayMessage _
const { text, title } = extensionTiddler
switch(extensionType) {
case 'plugin':
// enable at once
try {
eval(text)
displayMessage(this.lingo.getEvalSuccessMsg(title))
} catch(e) {
displayMessage(this.lingo.getEvalFailMsg(title, e))
//# don't import? only on confirm?
//# if(!confirm(title + " seem to produce errors, import anyway?")) return
}
// plugin-specific import preparation
extensionTiddler.tags.pushUnique('systemConfig')
break;
case 'collection':
extensionTiddler.tags.pushUnique(this.collectionTag)
break;
default:
//# add _ tag for themes?
//# displayMessage _
}
// actually import etc
this.updateExtension(extensionTiddler, sourceUrl)
//# what if exists already? (by the same name; other name)
},
updateExtension: function(extensionTiddler, sourceUrl) {
// import
var existingTiddler = store.fetchTiddler(extensionTiddler.title)
if(extensionTiddler.generatedByTextOnly && existingTiddler) {
existingTiddler.text = extensionTiddler.text
existingTiddler.modified = new Date()
//# update also modifier? changecount?
} else {
store.addTiddler(extensionTiddler)
}
if(sourceUrl && this.getSourceUrl(extensionTiddler) !== sourceUrl) {
this.setSourceUrl(extensionTiddler, sourceUrl)
}
delete this.availableUpdatesCache[sourceUrl]
store.setDirty(true)
//# store url for updating if slice is not present?
// make explorer and other stuff refresh
store.notify(extensionTiddler.title, true)
//# .oO reloading, hot reinstalling
displayMessage(this.lingo.getImportSuccessMsg(extensionTiddler.title,
this.getVersionString(extensionTiddler), !!existingTiddler))
},
guessSourceType: function(url) {
if(/\.(txt|js)$/.exec(url.split('#')[0])) return 'txt'
//# guess by url instead, fall back to 'txt'
return 'tw'
},
//# careful: extension keyword is overloaded (extension object/tiddler)
/*
tries to load update for tiddler, if succeeds calls callback with
argument depending on whether it has newer version than the existing one
@param url: _
@param extensionTiddler: _
@param callback: is called [not always yet..] with argument
{ tiddler: Tiddler | null, error?: string, noUpdateMessage?: string }
if update is found and it has version newer than extensionTiddler,
it is called with { tiddler: Tiddler }
*/
checkForUpdate: function(url, extensionTiddler, callback) {
if(!url) return
const title = extensionTiddler.title
this.loadExternalTiddler(null, url, title, loadedTiddler => {
if(!loadedTiddler) return callback({
tiddler: null,
error: "" //# specify
})
if(compareVersions(this.getVersion(loadedTiddler),
this.getVersion(extensionTiddler)
) >= 0)
//# also get and compare modified dates?
{
//# what about undefined?
console.log('loaded is not newer')
callback({
tiddler: loadedTiddler,
// TODO: move to lingo; may be change to noUpdate
noUpdateMessage: "current version is up-to-date"
})
} else {
this.cacheAvailableUpdate(url, loadedTiddler)
callback({ tiddler: loadedTiddler })
}
})
}
}
config.shadowTiddlers[centralSourcesListName] = '//{{{\n' +
JSON.stringify(config.macros.extensionsExplorer.defaultAvailableExtensions, null, 2) +
'\n//}}}'
config.annotations[centralSourcesListName] =
config.macros.extensionsExplorer.lingo.centralSourcesListAnnotation
//}}}
/***
|''Name''|ForEachTiddlerPlugin|
|''Version''|1.3.3'|
|''Forked from''|[[abego.ForEachTiddlerPlugin|http://tiddlywiki.abego-software.de/#ForEachTiddlerPlugin]], by Udo Borkowski|
|''Author''|Yakov Litvin|
|''CoreVersion''|2.6.2|
|~|Although 2.6.2 is theoretically minimal TW version required for the correct operation, tests showed that the plugin works in 2.6.0, too.|
***/
//{{{
// defines whether to set "none" param to "same" if it is not used (i.e. = begin + end)
if(config.options.chkFetUseSameIfNone === undefined)
config.options.chkFetUseSameIfNone = true;
(function(){
// Only install once
if (version.extensions.ForEachTiddlerPlugin) {
alert("Warning: more than one copy of ForEachTiddlerPlugin is set to be launched");
return;
} else
version.extensions.ForEachTiddlerPlugin = {
source: "[repository url here]",
licence: "[licence url here]",
copyright: "Copyright (c) Yakov Litvin, 2012-2015 [url of the meta page]"
};
//============================================================================
// forEachTiddler Macro
//============================================================================
// ---------------------------------------------------------------------------
// Configurations and constants
// ---------------------------------------------------------------------------
config.macros.forEachTiddler =
{
actions: {
addToList: {},
write: {}
}
};
// ---------------------------------------------------------------------------
// The forEachTiddler Macro Handler
// ---------------------------------------------------------------------------
config.macros.forEachTiddler.handler = function(place,macroName,params,wikifier,paramString,tiddler)
{
// --- Pre-parsing for up-to-date params ----------------
var preParsedParams = this.getUpToDateParams(paramString);
// for backward compability, "params" are used as well
// --- Parsing ------------------------------------------
var parsedParams = this.parseParams(preParsedParams,params);
if (parsedParams.errorText) {
this.handleError(place, parsedParams.errorText);
return;
}//else
parsedParams.place = place;
parsedParams.inTiddler = tiddler ? tiddler : getContainingTiddler(place);
// --- "Static" processing ------------------------------
// Choose the action
var actionName = parsedParams.actionName;
var action = this.actions[actionName]; // no this is always a "known" action
// Create the element
var element = document.createElement(action.element);
jQuery(element).attr({ refresh: "macro", macroName: macroName }).data(parsedParams);
place.appendChild(element);
// --- "Dynamic" processing -----------------------------
this.refresh(element);
};
config.macros.forEachTiddler.refresh = function(element)
{
var parsedParams = jQuery(element).data(),
action = this.actions[parsedParams.actionName];
jQuery(element).empty();
try {
var tiddlersAndContext = this.getTiddlersAndContext(parsedParams);
// Perform the action
action.handler(element, tiddlersAndContext.tiddlers,
parsedParams.actionParameter, tiddlersAndContext.context);
} catch (e) {
this.handleError(place, e);
}
};
config.macros.forEachTiddler.oldFashionParams = ["in", "filter", "where", "sortBy",
"script", "write", "begin", "end", "none", "toFile", "withLineSeparator"
//# add to docs: new actions are to be added here or used in name:param notation only
];
config.macros.forEachTiddler.getUpToDateParams = function(paramString)
// turns stuff like "... where 'tiddler.title.length < 20' ..."
// to "... where:'tiddler.title.length < 20' ..." and then applies parseParams,
// which allows to use params in an arbitrary order and other goodies of parsed params
{
var paramPairRegExp = new RegExp("("+this.oldFashionParams.join("|")+")\\s+"+
"("+ // adapted from String.prototype.parseParams
'(?:"(?:(?:\\\\")|[^"])+")|'+ // double-quoted param
"(?:'(?:(?:\\\\')|[^'])+')|"+ // quoted param
"(?:\\[\\[(?:\\s|\\S)*?\\]\\])|"+ // [[...]]-wrapped
"(?:\\{\\{(?:\\s|\\S)*?\\}\\})|"+ // {{...}}-wrapped
"(?:[^\"':\\s][^\\s:]*)|"+ // non-wrapped
"(?:\"\")|(?:'')"+ // empty '' or ""
")","g");
paramString =
paramString.replace(paramPairRegExp,function($0,$1,$2){ return $1+":"+$2; });
return paramString.parseParams("filter",null,true,false,true);
// the first unnamed param is now considered as the 'filter' param
};
// Returns an object with properties "tiddlers" and "context".
// tiddlers holds the (sorted) tiddlers selected by the parameter,
// context the context of the execution of the macro.
//
// The action is not yet performed.
//
// @param parameter holds the parameter of the macro as separate properties.
// The following properties are supported:
//
// place
// filter
// whereClause
// sortClause
// sortAscending
// actionName
// actionParameter
// scriptText
// tiddlyWikiPath
//
// All properties are optional.
// For most actions the place property must be defined.
//
config.macros.forEachTiddler.getTiddlersAndContext = function(parameter)
{
var context = config.macros.forEachTiddler.createContext(parameter.place, parameter.filter, parameter.whereClause, parameter.sortClause, parameter.sortAscending, parameter.actionName, parameter.actionParameter, parameter.scriptText, parameter.tiddlyWikiPath, parameter.inTiddler);
var tiddlyWiki = parameter.tiddlyWikiPath ? this.loadTiddlyWiki(parameter.tiddlyWikiPath) : store;
context["tiddlyWiki"] = tiddlyWiki;
// Get the tiddlers, as defined by the filter and the whereClause
var tiddlers = this.findTiddlers(parameter.filter, parameter.whereClause, context, tiddlyWiki);
context["tiddlers"] = tiddlers;
// Sort the tiddlers, when sorting is required.
if (parameter.sortClause)
this.sortTiddlers(tiddlers, parameter.sortClause, parameter.sortAscending, context);
return {tiddlers: tiddlers, context: context};
};
// ---------------------------------------------------------------------------
// The actions
// ---------------------------------------------------------------------------
// Internal.
//
// --- The addToList Action -----------------------------------------------
//
config.macros.forEachTiddler.actions.addToList.element = "ul";
config.macros.forEachTiddler.actions.addToList.handler = function(place, tiddlers, parameter, context)
{
for (var i = 0; i < tiddlers.length; i++)
{
var tiddler = tiddlers[i];
var listItem = document.createElement("li");
place.appendChild(listItem);
createTiddlyLink(listItem, tiddler.title, true);
}
};
// Internal.
//
// --- The write Action ---------------------------------------------------
//
config.macros.forEachTiddler.actions.write.element = "span";
config.macros.forEachTiddler.actions.write.handler = function(place, tiddlers, parameter, context)
{
var params = parameter[0].nonParsedParams;
if(!parameter[0]["write"])
return this.handleError(place, "Missing expression behind 'write'.");
var textExpression = config.macros.forEachTiddler.paramEncode(getParam(parameter,["write"]));
var getParamExpression = function(name)
{
if(params.contains(name) && !parameter[0][name])
throw "Missing text behind '%0'".format([name]);
return config.macros.forEachTiddler.paramEncode(getParam(parameter,name));
};
var beginExpression = getParamExpression("begin");
var endExpression = getParamExpression("end");
var noneExpression = getParamExpression("none")
|| (config.options.chkFetUseSameIfNone ? "same" : "");
var lineSeparator = undefined;
if(params.contains("toFile") && !parameter[0]["toFile"])
return this.handleError(place, "Filename expected behind 'toFile' of 'write' action.");
var filename = getParam(parameter,"toFile");
filename = config.macros.forEachTiddler.paramEncode(filename);
if(filename) {
filename = config.macros.forEachTiddler.getLocalPath(filename);
if(params.contains("withLineSeparator")&& !parameter[0]["withLineSeparator"])
return this.handleError(place, "Line separator text expected behind 'withLineSeparator' of 'write' action.")
lineSeparator = getParamExpression("withLineSeparator");
}
// Perform the action.
var func = config.macros.forEachTiddler.getEvalTiddlerFunction(textExpression, context),
count = tiddlers.length,
text = "";
if (count > 0 && beginExpression)
text += config.macros.forEachTiddler.getEvalTiddlerFunction(beginExpression, context)(undefined, context, count, undefined);
for (var i = 0; i < count; i++) {
var tiddler = tiddlers[i];
text += func(tiddler, context, count, i);
}
if (count > 0 && endExpression)
text += config.macros.forEachTiddler.getEvalTiddlerFunction(endExpression, context)(undefined, context, count, undefined);
if (count == 0 && noneExpression)
{
var beginAddition = beginExpression ? "("+beginExpression+")" : '""',
endAddition = endExpression ? "("+ endExpression+")" : '""',
bothAddition = "("+beginAddition+"+"+endAddition+")";
noneExpression = noneExpression
.replace(/(?=\W|^)begin(?=\W|$)/,beginAddition)
.replace(/(?=\W|^)end(?=\W|$)/, endAddition)
.replace(/(?=\W|^)same(?=\W|$)/, bothAddition);
text += config.macros.forEachTiddler.getEvalTiddlerFunction(noneExpression, context)(undefined, context, count, undefined);
}
if (filename) {
if (lineSeparator !== undefined) {
lineSeparator = lineSeparator.replace(/\\n/mg, "\n").replace(/\\r/mg, "\r");
text = text.replace(/\n/mg,lineSeparator);
}
saveFile(filename, convertUnicodeToUTF8(text));
} else
wikify(text, place, null/* highlightRegExp */, context.inTiddler);
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
config.macros.forEachTiddler.parseParams = function(preParsedParams,params)
{
if(params.contains("in") && !preParsedParams[0]["in"])
return { errorText: "TiddlyWiki path expected behind 'in'." };
var TWpath = getParam(preParsedParams,"in");
if(params.contains("filter") && !preParsedParams[0]["filter"])
return { errorText: "No filter specified." };
if(params.contains("where") && !preParsedParams[0]["where"])
return { errorText: "whereClause missing behind 'where'." };
var where = getParam(preParsedParams,"where");
var ascending = true;
if(params.contains("sortBy") && !preParsedParams[0]["sortBy"])
return { errorText: "sortClause missing behind 'sortBy'." };
var sortClause = getParam(preParsedParams,"sortBy");
if(preParsedParams[0]["sortBy"] && preParsedParams[0]["sortBy"].length > 1)
ascending = !(preParsedParams[0]["sortBy"][1] == "descending");
if(params.contains("script") && !preParsedParams[0]["script"])
return { errorText: "scriptText is not specified." };
var scriptText = !preParsedParams ? "" :
(!preParsedParams[0]["script"] ? "" :
preParsedParams[0]["script"].join(";"));
var actionName = "addToList";
for(var knownActionName in this.actions)
if(preParsedParams[0][knownActionName]) {
actionName = knownActionName;
break;
}
// no error handling if there's an unknown action
// because now the order is not important and actionName can have another position
preParsedParams[0].nonParsedParams = params; // for parsing inside actions
return {
tiddlyWikiPath: this.paramEncode(TWpath),
filter: getParam(preParsedParams,"filter"),
whereClause: this.paramEncode(where) || true,
sortClause: this.paramEncode(sortClause),
sortAscending: ascending,
scriptText: this.paramEncode(scriptText),
actionName: actionName,
actionParameter:preParsedParams // not much need to cut out other params
}
};
var getContainingTiddler = function(e)
{
while(e && !hasClass(e,"tiddler"))
e = e.parentNode;
var title = e ? e.getAttribute("tiddler") : null;
return title ? store.getTiddler(title) : null;
};
// Internal.
//
config.macros.forEachTiddler.createContext = function(placeParam, filterParam, whereClauseParam, sortClauseParam, sortAscendingParam, actionNameParam, actionParameterParam, scriptText, tiddlyWikiPathParam, inTiddlerParam) {
return {
place : placeParam,
filter : filterParam,
whereClause : whereClauseParam,
sortClause : sortClauseParam,
sortAscending : sortAscendingParam,
script : scriptText,
actionName : actionNameParam,
actionParameter : actionParameterParam,
tiddlyWikiPath : tiddlyWikiPathParam,
inTiddler : inTiddlerParam, // the tiddler containing the <<forEachTiddler ...>> macro call.
viewerTiddler : getContainingTiddler(placeParam) //the tiddler showing the forEachTiddler result
};
};
// Internal.
//
// Returns a TiddlyWiki with the tiddlers loaded from the TiddlyWiki of the given path.
//
config.macros.forEachTiddler.loadTiddlyWiki = function(path, idPrefix)
{
if (!idPrefix)
idPrefix = "store";
var lenPrefix = idPrefix.length;
// Read the content of the given file
var content = loadFile(this.getLocalPath(path));
if(content === null)
throw "TiddlyWiki '"+path+"' not found.";
var tiddlyWiki = new TiddlyWiki();
if (!tiddlyWiki.importTiddlyWiki(content))
throw "File '"+path+"' is not a TiddlyWiki.";
tiddlyWiki.dirty = false;
return tiddlyWiki;
};
// Internal.
//
// Returns a function that has a function body returning the given javaScriptExpression.
// The function has the parameters:
//
// (tiddler, context, count, index)
//
config.macros.forEachTiddler.getEvalTiddlerFunction = function (javaScriptExpression, context) {
var script = context["script"];
// var functionText = "var theFunction = function(tiddler, context, count, index) { return "+javaScriptExpression+"}";
var functionText = "var theFunction = function(tiddler, context, count, index) { "+(script ? script+";" : "")+"return "+javaScriptExpression+"}";
// var fullText = (script ? script+";" : "")+functionText+";theFunction;";
var fullText = functionText+";theFunction;";
return eval(fullText);
};
// Internal.
//
config.macros.forEachTiddler.findTiddlers = function(filter, whereClause, context, tiddlyWiki) {
var result = [];
var func = config.macros.forEachTiddler.getEvalTiddlerFunction(whereClause, context);
if(filter) {
var tids = tiddlyWiki.filterTiddlers(filter);
for(var i = 0; i < tids.length; i++)
if(func(tids[i], context, undefined, undefined))
result.push(tids[i]);
} else
tiddlyWiki.forEachTiddler(function(title,tiddler) {
if(func(tiddler, context, undefined, undefined))
result.push(tiddler);
});
return result;
};
// Internal.
//
config.macros.forEachTiddler.sortAscending = function(tiddlerA, tiddlerB)
{
return ((tiddlerA.forEachTiddlerSortValue == tiddlerB.forEachTiddlerSortValue)
? 0
: ((tiddlerA.forEachTiddlerSortValue < tiddlerB.forEachTiddlerSortValue)
? -1
: +1))
};
// Internal.
//
config.macros.forEachTiddler.sortDescending = function(tiddlerA, tiddlerB)
{
return ((tiddlerA.forEachTiddlerSortValue == tiddlerB.forEachTiddlerSortValue)
? 0
: ((tiddlerA.forEachTiddlerSortValue < tiddlerB.forEachTiddlerSortValue)
? +1
: -1))
};
// Internal.
//
config.macros.forEachTiddler.sortTiddlers = function(tiddlers, sortClause, ascending, context) {
// To avoid evaluating the sortClause whenever two items are compared
// we pre-calculate the sortValue for every item in the array and store it in a
// temporary property ("forEachTiddlerSortValue") of the tiddlers.
var func = config.macros.forEachTiddler.getEvalTiddlerFunction(sortClause, context);
var count = tiddlers.length;
for (var i = 0; i < count; i++) {
var tiddler = tiddlers[i];
tiddler.forEachTiddlerSortValue = func(tiddler,context, undefined,undefined);
}
// Do the sorting
tiddlers.sort(ascending ? this.sortAscending : this.sortDescending);
// Delete the temporary property that holds the sortValue.
for (i = 0; i < tiddlers.length; i++)
delete tiddlers[i].forEachTiddlerSortValue;
};
// Internal.
//
// Creates an element that holds an error message
//
config.macros.forEachTiddler.createErrorElement = function(place, exception) {
var message = (exception.description) ? exception.description : exception.toString();
return createTiddlyElement(place,"span",null,"forEachTiddlerError","<<forEachTiddler ...>>: "+message);
};
// Internal.
//
// @param place [may be null]
//
config.macros.forEachTiddler.handleError = function(place, exception)
{
if(place)
this.createErrorElement(place, exception);
else
throw exception;
};
// Internal.
//
// Encodes the given string.
//
// Replaces
// "$))" to ">>"
// "$)" to ">"
//
config.macros.forEachTiddler.paramEncode = function(s)
{
if(!s) return s;
var reGTGT = new RegExp("\\$\\)\\)","mg");
var reGT = new RegExp("\\$\\)","mg");
return s.replace(reGTGT, ">>").replace(reGT, ">");
};
//# document the .paramEncode transformation of the params; or get rid of it?
// Internal.
//
// Returns the given original path (that is a file path, starting with "file:")
// as a path to a local file, in the systems native file format.
//
// Handles relative links, too.
//
config.macros.forEachTiddler.getLocalPath = function(originalPath) {
// code adapted from SharedTiddlersPlugin to handle relative paths
var originalAbsolutePath = originalPath;
if(originalAbsolutePath.search(/^((http(s)?)|(file)):/) != 0) {
// no protocol prefix..
if (originalAbsolutePath.search(/^(.\:\\)|(\\\\)|(\/)/) != 0){// is relative?
// as Unix filesystem root is "/", urls starting with it are not considered as relative
var currentUrl = document.location.toString();
var currentPath = (currentUrl.lastIndexOf("/") > -1) ?
currentUrl.substr(0, currentUrl.lastIndexOf("/") + 1) :
currentUrl + "/";
originalAbsolutePath = currentPath + originalAbsolutePath;
} else
// an "absolute" path to a local file. Prefix it with file://
originalAbsolutePath = "file://" + originalAbsolutePath;
// replace every \ by a /, to cover Windows style pathes
originalAbsolutePath = originalAbsolutePath.replace(/\\/mg,"/");
}
return getLocalPath(originalAbsolutePath);
};
// ---------------------------------------------------------------------------
// Stylesheet Extensions (may be overridden by local StyleSheet)
// ---------------------------------------------------------------------------
//
setStylesheet(
".forEachTiddlerError{color: #ffffff;background-color: #880000;}",
"forEachTiddler");
// ---------------------------------------------------------------------------
// fet alias for the the forEachTiddler Macro
// ---------------------------------------------------------------------------
config.macros.fet = config.macros.forEachTiddler;
//============================================================================
// utilities for String and Tiddler objects useful in fet macros
//============================================================================
// Returns true if the string starts with the given prefix, false otherwise.
//
String.prototype.startsWith = function(prefix) {
var n = prefix.length;
return (this.length >= n) && (this.slice(0, n) == prefix);
};
// Returns true if the string ends with the given suffix, false otherwise.
//
String.prototype.endsWith = function(suffix) {
var n = suffix.length;
return (this.length >= n) && (this.right(n) == suffix);
};
// Returns true when the string contains the given substring, false otherwise.
//
String.prototype.contains = function(substring) {
return this.indexOf(substring) >= 0;
};
})();
// Returns the slice value if it is present or defaultText otherwise
//
Tiddler.prototype.getSlice = function(sliceName,defaultText)
{
var re = TiddlyWiki.prototype.slicesRE;
re.lastIndex = 0;
var m = re.exec(this.text);
while(m) {
if(m[2]) {
if(m[2] == sliceName)
return m[3];
} else {
if(m[5] == sliceName)
return m[6];
}
m = re.exec(this.text);
}
return defaultText;
};
// Returns the section value if it is present or defaultText otherwise
//
Tiddler.prototype.getSection = function(sectionName,defaultText)
{
var beginSectionRegExp = new RegExp("(^!{1,6}[ \t]*" + sectionName.escapeRegExp() + "[ \t]*\n)","mg"),
sectionTerminatorRegExp = /^!/mg;
var match = beginSectionRegExp.exec(this.text), sectionText;
if(match) {
sectionText = this.text.substr(match.index+match[1].length);
match = sectionTerminatorRegExp.exec(sectionText);
if(match)
sectionText = sectionText.substr(0,match.index-1); // don't include final \n
return sectionText;
}
return defaultText;
};
var transText = function(tiddlerOrGetTiddlerTextArg, moreArguments)
{
var title = (tiddlerOrGetTiddlerTextArg instanceof Tiddler) ? tiddlerOrGetTiddlerTextArg.title : tiddlerOrGetTiddlerTextArg;
return "<<tiddler [[" + title + "]] "+ (moreArguments||"") +">>"
};
//}}}
/***
|Description ||
|Documentation|[[SetManagerPluginInfo]]|
|Source |http://yakovlitvin.pro/TW/pre-releases/ForEachTiddler%20+%20SetManagerPlugin.html#SetManagerPlugin|
|Author |Yakov Litvin|
|Requires |ForEachTiddlerPlugin SetFieldPlugin SharedTiddlersPlugin|
|~|ForEachTiddlerPlugin is not required for the operation of the plugin, but if FETP is present, it should be evaluated before this plugin (same for SharedTiddlersPlugin); also, there's no other simple way to use this plugin aside with FETP; SetFieldPlugin is required for quality (removes extra refreshing which quickens some actions and removes some representation bugs|
|Version |0.11.6|
|License |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
***/
//{{{
// sort counters methods ("S" stands for "sort")
Tiddler.prototype.getSCounterPlace = function(storageName)
{
storageName = storageName || config.macros.itemMenu.defaultField
var withFieldNameRegExp = /(.*?)@@(.*)/,
withFieldNameMatch = withFieldNameRegExp.exec(storageName);
return {
storageName: withFieldNameMatch ? withFieldNameMatch[1] : null,
fieldName: withFieldNameMatch ? withFieldNameMatch[2] : storageName
}
};
Tiddler.prototype.getRawSData = function(fieldName)
{
fieldName = fieldName || config.macros.itemMenu.defaultField;
if(!this.getIncludeURL || !this.getIncludeURL())
return store.getValue(this,fieldName);
};
Tiddler.prototype.getSCounter = function(fieldName)
{
var storageData = this.getSCounterPlace(fieldName),
storageName = storageData.storageName,
storageField = storageData.fieldName,
indexText;
if(!this.getIncludeURL || !this.getIncludeURL())
return parseInt(this.getRawSData(storageField));
// for included tiddlers use a separate tiddler as a stored index
if(storageName) {
var storageTiddler = store.fetchTiddler(storageName);
indexText = storageTiddler ?
storageTiddler.getRawSData(storageField) : "";
} else
indexText = store.getTiddlerText(storageField);
// find the line in the index which describes the tiddler, if present
var indexLineRegExp = config.macros.itemMenu.getIndexLineRegExp(this.title),
indexMatch = indexLineRegExp.exec(indexText);
return indexMatch ? parseInt(indexMatch[1]) : undefined;
};
Tiddler.prototype.setRawSData = function(fieldName, value)
{
fieldName = fieldName || config.macros.itemMenu.defaultField;
// reduced version of store.setValue(this, fieldName, value) for SData
TiddlyWiki.checkFieldName(fieldName);
fieldName = fieldName.toLowerCase();
if(TiddlyWiki.standardFieldAccess[fieldName]) return;
if(this.fields[fieldName] == value) return;
this.fields[fieldName] = ""+value; // as a String, only string values are stored
store.setDirty(true);
};
Tiddler.prototype.updateSIndex = function(indexText,value)
{
// find the line in the index which describes the tiddler, if present
var indexLineRegExp = config.macros.itemMenu.getIndexLineRegExp(this.title),
indexMatch = indexLineRegExp.exec(indexText);
var newIndexLine = config.macros.itemMenu.createIndexLine(this.title, value);
if(indexMatch)
indexText = indexText.replace(indexLineRegExp, newIndexLine);
else
indexText += (newIndexLine+"\n");
return indexText;
};
Tiddler.prototype.setSCounter = function(fieldName, value)
{
var storageData = this.getSCounterPlace(fieldName),
storageName = storageData.storageName,
storageField = storageData.fieldName,
indexText;
if(!this.getIncludeURL || !this.getIncludeURL()) {
this.setRawSData(storageField, value);
return;
};
// for included tiddlers use a separate tiddler as a stored index
// for orderField@@tiddlerName syntax, use the storageName tiddler
// for storage, otherwise use the fieldName tiddler
var indexTid = store.fetchTiddler(storageName || fieldName) ||
store.createTiddler(storageName || fieldName);
if(storageName) {
indexText = indexTid.getRawSData(storageField) || "";
indexText = this.updateSIndex(indexText, value);
indexTid.setRawSData(storageField, indexText);
} else
indexTid.text = this.updateSIndex(indexTid.text, value);
};
Tiddler.prototype.deleteSCounter = function(fieldName)
{
fieldName = fieldName.toLowerCase();
// use of StandardFields is unlikely, but don't remove them anyway
if(TiddlyWiki.standardFieldAccess[fieldName]) return;
if(this.getSCounter(fieldName) !== undefined) delete this.fields[fieldName];
};
// --------------------------------------------------------------------------------
TiddlyWiki.prototype.saveModifiedTiddler = function(title, newTitle, newBody, tags, fields, clearChangeCount, created, creator) {
var tidBeingChanged = (title instanceof Tiddler) ? title : this.fetchTiddler(title);
title = tidBeingChanged.title;
var conflictingTiddler = this.fetchTiddler(newTitle) || this.fetchTiddler(title);
if(conflictingTiddler && conflictingTiddler != tidBeingChanged)
if(!confirm("A tiddler named \""+title+"\" already exists. Do you want to overwrite it?"))
return;
return this.saveTiddler(title, newTitle, newBody,
config.options.txtUserName,
new Date(),
tags, fields, clearChangeCount, created, creator)
};
var preventOtherHandling = window.preventOtherHandling = function(e)
{
// prevent propagation
if (e.stopPropagation) e.stopPropagation();
e.cancelBubble = true;
// prevent browser action from firing
if(e.preventDefault) e.preventDefault();
e.returnValue = false;
// see https://learn.javascript.ru/default-browser-action
}
// helps to avoid popup closing on event
var wrapNoClose = window.wrapNoClose = function(func)
{
return function(ev)
{
if(func) func.apply(this,arguments);
var e = ev || window.event; // support old IE
if(!e)
return false;
preventOtherHandling(e);
};
};
// helps to make the height of textareas appropriate (a working prototype)
//# defaultHeight should be calced as a height of one line;
//# maxHeight - not more than 1/2 or 3/4 of the screen
//# also, better to get rid of shrinking scrollbar..
var adjustHeightToContent = function()
{
var defaultHeight = 30;
var maxHeight = 400;
jQuery(this).height(defaultHeight);
jQuery(this).height(Math.min(this.scrollHeight, maxHeight));
};
// --------------------------------------------------------------------------------
// constants for using with jQuery .which() in keyup handlers
var $enter = 13,
$up = 38,
$down = 40,
$pgUp = 33,
$pgDn = 34
// --------------------------------------------------------------------------------
// detecting touch-screens (see http://stackoverflow.com/a/4819886/3995261)
window.isOpenedOnTouchScreen = function()
{
return !!('ontouchstart' in this);
}
//}}}
//{{{
config.macros.itemMenu =
{
getIndexLineRegExp: function(tiddlerName) {
return new RegExp(tiddlerName.escapeRegExp() + ": ([0-9]+)");
},
createIndexLine: function(tiddlerName, value) {
return tiddlerName+": "+value;
},
defaultField: "orderCounter",
itemMenuClass: "listMenuButton",
sortByCounter: function(tiddlerArray, fieldName)
{
var defaultValue = -1; // undefinedUp (1000 for undefinedDown)
return tiddlerArray.sort(function(t1,t2){
var c1 = t1.getSCounter(fieldName), c2 = t2.getSCounter(fieldName);
c1 = (!c1 && c1 != 0) ? defaultValue : c1;
c2 = (!c2 && c2 != 0) ? defaultValue : c2;
return c1 - c2;
});
},
currentlyDragged: [],
setCurrentlyDragged: function(tidName,sortField,dropAction,itemMenuElement,
onKeyDown,onKeyUp)
{
this.currentlyDragged.push({
name: tidName,
field: sortField,
dropAction: dropAction,
itemMenuElement: itemMenuElement,
onKeyDown: onKeyDown,
onKeyUp: onKeyUp
});
if(itemMenuElement) {
jQuery(itemMenuElement).bind("keyup",onKeyUp);
jQuery(itemMenuElement).bind("keydown",onKeyDown);
}
},
clearCurrentlyDragged: function()
{
if(!this.currentlyDragged[0]) return;
var i, context, upHandler, downHandler;
for(i = 0; i < this.currentlyDragged.length; i++)
{
context = this.currentlyDragged[i];
upHandler = context.onKeyUp;
downHandler = context.onKeyDown;
if(!context.itemMenuElement) continue;
if(downHandler)
jQuery(context.itemMenuElement).unbind("keydown",downHandler);
if(upHandler)
jQuery(context.itemMenuElement).unbind("keyup",upHandler);
}
this.currentlyDragged = [];
jQuery(".buttonSortState").parent().parent().removeClass("selected");
//# check if /\ grandparent is <tr> and remove only in that case
jQuery(".buttonSortState").removeClass("buttonSortState");
},
getCurrentlyDragged: function() {
//# for now, works only with the first selection (if multiple)
return this.currentlyDragged[0] ? this.currentlyDragged[0].name : null;
},
getCurrentSourceListContext: function() {
//# for now, works only with the first selection (if multiple)
return this.currentlyDragged[0];
},
markSelected: function(itemMenu)
{
if(!itemMenu) return;
jQuery(itemMenu).addClass("buttonSortState").focus();
if(itemMenu.parentElement.tagName.toLowerCase() == "td")
jQuery(itemMenu).parent().parent().addClass("selected");
},
ensureFocusOnCurrentlyDragged: function()
{
var currentlyDragged = this.getCurrentSourceListContext();
console.log("currentlyDragged:");console.log(currentlyDragged);
if(!currentlyDragged) return;
var itemMenu = currentlyDragged.itemMenuElement;
// because of refreshing, itemMenu can be no longer attached to the ~root
// element (document? html?). If that's the case, find the new one
var newItemMenu, itemMenus = jQuery("."+this.itemMenuClass+
'[filter=\''+itemMenu.getAttribute("filter")+'\']')
.each(function(i,el){
if(el.tiddler == itemMenu.tiddler)
newItemMenu = el;
});
if(newItemMenu != itemMenu) {
// remember actual element, reattach onkeyup, onkeydown handlers
currentlyDragged.itemMenuElement = newItemMenu;
jQuery(newItemMenu).bind("keyup",currentlyDragged.onKeyUp);
jQuery(newItemMenu).bind("keydown",currentlyDragged.onKeyDown);
}
this.markSelected(newItemMenu);
},
actionStepsWithArguments: {},
actionStepsWithoutArguments: {},
applyActionStep: function(tiddler, actionStep, rootElement)
{
var actionRegExp = /(.\w+)\.\.(.*)/,
match = actionRegExp.exec(actionStep),
actionStepName;
if(match) { // action with an argument
for(actionStepName in this.actionStepsWithArguments)
if(actionStepName == match[1])
return this.actionStepsWithArguments[actionStepName](tiddler,match[2],rootElement);
//# may be throw an error/warning?
return;
} else // actionStep without arguments
for(actionStepName in this.actionStepsWithoutArguments)
if(actionStepName == actionStep)
return this.actionStepsWithoutArguments[actionStepName](tiddler,rootElement);
//# may be throw an error/warning?
return;
},
parseAndApplyAction: function(tiddler, actionLine, rootElement, noNotify)
{
if(!tiddler || !actionLine)
return;
var actionStepsArray = actionLine.split(",,");
for(var i = 0; i < actionStepsArray.length; i++)
this.applyActionStep(tiddler,jQuery.trim(actionStepsArray[i]), rootElement);
if(!noNotify)
store.notify(tiddler.title,true);
},
parseAndSeparateActions: function(actionsLine)
{
var actionsArray = actionsLine.split(";;");
if(actionsArray.length == 1 && !actionsArray[0].contains("::"))
return jQuery.trim(actionsArray[0]);
var actionMap = {},
name_and_action_RegExp = /(.+?)::(.*)/, match;
for(var i = 0; i < actionsArray.length; i++)
{
match = name_and_action_RegExp.exec(actionsArray[i]);
if(!match || !match[1] || !match[2])
//# may be throw an error/warning?
continue;
actionMap[jQuery.trim(match[1])] = jQuery.trim(match[2]);
actionMap["default"] = actionMap["default"] || jQuery.trim(match[2]);
}
return actionMap;
},
// moving helpers
checkCounters: function(filter,field)
{
var tids = store.filterTiddlers(filter);
tids = this.sortByCounter(tids, field);
for(var i = 0; i < tids.length; i++)
tids[i].setSCounter(field,i);
return tids;
},
moveToArbitraryPlace: function(filter,tiddler,field,index,doCycle)
{
var tids = this.checkCounters(filter,field), i,
tidIndex = tids.indexOf(tiddler),
tidsNum = tids.length;
// parse "top"/"bottom" values
index = (index == "top") ? 0 : (index == "bottom" ? tidsNum-1 : index);
if(doCycle) {
index = index % tidsNum;
index = (index < 0) ? (index + tidsNum) : index;
}
// do nothing in cases.. ("tidIndex < 0" = "tiddler is not in the list")
if(tidIndex == index || tidIndex < 0 || index < 0 || index >= tids.length)
return;
// move items
tiddler.setSCounter(field,index);
if(tidIndex > index)
for(i = index; i < tidIndex; i++)
tids[i].setSCounter(field,i+1);
else
for(i = index; i > tidIndex; i--)
tids[i].setSCounter(field,i-1);
// refresh the list (order)
store.notify(field,true);
// ~200 ms for notifying / in which browser?
},
moveToTop: function(filter,tiddler,field)
{ this.moveToArbitraryPlace(filter,tiddler,field,"top"); },
moveToBottom: function(filter,tiddler,field)
{ this.moveToArbitraryPlace(filter,tiddler,field,"bottom"); },
// syntax: <<itemMenu tiddlerName filter field:fieldName addAction:actionSyntax
// dropAction:actionSyntax switchActions:actionsSyntax>>
handler: function(place,macroName,params,wikifier,paramString,tiddler)
{
// parse params: tiddlerName, context filter; field, noConflicts
var pParams = paramString.parseParams("pP",null,true,false,true),
tName = pParams[0]["pP"][0], // name of the tid governed by itemMenu
tid = store.fetchTiddler(tName),
filter = pParams[0]["pP"][1], // tids among which the sorting is done
field = getParam(pParams,"field",this.defaultField), // field which holds the counter value
addAction = getParam(pParams,"addAction",""), //
dropAction = getParam(pParams,"dropAction",""), //
switchActions = getParam(pParams,"switchActions",""); //
var cmi = config.macros.itemMenu; // shortcut
var serapartedSwitchActions = this.parseAndSeparateActions(switchActions);
var checkCounters = function()
{ return cmi.checkCounters(filter,field); };
// selecting/rearranging helpers
var cancelSelection = function()
{
cmi.clearCurrentlyDragged();
Popup.remove(); // close the popup, including on esc
};
var getSelected = function()
{
// get currently "dragged" itemMenu element
var draggedContext = cmi.getCurrentSourceListContext();
return draggedContext ? draggedContext.itemMenuElement : null;
};
// jQuery of itemMenu elements with the same filter
var getJMenus = function()
{
return jQuery("."+cmi.itemMenuClass+"[filter='"+filter+"']");
};
var reselectByIndex = function(jMenus,index) // jMenus is "jQuery(menus)"
{
var menuToSelect = jMenus[index];
if(!menuToSelect) return;
cancelSelection();
// select (focus is needed for keyboard events to fire)
jQuery(menuToSelect).focus().click();
console.log("click generated in reselectByIndex");
//# may be optimize the two previous steps (do reselection directly)
};
var selectNext = function()
{
var itemMenu = getSelected();
if(!itemMenu) return;
// other item menus, found by the same filter
var menus = getJMenus(),
currentIndex = menus.index(itemMenu);
reselectByIndex(menus,currentIndex+1);
};
var selectPrev = function()
{
var itemMenu = getSelected();
if(!itemMenu) return;
// other item menus, found by the same filter
var menus = getJMenus(),
currentIndex = menus.index(itemMenu);
reselectByIndex(menus,currentIndex-1);
};
var selectNeighbour = function()
{
var itemMenu = getSelected();
if(!itemMenu) return;
// other item menus, found by the same filter
var menus = getJMenus(),
currentIndex = menus.index(itemMenu);
if(currentIndex > 0)
reselectByIndex(menus,currentIndex-1);
else
reselectByIndex(menus,currentIndex+1);
// returns true when successfully selected another item
return getSelected() != itemMenu;
};
var selectFirst = function()
{
// other item menus, found by the same filter
var menus = getJMenus();
reselectByIndex(menus,0);
};
var selectLast = function()
{
// other item menus, found by the same filter
var menus = getJMenus();
reselectByIndex(menus,menus.length-1);
};
var moveDown = function(doReselect)
{
var tids = checkCounters(), i = tids.indexOf(tid);
if(i < 0) return; // not among tids
if(i >= tids.length-1) // out of boundaries
return doReselect ? "" : cancelSelection();
// make the switch with the neighbour
tids[i].setSCounter(field,i+1);
tids[i+1].setSCounter(field,i);
// refresh the list (order)
store.notify(field,true);
//# try to avoid extra refreshing on long press down
if(doReselect) reselectByIndex(getJMenus(),i+1);
};
var moveUp = function(doReselect)
{
var tids = checkCounters(), i = tids.indexOf(tid);
if(i < 0) return; // not among tids
if(i <= 0) // out of boundaries
return doReselect ? "" : cancelSelection();
// make the switch with the neighbour
tids[i].setSCounter(field,i-1);
tids[i-1].setSCounter(field,i);
// refresh the list (order)
store.notify(field,true);
if(doReselect) reselectByIndex(getJMenus(),i-1);
};
var moveToTop = function(doReselect)
{
cmi.moveToTop(filter,tid,field);
if(doReselect) reselectByIndex(getJMenus(),0);
};
var moveToBottom = function(doReselect)
{
cmi.moveToBottom(filter,tid,field);
var menus = getJMenus();
if(doReselect) reselectByIndex(menus,menus.length-1);
};
var moveToArbitraryPlace = function(movingTidName, above)
{ // filter, targetTidName is got via enclosure
var tids = checkCounters(),
movingTid = store.fetchTiddler(movingTidName),
targetTid = store.fetchTiddler(tName),
movingInd = tids.indexOf(movingTid),
targetInd = tids.indexOf(targetTid),
index = targetInd + ((movingInd > targetInd) ? (above ? 0 : 1) : (above ? -1 : 0));
cmi.moveToArbitraryPlace(filter,movingTid,field, index);
};
var onClickItemMenu = wrapNoClose(function(e)
{
if(!cmi.getCurrentlyDragged()) // ~drag
{
clearMessage(); // useful on smartphones
// open the popup menu:
var manageButton = this;
var popup = Popup.create(this);
jQuery(popup).addClass("itemMenu");
// then, add buttons there:
// the "cancel selection" button
createTiddlyButton(popup,"","cancel selection",
cancelSelection,"cancelSelectionButton button");
// switch action(s) button
var createActionButtonInPopup = function(place,actionName)
{
var li = createTiddlyElement(place,"li"),
action = serapartedSwitchActions[actionName];
createTiddlyButton(li,actionName,"",function(){
cmi.parseAndApplyAction(tid,action,this);
cancelSelection();
},"listedDoActionButton button");
};
var bringActions = wrapNoClose(function() {
if(serapartedSwitchActions["default"])
{
// named actions
var dPopup = Popup.create(popup), li, actName;
for(actName in serapartedSwitchActions) {
if(actName == "default")
continue;
createActionButtonInPopup(dPopup, actName);
}
Popup.show("bottom","left");
} else { // single unnamed action is defined
cmi.parseAndApplyAction(tid,switchActions, this);
cancelSelection();
}
cmi.clearCurrentlyDragged();
// no class removing since refreshing is applied
});
if(serapartedSwitchActions && tid)
createTiddlyButton(popup,"","drop this item",
bringActions,"doActionButton button");
// tag toggler
var macroText = "<<tagToggler [["+tName+"]] \"\">>";
wikify(macroText, popup);
var tagButton = popup.lastChild,
startTagToggling = function() {
jQuery(tagButton).click();
console.log("click generated in startTagToggling");
};
// the "info" button
var showTiddlerInfo = wrapNoClose(function(){
var infoPopup = Popup.create(popup);
createTiddlyText(infoPopup,"references:");
config.commands.references.handlePopup(infoPopup, tid.title);
Popup.show("bottom","left");
return false;
});
createTiddlyButton(popup,"","tiddler info", showTiddlerInfo,"tiddlerInfoButton button");
// the "rename" button
var startRenaming = wrapNoClose(function(){
var renamePopup = Popup.create(popup),
li = createTiddlyElement(renamePopup,"li"),
initTitle = tid.title,
nameField = createTiddlyElement(li,"textarea",null,"nameInput"),
changeName = function(doSave,goOnSelected)
{
var newTitle = jQuery.trim(nameField.value);
store.saveModifiedTiddler(initTitle,newTitle);
story.refreshTiddler(tid.title,null,true);
if(doSave) autoSaveChanges();
if(!goOnSelected)
cancelSelection();
else
cmi.ensureFocusOnCurrentlyDragged();
};
nameField.value = tid.title;
createTiddlyButton(li,"rename","rename \""+tid.title
+"\"",changeName,"button renameButton");
nameField.onclick = wrapNoClose(); // prevent popup closing
// press enter to apply or esc to exit
// (shift and ctrl are also taken into account)
nameField.onkeydown = function(ev)
{
var e = ev || window.event;
if(e.which == $enter) {
changeName(e.ctrlKey,e.shiftKey);
Popup.remove(1); // renamePopup only
window.preventOtherHandling(e);
}
if(e.key === 'Escape') {
if(e.shiftKey) {
cmi.ensureFocusOnCurrentlyDragged();
Popup.remove(1);
window.preventOtherHandling(e);
} else
cancelSelection();
}
};
Popup.show("bottom","left");
nameField.focus(); // put the cursor inside the name edit area
});
createTiddlyButton(popup,"","rename this tiddler", startRenaming,"renameTiddlerButton button");
// the "edit text" button
var startEditing = wrapNoClose(function()
{
var textEditPopup = Popup.create(popup),
li = createTiddlyElement(textEditPopup,"li"),
textField = createTiddlyElement(li,"textarea",null,"ttextInput"),
changeText = function(e)
// e is either click or keydown (enter) event
{
var goOnSelected = e.shiftKey;
tid.set(null,textField.value);
store.saveModifiedTiddler(tid);
story.refreshTiddler(tid.title,null,true);
autoSaveChanges();
if(goOnSelected)
cmi.ensureFocusOnCurrentlyDragged();
else
cancelSelection();
};
textField.value = tid.text;
// change the width of the popup and hence of {{{textField}}}
textEditPopup.style.width = "100%";
createTiddlyButton(li,"save","save the text of \""+
tid.title+"\"",changeText,
"button saveTextButton");
textField.onclick = wrapNoClose(); // prevent popup closing
// press enter to apply or esc to exit
// (shift and ctrl are also taken into account)
textField.onkeydown = function(ev)
{
var e = ev || window.event;
if(e.ctrlKey && e.which == $enter) {
changeText(e);
Popup.remove(1); //textEditPopup only
window.preventOtherHandling(e);
}
if(e.key === 'Escape') {
if(e.shiftKey) {
cmi.ensureFocusOnCurrentlyDragged();
Popup.remove(1);
window.preventOtherHandling(e);
} else {
Popup.remove();
cancelSelection();
}
}
};
textField.onkeypress = function(e)
{
adjustHeightToContent.apply(this);
// avoid firing of story.onTiddlerKeyPress
if(e.which == $enter) {
if(e.stopPropagation)
e.stopPropagation();
e.cancelBubble = true;
}
// don't return false to insert linebreaks
// on enter (when typing)
}
Popup.show("bottom", "left");
adjustHeightToContent.apply(textField);
textField.focus(); // put the cursor inside the text edit area
});
createTiddlyButton(popup, "", "edit the tiddler text",
startEditing, "editTiddlerTextButton button");
// the "open" button
var i = getTiddlyLinkInfo(tName,"openTiddlerButton button"),
openTiddler = function(e) {
onClickTiddlerLink(e);
cancelSelection();
},
openButton = createTiddlyButton(popup,"","open "+tName,
openTiddler, i.classes, null, null, {
refresh: "link", tiddlyLink: tName
}), // see createTiddlyLink source
clickOpenTiddler = function() {
jQuery(openButton).click();
};
// "add" button
var cma = config.macros.addTiddler;
if(cma && addAction) {
var btn = createTiddlyButton(popup, "",
"add tiddler here", cma.onClick,
"addTiddlerButton button"),
currentIndex = getJMenus().index(this);
btn.params = { title:"", text:"", commonTags:[],
addAction: addAction,
orderCounter: field,
orderFilter: filter,
orderMode: currentIndex + 1
}
var addTiddler = function() {
jQuery(btn).click()
};
}
// the "delete" button
var confirmDeleteMsg = config.commands.deleteTiddler.warning,
deleteTiddler = function(e, goOnSelected)
{
if(e) // on click
goOnSelected = goOnSelected || e.shiftKey;
if(confirm(confirmDeleteMsg.format([tName]))) {
if(goOnSelected)
goOnSelected = selectNeighbour();
// if no neighbour, nothing to reselect
store.removeTiddler(tName);
autoSaveChanges();
}
if(goOnSelected)
cmi.ensureFocusOnCurrentlyDragged();
else
cancelSelection();
};
createTiddlyButton(popup, "", "delete this tiddler",
deleteTiddler, "deleteTiddlerButton button");
// support keyboard
// keypress ignores "delete", arrows etc, hence keydown
var onKeyDownItemMenu = function(e) {
switch(e.key) {
case 'Escape':
// onkeydown here, because onkeyup ~can't be
// prevented from ~propagation in subpopups
if(!e.shiftKey) cancelSelection();
// for the info and tag popups
else Popup.remove(1);
break;
case 'ArrowDown':
if(e.ctrlKey) moveDown(true);
else selectNext();
break;
case 'ArrowUp':
if(e.ctrlKey) moveUp(true);
else selectPrev();
break;
case 'End':
if(e.ctrlKey) moveToBottom(true);
else selectLast();
break;
case 'Home':
if(e.ctrlKey) moveToTop(true);
else selectFirst();
break;
case 'Enter': clickOpenTiddler(); break;
}
// allow browser tab switching, prevent other default actions
if(e.key !== 'Tab' || !e.ctrlKey) return false;
};
var onKeyUpItemMenu = function(e) {
switch(e.originalEvent.code) {
case 'KeyI': showTiddlerInfo(); break;
case 'KeyR': startRenaming(); break;
case 'KeyE': startEditing(); break;
case 'KeyT': startTagToggling();break;
case 'KeyD':
if(serapartedSwitchActions && tid)
bringActions();
break;
// tries to reselect if e.shiftKey
case 'KeyX':
deleteTiddler(null, e.shiftKey);
break;
}
switch(e.key) {
case '+':
case '=':
if(cma && addAction) addTiddler(e);
break;
// tries to reselect if e.shiftKey
case 'Delete':
deleteTiddler(null, e.shiftKey);
break;
//# add keyboard support for actions D[A?]
}
return false;
};
cmi.setCurrentlyDragged(tName, field, dropAction, this, onKeyDownItemMenu, onKeyUpItemMenu);
cmi.markSelected(this);
/*# create array of { handler: .. , label: .. , activationKeys: .. }
and try to make button creation, keyboard support "for each ..", extendable set of .. */
Popup.show("top","right");
jQuery(popup).addClass("hide");
} else { // ~drop
// if taken from another list, the item must be changed by the drop+add actions
var dragListContext = cmi.getCurrentSourceListContext(),
dropActionNow = dragListContext.dropAction,
/* not 100% accurate */ sameList = (dropActionNow == dropAction) && (dragListContext.field == field),
dropFromOtherList = !sameList && (dropActionNow || addAction),
dropAndAddAction = (dropActionNow && addAction) ?
(dropActionNow+",,"+addAction)
: (dropActionNow || addAction);
if(dropFromOtherList)
{
var draggedTid = store.fetchTiddler(cmi.getCurrentlyDragged());
cmi.parseAndApplyAction(draggedTid,dropAndAddAction);
draggedTid.deleteSCounter(dragListContext.field);
}
// move the item
Popup.remove(); // close the popup
if(cmi.getCurrentlyDragged() == tName) {
jQuery(this).removeClass("buttonSortState");
moveDown(); // on drop-on-self
} else
moveToArbitraryPlace(cmi.getCurrentlyDragged(),true);
// refreshing removes the "buttonSortState" class
cmi.clearCurrentlyDragged();
}
});
// create the 2 sets of classes for the button and for the table row
// (if itemMenu is inside a td element) based on the tiddler's tags
var itemMenuClasses = this.itemMenuClass,
rowClasses = "",
badSymbolsRE = /[\.,;:`!@#\$%\^&\*\(\)\+=\[\]\{\}\|\\/'"~ ]/g;
for(var i = 0 ; tid && i < tid.tags.length ; i++) {
// process each tag: substitute spaces and symbols .,;:!'"()[]{}=+\|/*&^%$#@`~ with "_"
itemMenuClasses += " marker_"+ tid.tags[i].replace(badSymbolsRE,"_");
rowClasses += "line_"+ tid.tags[i].replace(badSymbolsRE,"_")+" ";
}
// create the button(s) // text is set via CSS
var btn = createTiddlyButton(place,"","",onClickItemMenu,itemMenuClasses);
btn.setAttribute("filter",filter);
btn.tiddler = tid;
// in table, add the line_className classes based on the tiddler's tags
if(place.tagName.toLowerCase() == "td")
jQuery(place).parent().addClass(rowClasses);
}
}
// define the actions
// add tag
config.macros.itemMenu.actionStepsWithArguments["+tag"] = function(tiddler,tag)
{
if(tiddler.isTagged(tag)) return;
tiddler.tags.push(tag);
tiddler.modifier = config.options.txtUserName;
tiddler.modified = new Date();
store.setDirty(true);
};
// remove tag
config.macros.itemMenu.actionStepsWithArguments["-tag"] = function(tiddler,tag)
{
if(!tiddler.isTagged(tag)) return;
tiddler.tags.splice(tiddler.tags.indexOf(tag),1);
tiddler.modifier = config.options.txtUserName;
tiddler.modified = new Date();
store.setDirty(true);
};
// toggle tag
config.macros.itemMenu.actionStepsWithArguments["!tag"] = function(tiddler,tag)
{
if(tiddler.isTagged(tag))
tiddler.tags.splice(tiddler.tags.indexOf(tag),1);
else
tiddler.tags.push(tag);
tiddler.modifier = config.options.txtUserName;
tiddler.modified = new Date();
store.setDirty(true);
};
// set field
config.macros.itemMenu.actionStepsWithArguments["setField"] = function(tiddler,argument)
{
var argRE = /(\w+)\.\.(.*)/, argMatch = argRE.exec(argument);
if(!argMatch) return;
var field = argMatch[1], value = argMatch[2];
value = value || null; // to remove the field if empty
store.setValue(tiddler,field,value,true);
};
// delete field
config.macros.itemMenu.actionStepsWithArguments["deleteField"] = function(tiddler,argument)
{
store.setValue(tiddler,argument,null,true);
};
//config.macros.itemMenu.actionStepsWithArguments["<name>"] = function(tiddler,arg) {
//};
// save changes
config.macros.itemMenu.actionStepsWithoutArguments["save"] = function() {
saveChanges();
};
config.macros.itemMenu.actionStepsWithoutArguments["delete"] = function(tiddler) {
var confirmDeleteMsg = config.commands.deleteTiddler.warning;
if(confirm(confirmDeleteMsg.format([tiddler.name])))
{
store.removeTiddler(tiddler.title);
autoSaveChanges();
}
};
//config.macros.itemMenu.actionStepsWithoutArguments["<name>"] = function(tiddler) {
//};
// implement the styling
var iMenuClass = config.macros.itemMenu.itemMenuClass;
config.shadowTiddlers["StyleSheetManualSorter"] = "/*{{{*/\n"+
".editTTextItem { width: 100% }\n"+
".ttextInput, .nameInput, .newTitleInput, .newTagInput {\n"+
" padding: 0.2em 0.4em; \n"+
" border: none;\n"+
" margin: 0.4em 0.4em 0;\n"+
" box-shadow: 1px 1px 3px #999 inset;\n"+
"}\n"+
".ttextInput, .nameInput {\n"+
" box-sizing: border-box;\n"+
" width: calc(100% - 0.8em);\n"+
"}\n\n"+
"."+iMenuClass+" { color: inherit; font-weight: bold; }\n"+
"."+iMenuClass+":hover { background-color: inherit; color: green; }\n"+ // use ColorPalette?
"."+iMenuClass+":before { content: \"⊙\"; }\n"+
".cancelSelectionButton:before { content: \"⊗\"; }\n"+
".doActionButton:before { content: \"D\"; }\n"+
".tiddlerInfoButton:before { content: \"I\"; }\n"+
".renameTiddlerButton:before { content: \"R\"; }\n"+
".editTiddlerTextButton:before { content: \"E\"; }\n"+
".openTiddlerButton:before { content: \"O\"; font-weight: normal; }\n"+
".addTiddlerButton:before { content: \"+\"; }\n"+
".deleteTiddlerButton:before { content: \"X\"; }\n"+
".itemMenu .tagTogglerButton:before { content: \"T\"; }\n"+
".itemMenu .button { padding-left: 0.25em; padding-right: 0.25em; }\n"+
".buttonSortState:hover,\n"+
".buttonSortState { background-color: inherit; color: #00B000; }\n"+
// for "lists" with buttons as "markers" (using tables)
".tableList,\n"+
".tableList > tbody,\n"+
".tableList > tbody > tr,\n"+
".tableList > tbody > tr > td,\n"+
".tableList > tbody > tr > td > .button { border: none !important; }\n"+
".tableList td { vertical-align: top; padding: 0px 3px; }\n"+
".tableList ul,\n"+
".tableList ol,\n"+
"li .tableList,\n"+
".tableList .tableList { margin: 0; }\n"+
".tableList > tbody > .selected { background-color: rgba(0,0,255,0.15); }\n\n"+
".darkMode .tableList > tbody > .selected { background-color: rgba(0,0,255,0.4); }\n\n"+
// full width of table with minimal width of the first column
".tableList { width: calc(100% - 2em); }\n"+ // 2em of margins on both sides
".tableList > tbody > tr > td:first-child { width: 1px; }\n"+
/* is some cases
* .tableList td { width: 1px; }
* .tableList td:last-child { width: 100%; }
* would be more suitable */
"/*}}}*/";
store.addNotification("StyleSheetManualSorter", refreshStyles);
//}}}
/***
!!!Tagging helpers and tagToggler macro
***/
//{{{
config.filters.existingTags = function(results,match)
{
var allTags = store.getTags();
for(var i = 0; i < allTags.length; i++)
allTags[i] = allTags[i][0];
if(match[3] == "-") {
for(i = 0; i < results.length; i++)
if(!allTags.contains(results[i].title))
results.splice(i--,1);
return results;
}
for(var i = 0; i < allTags.length; i++)
allTags[i] = store.fetchTiddler(allTags[i]) || new Tiddler(allTags[i]);
// default action: add all the tags
for(i = 0; i < allTags.length; i++)
results.pushUnique(allTags[i]);
return results;
};
config.filters.tagging = function(results,match)
{
var prefix = match[3].substr(0,1), title = match[3].substr(1), tid = store.fetchTiddler(title),
tagging = [], notTagging = [];
if(!tid)
return [];
//# this behaviour may be changed after some testing
for(var i = 0; i < results.length; i++)
if(tid.tags.contains(results[i].title))
tagging.push(results[i]);
else
notTagging.push(results[i]);
switch(prefix) {
case ">":
return tagging.concat(notTagging)
case "<":
return notTagging.concat(tagging)
case "+":
return tagging
case "-":
return notTagging
}
displayMessage("Warning: the \"tagging\" filter must be used with one of the prefixes +, -, > or <, which is not the case.");
// use cookie to decide whether to suppress the message?
return results;
};
config.macros.tagToggler = {};
config.macros.tagToggler.handler = function(place,macroName,params,wikifier,paramString,tiddler)
{
// parse params
var pParams = paramString.parseParams("pParams",null,true,false,true),
tid = store.fetchTiddler(pParams[0]["pParams"][0]) || tiddler; // tid. which tags will be toggled
if(!tid) return;
var label = pParams[0]["pParams"][1];
if(label == "." || label == undefined)
label = "toggle tags";
var tooltip = getParam(pParams, "tooltip", "toggle tags of " + tid.title),
doAutoSave = params.contains('doAutoSave') || false,
tagsSet = getParam(pParams, "tags", "[existingTags[+]] [tagging[>"+tid.title+"]]");
// for compability with SetManagerPlugin
var cmi = config.macros.itemMenu,
clearSelected = function()
{
if(cmi) cmi.clearCurrentlyDragged();
},
returnItemMenuSelection = function()
{
if(cmi) cmi.ensureFocusOnCurrentlyDragged();
};
var whereToScroll;
var toggleTag = function(tid,tag,refreshTagListIfNotClosing)
{
if(tid.isTagged(tag))
tid.tags.splice(tid.tags.indexOf(tag), 1);
else
tid.tags.push(tag);
if(refreshTagListIfNotClosing) return refreshTagListIfNotClosing();
store.saveModifiedTiddler(tid);
story.refreshTiddler(tid.title, null, true);
if(doAutoSave)
autoSaveChanges();
if(whereToScroll)
window.scrollTo(0,ensureVisible(whereToScroll));
clearSelected();
};
// define the onclick handlers
var onTagClick = wrapNoClose(function(e)
{
var tag = this.getAttribute("tag"),
shiftWasHold = (e || window.event).shiftKey;
toggleTag(tid,tag,shiftWasHold ? this.refreshTagList : null);
return false;
});
var onClick = wrapNoClose(function()
{
// form the list of the tags to choose from, all tags to start somewhere
var availableTags = [],
allTags = store.getTags(),
noTagsMsg = "No tags found by the provided criterion";
for(var i = 0; i < allTags.length; i++)
availableTags.push(allTags[i][0]);
// build the list of tags and labels
var tagsToWorkWith = store.filterTiddlers(tagsSet);
for(i = 0; i < tagsToWorkWith.length; i++)
tagsToWorkWith[i] = tagsToWorkWith[i].title;
var menuTags = [], menuLabels = [], menuItems = [], t;
for(i = 0; i < tagsToWorkWith.length; i++) {
t = tagsToWorkWith[i];
menuItems.push({
tag: t,
label: t // '[x] ' or '[ ] ' addition is now defined via css
})
}
// create the popup menu
var popup = Popup.create(this), li, tagButton;
// arbitrary tag toggler
li = createTiddlyElement(popup, "li");
var newTagField = createTiddlyElement(li, "input", null, "newTagInput", { type: "text" });
newTagField.onclick = wrapNoClose(); // prevent the popup from closing on click here
var selectedTagIndex = 0, // 0 means "not selected"
selectNextTag = function()
{
var nextListItem = popup.childNodes[selectedTagIndex+1];
if(!nextListItem) return // out of boundaries, won't move
jQuery(popup).children().children().removeClass("selectedTag");
selectedTagIndex++;
nextListItem.childNodes[0].classList.add("selectedTag");
},
selectTag15ahead = function()
{
var numberOfItems = jQuery(popup).children().length; // tags+1
selectedTagIndex = Math.min(selectedTagIndex + 15, numberOfItems - 1);
var newListItem = popup.childNodes[selectedTagIndex];
jQuery(popup).children().children().removeClass("selectedTag");
newListItem.childNodes[0].classList.add("selectedTag");
},
selectPrevTag = function()
{
var prevListItem = popup.childNodes[selectedTagIndex - 1];
if(selectedTagIndex == 0) return;
jQuery(popup).children().children().removeClass("selectedTag");
selectedTagIndex--;
if(selectedTagIndex == 0) return; // don't color the main field
//# or do so but color it in the beginning as well
prevListItem.childNodes[0].classList.add("selectedTag");
},
selectTag15back = function()
{
var numberOfItems = jQuery(popup).children().length; // tags+1
selectedTagIndex = Math.max(selectedTagIndex - 15, 0);
var newListItem = popup.childNodes[selectedTagIndex];
jQuery(popup).children().children().removeClass("selectedTag");
if(selectedTagIndex > 0)
newListItem.childNodes[0].classList.add("selectedTag");
},
selectTag = function(tag)
{
var button = jQuery(popup).children().children("*[tag='" + tag + "']");
if(!button.length) return;
button.addClass("selectedTag");
// get index, set selectedTagIndex
selectedTagIndex = jQuery(popup).children().children().index(button)
- 1;
},
getSelectedTagButton = function() {
if(selectedTagIndex == 0) // fosuced on new tag field
return null;
return popup.childNodes[selectedTagIndex].childNodes[0];
},
toggleSelected = function(goOnToggling)
{
var button = getSelectedTagButton();
if(!button) return;
tag = button.getAttribute("tag");
fieldValue = newTagField.value;
toggleTag(tid,tag,goOnToggling ? refreshTagList : null);
newTagField.value = fieldValue;
newTagField.select();
selectTag(tag);
return tag;
},
toggleNew = function(goOnToggling) {
var tag = jQuery.trim(newTagField.value), isNewTag = true, i;
// add the tag to the list if it's totally new:
for(i = 0; i < menuItems.length; i++)
if(menuItems[i].tag == tag)
isNewTag = false;
if(isNewTag)
menuItems.push({ tag: tag, label: tag });
toggleTag(tid, tag, goOnToggling ? refreshTagList : null);
};
// push the button to apply/click elsewhere to cancel..
createTiddlyButton(li, "toggle", "toggle the entered tag in the tiddler",
toggleNew, "button tagAdderButton");
// ..or use keyboard (see below)
// tags from the set
var refreshTagList = function()
{
//console.log("caller is " + arguments.callee.caller.toString());
// clear the list (but don't remove the first item)
while(popup.childNodes[1])
popup.removeChild(popup.childNodes[1]);
newTagField.focus();
selectedTagIndex = 0;
// refill the list
if(menuItems.length == 0) {
createTiddlyText(createTiddlyElement(popup, "li"), noTagsMsg);
return;
}
var fieldValueLowered = newTagField.value.toLocaleLowerCase(),
sortedMenuItems = menuItems.concat([]).sort(function(a, b){
if(!fieldValueLowered) return 0;
// store where the value from the field starts in the tag
a.index = a.tag.toLocaleLowerCase().search(fieldValueLowered);
b.index = b.tag.toLocaleLowerCase().search(fieldValueLowered);
// if(tid.tags.contains(a.tag) && a.index != -1) a.index = -2;
// if(tid.tags.contains(b.tag) && b.index != -1) b.index = -2;
return a.index > b.index;
}),
item;
for(i = 0; i < sortedMenuItems.length; i++)
{
item = fieldValueLowered ? sortedMenuItems[i] : menuItems[i];
if(fieldValueLowered && item.index == -1) continue;
li = createTiddlyElement(popup, "li");
tagButton = createTiddlyButton(li, item.label,
"toggle '" + item.tag+"'", onTagClick, "button tag"+
(tid.tags.contains(item.tag)? "" : "Not") + "Present");
tagButton.setAttribute("tag", item.tag);
tagButton.refreshTagList = refreshTagList;
}
};
refreshTagList();
// show the popup menu
Popup.show("bottom","left");
// support keyboard navigation
jQuery(newTagField).bind('input',function(e)
{
refreshTagList();
});
jQuery(newTagField).bind('keydown', function(e)
{
goOnToggling = e.shiftKey;
if(e.which == $enter) {
toggleSelected(goOnToggling) || toggleNew(goOnToggling);
if(!goOnToggling) Popup.remove();
//document.getElementById("displayArea").focus()
return;
}
if(e.key === 'Escape') {
if(whereToScroll)
window.scrollTo(0, ensureVisible(whereToScroll));
if(e.shiftKey) {
returnItemMenuSelection();
Popup.remove(1);
//# make sure doesn't get propagated?
} else {
clearSelected();
Popup.remove();
}
//document.getElementById("displayArea").focus()
}
});
jQuery(newTagField).bind('keydown', function(e)
{
if(e.which == $down && !e.ctrlKey)
selectNextTag();
if(e.which == $up && !e.ctrlKey)
selectPrevTag();
if(e.which == $pgDn || (e.which == $down && e.ctrlKey))
selectTag15ahead();
if(e.which == $pgUp || (e.which == $up && e.ctrlKey))
selectTag15back();
if(selectedTagIndex)
window.scrollTo(0, ensureVisible(getSelectedTagButton()));
else
window.scrollTo(0, ensureVisible(newTagField));
});
});
// create the button
whereToScroll = createTiddlyButton(place, label, tooltip, onClick, "button tagTogglerButton");
}
// set styling
config.shadowTiddlers["TagAdderStyleSheet"] = "/*{{{*/\n"+
".tagPresent:before { content: \"[x] \"; }\n"+
".tagNotPresent:before { content: \"[ ] \"; }\n"+
".newTagInput { float: left; margin-right: 0.5em; }\n"+
".tagAdderButton { text-align: center; }\n"+
".selectedTag { color: blue !important; }\n"+
"/*}}}*/";
store.addNotification("TagAdderStyleSheet", refreshStyles);
//}}}
/***
!!!Hijack forEachTiddler macro to enable the new params
***/
//{{{
// helper filter for hiding hidden tiddlers
config.filters.hideFromFet = function(results,match)
{
var contextName = match[3], noContext = contextName == "-";
for(var i = 0; i < results.length; i++)
if(results[i].fields["hideInFet".toLowerCase()] &&
(results[i].fields["hideInFet".toLowerCase()] && noContext ||
results[i].fields["hideInFet".toLowerCase()]
.split(" ").contains(contextName)))
results.splice(i--,1);
return results;
};
// hijack config.macros.forEachTiddler.parseParams so that it handles
// "set", "sortable"/"sortableBy", "addAction", "dropAction", "switchAction" params
// the "params" array is not changed as its usages in parseParams don't need it,
// same story for preParsedParams[i] (i > 0)
if(config.macros.forEachTiddler && !config.macros.forEachTiddler.hijacked_sortable)
{
//# check if the proper version of FET (1.3.0 or above) is used
config.macros.forEachTiddler.hijacked_sortable = true;
config.macros.forEachTiddler.oldFashionParams =
config.macros.forEachTiddler.oldFashionParams.concat([
"sortableBy", "addAction", "dropAction", "switchActions", "writeToList"]);
config.extensions.ManualSortMacroPlugin = {
orig_fet_parseParams: config.macros.forEachTiddler.parseParams
};
var origParse = config.extensions.ManualSortMacroPlugin.orig_fet_parseParams;
config.macros.forEachTiddler.parseParams = function(preParsedParams,params)
{
// parse the "set" param
var setDescription = getParam(preParsedParams,"set",""), filter, sortField,
setAddAction, setDropAction, cmd = config.macros.defineSet;
if(setDescription) {
if(!setDescription.contains("[")) {
filter = "[set["+ setDescription +"]]"; // named set
sortField = cmd.getSortFieldForNamedSet(setDescription);
setAddAction = cmd.getAddToNamedSetAction(setDescription);
setDropAction= cmd.getDropFromNamedSetAction(setDescription);
} else {
filter = "set: "+setDescription; // inline set
var setDefinition = cmd.parseSetDefinition(setDescription);
// don't overwrite setDefinition as it is passed to adder
sortField = setDefinition.sortField;
setAddAction = cmd.getAddToSetAction(setDefinition);
setDropAction = cmd.getDropFromSetAction(setDefinition);
}
sortField = sortField || config.macros.itemMenu.defaultField;
}
// remember the filter (calc from both "set" and "filter" params)
var filterParam = getParam(preParsedParams,"filter","") +
(setDescription ? " [hideFromFet[-]]" : "");
if(filter && filterParam) {
if(filter.indexOf("set: ") == 0)
filter = filter + " modify: " + filterParam;
else
filter = filter + " " + filterParam;
} else if(filterParam)
filter = filterParam;
if(!filter)
return origParse.apply(this,arguments);
// the "in", "where" params stay untouched; change the filter param
preParsedParams[0]["filter"] = [filter];
// hijack the "script" param (define the "editable" and "adder" helpers)
// for now, deprecated "insert" helper is supported (same as "editable")
var usedScript = getParam(preParsedParams,"script",""),
insertDefinition = "var editable = "+
"function(container,params,defaultText,preprocessScript) {"+
"container = container || 't';"+
"params = params || '';"+
"if(defaultText)"+
" params = 'showIfNoValue:\\''+defaultText+'\\' '+params;"+
"if(preprocessScript)"+
" params = 'preprocess:\\''+preprocessScript+'\\''+params;"+
"return '<<insertEditable tiddler:['+'['+tiddler.title+']] container:\"'+container+'\" '+(params||'')+'>>';"+
"}, insert = editable;";
adderDefinitionBegin = 'var adder = '+
'function(args,label,orderMode,title) {'+
'return "<<addTiddler"'+
' +(" label:\'"+(label || "+")+"\'")'+
' '+(setDescription ? ('+" set:\''+setDescription+'\'"') : ''),
adderDefinitionEnd =
' +(" title:\'"+(title || "")+"\'")'+ // empty by default
' +" "+(args||"")'+
' +">>"'+
'};',
adderDefinition = adderDefinitionBegin + adderDefinitionEnd,
// unless sortableBy is defined, orderMode is ignored (see below)
fullScript = insertDefinition + adderDefinition + usedScript;
preParsedParams[0]["script"] = [fullScript];
// process and apply sortable/sortableBy params/sortField from set definition
var sortableParamIndex = params.indexOf("sortable"),
justSortable = sortableParamIndex != -1,
sortableBy = getParam(preParsedParams,"sortableBy");
if(params.contains("sortableBy") && !sortableBy // empty = default
|| justSortable)
sortableBy = config.macros.itemMenu.defaultField;
// support the deprecated {{{sortableBy '"orderCountName"'}}} syntax
var fieldWithQuotsMatch = /^"(.+)"$/.exec(sortableBy);
sortableBy = fieldWithQuotsMatch ? fieldWithQuotsMatch[1] : sortableBy;
// sortField can be defined directly or from the set (see above)
sortField = sortableBy || sortField;
//# rethink from here: either move this stuff below actions parsing etc (more meaninglful)
// or add "&& !setDescription" (this is to enable actions and other stuff for sets,
// even if sortField is not defined)
//+ from here
if(!justSortable && !sortField)
return origParse.apply(this,arguments);
// support the "extended scope for sorting"
//# is it extended or narrowed?
var fieldAndFilterMatch = /^(\w+) (\[.+)$/.exec(sortField),
sortFilter = fieldAndFilterMatch ? fieldAndFilterMatch[2] : filter;
sortField = fieldAndFilterMatch ? fieldAndFilterMatch[1] : sortField;
// set the "sortBy" param
var undefinedUp = true;
var sortScript = "(function(){ var c = tiddler.getSCounter(\"" + sortField +
"\"); return (c != 0 && !c)?" + (undefinedUp ? "-1" : "1000") + ": c; })()";
// lists of 999+ tiddlers long are not supposed to be used with manual sorting
preParsedParams[0]["sortBy"] = [sortScript];
// the sortable/sortableBy part is left in preParsedParams[0] as is
// extend the "adder" helper in the "script" param using specified sortField
adderDefinition = adderDefinitionBegin +
' +(orderMode ? (" order:\''+sortField+','
+filter.replace(/"/g,'\\"')
//# do smth about "'"s in filter (macro param parsing)
+',"+orderMode+"\'") : "")'+
adderDefinitionEnd;
fullScript = insertDefinition + adderDefinition + usedScript;
preParsedParams[0]["script"] = [fullScript];
//= up to here
// for actions other than "write" (and "writeToList" ~action), do no more
for(var knownActionName in config.macros.forEachTiddler.actions)
if(knownActionName != "write" && params.contains(knownActionName))
return origParse.apply(this,arguments);
// in original FETP, that's "addToList" action only
// parse the [SMP] actions-defining params
var addAction = getParam(preParsedParams,"addAction",setAddAction),
dropAction = getParam(preParsedParams,"dropAction",setDropAction),
switchActions = preParsedParams ? (
preParsedParams[0]["switchActions"] ?
preParsedParams[0]["switchActions"].join(";;")
: ""
) : "";
// allow multiple switchActions params (but each must have a name..)
var commonText = "description is expected behind";
if(!addAction && !(addAction == "") && params.contains("addAction"))
return { errorText: "An action "+commonText+" 'addAction'." };
if(!dropAction && !(dropAction == "") && params.contains("dropAction"))
return { errorText: "An action "+commonText+" 'dropAction'." };
if(!switchActions && !(switchActions=="")&& params.contains("switchActions"))
return { errorText: "An action(s) "+commonText+" 'switchActions'."};
// parse [FET] action
var action = "writeToList"; // default pseudo-action
// when action is not specified it is considered as writeToList with..
var defaultText = '"["+"["+tiddler.title+"]]"';
//# unknown actions are considered as the default one.. which is bad for other extensions
// when "writeToList" is used, in fact it preparses the argument for "write"
writeToListText = getParam(preParsedParams,"writeToList",defaultText);
var writeText = '"| "+itemMenu()+" |"+(' +writeToListText+ ')+"|\\n"';
// when "write" is used, its argument is used after only "minimal"preparsing
if(preParsedParams[0]["write"])
action = "write";
writeText = getParam(preParsedParams,"write",writeText);
// substitute all the "itemMenu()" expressions in the argument of "write"
// with their intended "meaning" (hence use non-greedy regexp)
var itemMenuRegExp = /(.*?)itemMenu\(\)/g;
var insertItemMenu = function($0,$1) {
var escapedFilter = sortFilter.contains('"') ?
('\\\''+sortFilter.replace(/"/g,'\\\"')+'\\\'') :
("\\\""+sortFilter.replace(/'/g,"\\\'")+"\\\"") ;
// this is a semi-fix: tags with both ' and " will cause troubles with manual sorting..
// escape the actions as well
return $1 + "\"<<itemMenu [[\"+tiddler.title+\"]] "+ escapedFilter +
(sortField ? (" field:\\\""+ sortField +"\\\"") : "")+
(addAction ? " addAction:\\\""+addAction+"\\\"" : "")+
(dropAction ? " dropAction:\\\""+dropAction+"\\\"" : "")+
(switchActions ? " switchActions:\\\""+switchActions+"\\\"" : "")+">>\"";
}
writeText = writeText.replace(itemMenuRegExp,insertItemMenu);
// change preParsedParams accordingly (use writeText, "write" action)
preParsedParams[0]["write"] = [writeText];
// change the begin argument (leave end, none, toFile parts unchanged)
if(action == "writeToList")
preParsedParams[0]["begin"] = [(preParsedParams[0]["begin"] ?
preParsedParams[0]["begin"][0]:'""') + '+"|tableList|k\\n"'];
// call the parser with the new arguments
return origParse.apply(this,arguments);
}
}
//}}}
/***
!!!addTiddler macro
***/
//{{{
config.macros.addTiddler = {
handler: function(place, macroName, params, wikifier, paramString) {
if(readOnly)
return;
// param parsing (partially taken from the newTiddler macro)
params = paramString.parseParams("title", null, true, false, false);
var title = getParam(params, "title", config.macros.newTiddler.title),
label = getParam(params, "label", config.macros.newTiddler.label),
prompt = getParam(params, "prompt", config.macros.newTiddler.prompt),
text = getParam(params, "text", ""),
set = getParam(params, "set", ""),
commonTags = [], t,
orderParts = getParam(params,"order",""),
orderPartsMatch = /^(\w*),(.+),([\w\d\-]+(?:,\w+)?)$/ .exec(orderParts),
orderCounter = orderPartsMatch ? orderPartsMatch[1] : undefined,
orderFilter = orderPartsMatch ? orderPartsMatch[2] : undefined,
orderMode = orderPartsMatch ? orderPartsMatch[3] : undefined,
orderParamDefault;
if(orderMode) {
orderPartsMatch = /^(\w+),(\w+)$/.exec(orderMode);
orderMode = orderPartsMatch ? orderPartsMatch[1] : orderMode;
orderParamDefault = orderPartsMatch ? orderPartsMatch[2] : undefined;
}
var cmd = config.macros.defineSet;
// get addAction for the set and orderCounter
if(set && cmd) {
// set may be either set name or set definition
if(!set.contains("[")) {
orderCounter = orderCounter ||
cmd.getSortFieldForNamedSet(set);
var action = cmd.getAddToNamedSetAction(set);
} else {
var setDefinition = cmd.parseSetDefinition(set);
orderCounter = orderCounter || setDefinition.sortField;
var action = cmd.getAddToSetAction(setDefinition)
}
}
for(t = 1; t < params.length; t++)
if((params[t].name == "anon" && t != 1) || (params[t].name == "tag"))
commonTags.push(params[t].value);
if((orderCounter =="default" || orderCounter =="") && config.macros.itemMenu)
orderCounter = config.macros.itemMenu.defaultField;
// create button, attach params to it
var btn = createTiddlyButton(place, label, prompt, this.onClick);
btn.params =
{
title: title,
commonTags: commonTags,
addAction: (set && cmd) ? action : "",
text: text,
orderCounter: orderCounter,
orderFilter: orderFilter,
orderMode: orderMode,
orderParamDefault: orderParamDefault
};
},
onClick: window.wrapNoClose(function()
{
// extract params
var title = this.params.title,
text = this.params.text,
tags = [].concat(this.params.commonTags), // should be a new array
//# do the same "copying" for fields, if are set here
addAction = this.params.addAction,
orderCounter = this.params.orderCounter,
orderFilter = this.params.orderFilter,
orderMode = this.params.orderMode,
orderParamDefault = this.params.orderParamDefault,
// create DOM
popup = Popup.create(this),
wrapper = createTiddlyElement(popup, "li"),
nameField = createTiddlyElement(wrapper, "input", null, "newTitleInput",{type:"text"});
nameField.onclick = window.wrapNoClose();
nameField.value = title;
var cmi = config.macros.itemMenu,
addTidToSet = function(tiddler)
{
if(!addAction || !cmi) return;
cmi.parseAndApplyAction(tiddler, addAction, null, true);
};
var createTheTiddler = function(goOnSelected)
{
var theTiddler = new Tiddler(jQuery.trim(nameField.value)),
modifier = config.options.txtUserName;
theTiddler.assign(null, text, modifier, null, tags);
if(store.fetchTiddler(theTiddler.title) && !confirm("A tiddler named \""+theTiddler.title+"\" already exists. Do you want to overwrite it?"))
return;
addTidToSet(theTiddler);
store.saveTiddler(theTiddler);
if(orderCounter && Tiddler.prototype.setSCounter && cmi)
{
if(orderMode == "top")
cmi.moveToTop(orderFilter, theTiddler, orderCounter);
if(orderMode == "bottom")
cmi.moveToBottom(orderFilter,theTiddler,orderCounter);
var orderModeIndex = parseInt(orderMode);
if(!isNaN(orderModeIndex))
cmi.moveToArbitraryPlace(orderFilter, theTiddler, orderCounter, orderModeIndex, true);
// use: orderParamDefault
}
// for compability with SetManagerPlugin (usage in itemMenus)
if(cmi)
{
if(goOnSelected)
cmi.ensureFocusOnCurrentlyDragged();
else
cmi.clearCurrentlyDragged();
}
// bad fix for SMP: if autoSaveChanges(); is called directly,
// reselection doesn't work; but it's enough to set timeout
// of 1 ms and the bug disappears
setTimeout(autoSaveChanges, 1);
};
// process enter/esc key presses
// compatible with SetManagerPlugin (for usage in itemMenus)
nameField.onkeydown = function(ev)
{
var e = ev || window.event;
if(e.which == $enter)
{
createTheTiddler(e.shiftKey);
if(e.shiftKey)
Popup.remove(1);
else
Popup.remove();
window.preventOtherHandling(e);
}
if(e.key === 'Escape') {
if(e.shiftKey) {
if(cmi) cmi.ensureFocusOnCurrentlyDragged();
Popup.remove(1);
} else {
if(cmi) cmi.clearCurrentlyDragged();
Popup.remove();
}
}
};
if(orderMode == "checkboxes") {
// add possibilities to put the tiddler on top/bottom of a certain list (certain orderCounter):
// create 2 checkboxes (t: [] b: []), add .. behaviour
createTiddlyText(popup,"t:");
var checkBoxTop = createTiddlyElement(popup,"input","test"/*null*/,null,null,{
type:'checkbox',
value:false
// calc the value the way it should be calced (chkAddToTop, ..)
});
checkBoxTop.onclick = window.wrapNoClose(function(){
checkBoxTop.setAttribute(!checkBoxTop.value);
// checkboxes should deactivate each other, ..
});
// add the onclick handler (change .., no close)
createTiddlyText(popup,"b:");
config.macros.option.handler(popup, "option", null, wikifier, "chkAddToBottom");
checkBox = popup.lastChild;
checkBox.onclick = window.wrapNoClose(checkBox.onclick);
// this works, but the checkbox being checked/unchecked is not displayed unless the popup is reopened
// - try config.macros.option.genericCreate(place, type, opt, className, desc)
// process the orderCounter taken from the check box
}
// "add" button
createTiddlyButton(wrapper, "add", "add the tiddler", createTheTiddler);
// cancel - on click elsewhere
// show the popup menu, focus inside the text field
Popup.show();
nameField.focus();
nameField.select()
})
}
//}}}
/***
!!!insertEditable macro
***/
//{{{
// Sets the section value if it is present, appends otherwise
// tip: if sectionName is "!!smth", then "!!!smth" is appended
//
Tiddler.prototype.setSection = function(sectionName,value)
{
var beginSectionRegExp = new RegExp("(^!{1,6}[ \t]*" + sectionName.escapeRegExp() + "[ \t]*(\n|$))","mg"),
sectionTerminatorRegExp = /^!/mg,
match = beginSectionRegExp.exec(this.text);
if(match) // edit existing section
{
var sectionTitle = match[1],
emptyAtEnd = match[2] != "\n",
beforeSection = this.text.substr(0,match.index),
sectionAndAfter = this.text.substr(match.index + match[1].length);
match = sectionTerminatorRegExp.exec(sectionAndAfter);
var afterSection = match ? sectionAndAfter.substr(match.index) : "";
this.text = beforeSection + sectionTitle + (emptyAtEnd ? "\n" : "") + value
+ (afterSection ? ("\n" + afterSection) : "");
} else // add anew
this.text = this.text + "\n!"+sectionName + "\n"+value;
// setting dirty, notifying is not done here
};
// Sets the slice value if it is present, otherwise appends it as |name|value|
// either after the last slice or to the beginning of the text (if no slices are present)
//
Tiddler.prototype.setSlice = function(sliceName,value)
{
var replaceSliceSubPart = function(text,part,oldValue)
{
if(oldValue == value)
return text;
var eOldValue = oldValue.escapeRegExp(),
eSliceName = sliceName.escapeRegExp();
// "table" notation
var simplifiedPattern = "^(.*"+eSliceName+".*\\|.*)"+eOldValue+"(.*\\|)$",
simplifiedRegExp = new RegExp(simplifiedPattern),
newPart = part.replace(simplifiedRegExp,function($0,$1,$2){
return $1 + value + $2;
});
if(newPart != part)
return text.replace(part, newPart);
// "sliceName: sliceValue" notation
simplifiedPattern = "^(.*"+eSliceName+"\\:[\\s\\t])"+eOldValue+"(.*)$";
simplifiedRegExp = new RegExp(simplifiedPattern),
newPart = part.replace(simplifiedRegExp,function($0,$1,$2){
return $1 + value + $2;
});
if(newPart != part)
return text.replace(part, newPart);
};
// modification of TiddlyWiki.prototype.slicesRE to process "|..sliceName..||" syntax
// empty slices in the "sliceName:" notation are not supported
var re = /(?:^([\'\/]{0,2})~?([\.\w]+)\:\1[\t\x20]*([^\n]+)[\t\x20]*$)|(?:^\|\x20?([\'\/]{0,2})~?([^\|\s\:\~\'\/]|(?:[^\|\s~\'\/][^\|\n\f\r]*[^\|\s\:\'\/]))\:?\4[\x20\t]*\|[\t\x20]*([^\n\t\x20]?(?:[^\n]*[^\n\t\x20])?)[\t\x20]*\|$)/gm;
re.lastIndex = 0;
var m = re.exec(this.text);
while(m) {
if(m[2]) {
if(m[2] == sliceName) {
this.text = replaceSliceSubPart(this.text,m[0],m[3]);
break;
}
} else {
if(m[5] == sliceName) {
this.text = replaceSliceSubPart(this.text,m[0],m[6]);
break;
}
}
m = re.exec(this.text);
}
if(!m || !m[2] && !m[5]) // if the slice is not present
{
// append after the last slice/to the start of text (adapted from GridPlugin)
var matches = this.text.match(re),
lastSlice = matches ? matches[matches.length-1] : null,
where = lastSlice ? this.text.indexOf(lastSlice)+lastSlice.length : 0;
this.text = this.text.substr(0,where)+
(lastSlice ? '\n|%0|%1|' : '|%0|%1|\n').format(sliceName,value)+
this.text.substr(where);
}
// recalc "stored" slices for this tiddler:
delete store.slices[this.title];
// setting dirty, notifying is not done here
};
config.macros.insertEditable = {
handler: function(place,macroName,params,wikifier,paramString,tiddler)
{
// parse and attach params to DOM
var pParams = paramString.parseParams("tiddler",null,true,false,true),
cell = params.contains("cell"),
fill = cell || params.contains("fillElement"),
wrapper = fill ? place : createTiddlyElement(place,"span"),
tidName = getParam(pParams,"tiddler",""),
partName = getParam(pParams,"container","t"),
applyOnEnter = getParam(pParams,"applyOnEnter"),
selectOptions = getParam(pParams,"options","[]");
eval('selectOptions = '+selectOptions);
wrapper.options = {
tiddler: tidName ? store.fetchTiddler(tidName) : tiddler,
//# store tiddler name so that we can create it if it doesn't exist
part: partName,
viewType: getParam(pParams,"viewType","wikified"),
selectOptions: selectOptions,
withButton: params.contains("button") || window.isOpenedOnTouchScreen(), // default for touch screens
defaultShowText: getParam(pParams,"showIfNoValue"),
preprocess: getParam(pParams,"preprocess",""),
size: getParam(pParams,"size",""),
fill: fill,
transparentEmpty: cell || params.contains("transparentEmpty"),
saveOnApply: params.contains("saveOnApply"),
applyOnEnter: (partName[0] == ":") || (partName[0] == "!") ||
(applyOnEnter === undefined ?
params.contains("applyOnEnter") : applyOnEnter),
keepOnBlur: params.contains("keepOnBlur"),
noNotify_partial: cell || params.contains("noNotify"),
noedit: params.contains("noedit")
};
// when "transcluding", use same stack and nest limit to prevent looping
var stack = config.macros.tiddler.tiddlerStack,
title = wrapper.options.tiddler ? wrapper.options.tiddler.title : '';
if(!config.options.transclusionsNestLimit && (stack.indexOf(title) != -1)
|| config.options.transclusionsNestLimit &&
(stack.length+1 > config.options.transclusionsNestLimit))
{
createTiddlyError(wrapper,
"insertEditable: maximum transcluding depth reached");
return;
}
stack.push(title);
try {
this.turnViewMode(wrapper);
} finally {
stack.pop();
}
},
getData: function(tiddler, part)
{
var partName = part.substr(1);
switch(part[0]) {
case "t":
return tiddler.text || "";
case "!":
return tiddler.title;
case "#":
return (tiddler.getSection ? tiddler.getSection(partName)
: store.getTiddlerText(tiddler.title+"##"+partName))
|| "";
case ":":
return (tiddler.getSlice ? tiddler.getSlice(partName)
: store.getTiddlerText(tiddler.title+"::"+partName))
|| "";
case "@":
return store.getValue(tiddler,partName) || "";
}
},
setData: function(tiddler, part, value, noNotify_partial)
{
var partName = part.substr(1);
//if(!tiddler)
//# deal with the case when tiddler doesn't exist yet
switch(part[0]) {
case "t":
store.saveTiddler(tiddler,null,value);
break;
case "!":
store.saveTiddler(tiddler,value); // requires my fix to .sT
break;
case "#":
tiddler.setSection(partName,value);
store.setDirty(true);
// refresh display of the corresponding tiddler:
if(!noNotify_partial) store.notify(tiddler.title,true);
break;
case ":":
tiddler.setSlice(partName,value);
store.setDirty(true);
if(!noNotify_partial) store.notify(tiddler.title,true);
break;
case "@":
store.setValue(tiddler,partName,value);
break;
}
//# change the "modifier/d" fields?
},
turnViewMode: function(place)
{
if(!place.options.tiddler && place.options.part[0] != "c")
return;
//# wtf is this "c" as first character?
//# may be add warning; also, do the same for unsupported container types
var value = this.getData(place.options.tiddler, place.options.part) ||
place.options.defaultShowText || "",
fill = place.options.fill,
noedit = place.options.noedit,
classEmpty = place.options.transparentEmpty ? "transparentEmptyViewer"
: (place.options.viewType == "select"? "":"emptyViewer");
var preprocessScript = place.options.preprocess;
if(preprocessScript) {
var fullScript =
"place.options.preprocessFunc = function(text){"+
"var q = \"'\"\n"+
preprocessScript+
"\nreturn text;};";
eval(fullScript);
value = place.options.preprocessFunc(value);
}
if(fill)
place.style.padding = (place.options.initialPadding !== undefined)
? place.options.initialPadding
: place.style.padding;
if(fill && !noedit && !value)
place.classList.add(classEmpty);
var html = '<span'+
(!fill && !value ? ' class="'+classEmpty+'"' : '')+
'></span>';
place.innerHTML = html;
place.onclick = function(e)
{
// prevent editor-containing popup closing etc:
if(e.stopPropagation) e.stopPropagation();
e.cancelBubble = true;
// prevent editing when clicking links, buttons, etc; if noedit:
if(noedit || e.target.tagName.match(new RegExp("^(a|img|button|"+
"input|textarea|select|option|canvas)$", "i")))
return true;
// prevent editing on text selection
var selection = "";
if (window.getSelection) {
selection = window.getSelection().toString();
}
// cross-browser enough https://stackoverflow.com/q/10478/3995261
if(selection) return true;
place.classList.remove(classEmpty);
config.macros.insertEditable.turnEditMode(this);
return false;
}
var container = fill ? place : place.firstChild;
switch(place.options.viewType) {
case "plain":
createTiddlyText(container,value);
break;
case "html":
container.innerHTML = value;
break;
case "select":
var select = createTiddlyElement(container, 'select'),
options = place.options.selectOptions, i,
selectedIndex = -1;
// populate with options
for(i = 0; i < options.length; i++) {
createTiddlyElement(select,'option',null,null,
options[i].label, {value:options[i].option});
if(options[i].option == value)
selectedIndex = i;
}
if(selectedIndex != -1)
select.selectedIndex = selectedIndex;
else {
// indicate that value is not a suggested option
jQuery(select).addClass('unsuggested');
jQuery(select).find('option').addClass('normal');
// but show it as a selected "option"
var currentOption = jQuery('<option>'+ value +'</option>')
.prependTo(select);
select.selectedIndex = 0;
}
select.onchange = function(e) {
// once wrong option is changed to an expected one
if(jQuery(select).hasClass('unsuggested')
&& select.selectedIndex != 0)
jQuery(select).removeClass('unsuggested')
.find(':first-child').remove();
var newValue = e.target.value;
config.macros.insertEditable.setData( place.options.tiddler, place.options.part, newValue, place.options.noNotify_partial);
if(place.options.saveOnApply)
autoSaveChanges();
};
break;
case "wikified":
default:
wikify(value,container,null,place.options.tiddler);
}
},
turnEditMode: function(place)
{
// add the edit area
var initialValue = this.getData(place.options.tiddler,place.options.part),
rowslimit = 1,
size = place.options.size;
place.innerHTML = (size == "minimal" || size == "min") ?
'<input type="text" class="mini-inline-editor"></input>'
:('<textarea class="inline-editor"' +
' style="height: '+rowslimit+'.1em;'+
(size == "max" ? 'width:100%;' :
(place.options.fill ? 'width:98%;' : ''))+'"' +
'></textarea>');
if(place.options.fill) {
place.options.initialPadding = place.style.padding;
place.style.padding = "0";
}
var editarea = place.firstChild, button;
editarea.value = initialValue;
// define apply/cancel helpers
var turnOffEditing = function()
{
config.macros.insertEditable.turnViewMode(place);
jQuery('html').off("click",onClick);
},
applyChanges = function()
{
config.macros.insertEditable.setData(place.options.tiddler, place.options.part, editarea.value, place.options.noNotify_partial);
turnOffEditing();
if(place.options.saveOnApply)
autoSaveChanges();
};
if(place.options.withButton)
button = createTiddlyButton(place,"save",null,applyChanges);
// on click outside the edit area, switch to the view mode
var onClick = function(e) {
if(place.options.keepOnBlur || jQuery(e.target).parents()/*and self*/.addBack().is(place))
return;
turnOffEditing();
};
place.onclick = null;
jQuery('html').click(onClick);
//# avoid creating a "global" handler? (to remove extra code from .onkeydown handler)
editarea.onkeydown = function(ev)
{
var e = ev || window.event;
if(e.key === 'Escape')
// for now, don't check if was changed
turnOffEditing()
if((e.which == $enter) && (e.ctrlKey || place.options.applyOnEnter))
{
applyChanges();
e.cancelBubble = true;
if(e.stopPropagation) e.stopPropagation();
return false;
}
};
editarea.oninput = function(){
adjustHeightToContent.apply(this);
if(editarea.value != initialValue)
editarea.classList.add('inline-editor_dirty');
else
editarea.classList.remove('inline-editor_dirty')
}
// set initial "state"
editarea.focus();
if(!window.isOpenedOnTouchScreen()) // with FF for Android, better not to
editarea.select();
adjustHeightToContent.apply(editarea);
}
};
// define styles
setStylesheet(
'.emptyViewer { color: #ddd; background-color: #ddd; }\n'+
'.transparentEmptyViewer { color: rgba(0,0,0,0); background-color: rgba(0,0,0,0); }\n'+
'.emptyViewer:before, .transparentEmptyViewer:before { content: "__" }\n'+
'.mini-inline-editor { width: 1.5em; }\n'+
'@-moz-document url-prefix() {.inline-editor { font-family: Consolas !important; font-size: 100% !important; }}\n'+
'.unsuggested { background-color: #ffdddd; }\n'+
'option.normal { background-color: white; }',
"StyleSheetInsertEditable");
//# think about better styling (no "__" when copying)
// =========== Extras ===========
// hijack edit macro to make tiddler titles editable inline
(function(){
/* config.macros.chkEditableTitles = (config.macros.chkEditableTitles === undefined) ?
!('ontouchstart' in window) : config.macros.chkEditableTitles;
// default for non-touch screens for now
if(!config.macros.chkEditableTitles)
return;
config.macros.view.iep_orig_handler = config.macros.view.handler;
config.macros.view.handler = function(place,macroName,params,wikifier,paramString,tiddler) {
if(readOnly || !(tiddler instanceof Tiddler) || tiddler.isReadOnly() ||
params[0] != "title" || !place.classList.contains("title"))
// the last is a hack for avoiding insertEditable in the <<list>> macro
return this.iep_orig_handler.apply(this,arguments);
wikify('<<insertEditable container:"!" size:max viewType:plain>>',place,null,tiddler);
//story.setDirty(tiddler.title,true); ?
};
*/
})()
//}}}
/***
!!!Sets API and macros
***/
//{{{
// overwrite filterTiddlers to enable different kinds of hijacking
//# to be incorporated into the core
TiddlyWiki.prototype.filterTiddlers = function(filter,results)
{
var re = /([^\s\[\]]+)|(?:\[([ \w\.\-]+)\[([^\]]+)\]\])|(?:\[\[([^\]]+)\]\])/mg;
results = results || [];
var match, handler;
if(filter)
while(match = re.exec(filter)) {
handler = ( match[1] || match[4] ) ? 'tiddler' :
config.filters[match[2]] ? match[2] : 'field';
results = config.filters[handler].call(this,results,match);
}
return results;
};
// extendable set of elementary sets definitions
// "?" is for the belongCheck, "+" - for addAction getter, "-" - for dropAction getter
config.elementarySets =
{
tiddler: {
"?": function(title,tiddler) {
return tiddler.title == title;
},
"+": function(title) {
return ""; // no action for now
},
"-": function(title) {
return ""; // no action for now
}
},
tag: {
"?": function(tagName,tiddler) {
return tiddler.tags.contains(tagName);
},
"+": function(tagName) {
//# check the absence of ",,","::",";;"
return "+tag.."+tagName;
},
"-": function(tagName) {
//# check the absence of ",,","::",";;"
return "-tag.."+tagName;
}
}
}
config.macros.defineSet =
{
sets: {},
add: function (setName, setDefinition, setTags)
{
if(this.sets[setName])
displayMessage("the set \""+setName+"\" will be redifined");
//# this behaviour may be changed if necessary
this.sets[setName] = { definition: setDefinition, tags: setTags };
},
// this returns a function(tiddler) which checks if the tiddler is in the set;
// if there's some "do" parts, it ignores them if forceFunc and
// returns an array of tiddlers instead of a function otherwise
//
getIsInSet: function (setDefinition,forceFunc)
{
var returnFunc = true, definedPart, resultRecursive,
parts = setDefinition.parts, type, i,
singleTokenRE = /(?:(\w*)\[((?:[^\]]|(?:\]\]))*)\])/,
tokenMatch, tokenType, tokenValue;
//# precalc setDefinition.parts[i].definedPart in .parseSetDefinition? (although func/not func stuff ..)
// for each part..
for(i = 0; i < parts.length; i++)
{
definedPart = null;
// process tokens first
if(parts[i].token)
{
tokenMatch = singleTokenRE.exec(parts[i].token);
//# single tokens first; .oO when multiple are needed, implement parsing
if(!tokenMatch) continue;
tokenType = tokenMatch[1];
tokenValue = tokenMatch[2];
definedPart = {
type: tokenType,
value: tokenValue
};
// process elementaries with corresponding handlers
if(config.elementarySets[tokenType]) {
definedPart.checkTiddler = function(tiddler) {
return config.elementarySets[this.type]["?"]
(this.value, tiddler);
};
parts[i].definedPart = definedPart;
continue;
}
//# process non-elementary tokens,
// for "set" and "setsTagged", launch recursively,
// if returns an array of tids instead of a function
// and !forceFunc, set returnFunc = false
// in contrast to inline sets, named sets can cause infinite loops..
}
// next, process "sets" (defined for brackets)
if(parts[i].set)
{
resultRecursive = this.getIsInSet(parts[i].set,forceFunc);
parts[i].definedPart = resultRecursive instanceof Function ?
{ checkTiddler: resultRecursive } : resultRecursive;
// in the latter case resultRecursive is an array with tids
continue;
}
//# for each "do" if !forceFunc, /skip/ it;
// set returnFunc = false otherwise
}
if(returnFunc) {
// combine checks from parts via setDefinition.combine (example: "+4*1-3+5")
return function(tiddler) {
var re = /([\+\-\*])(\d+)/g, m, isGood = false, val;
//console.log(".combine: "+setDefinition.combine+", parts:");console.log(parts);
//console.log("find definedParts in parts[i].definedPart");
while(m = re.exec(setDefinition.combine)) {
i = parseInt(m[2]);
val = parts[i].definedPart.checkTiddler(tiddler);
//console.log(i+": val is "+val+", isGood is "+isGood);
switch(m[1])
{
case "+": isGood = isGood || val; break;
case "*": isGood = isGood && val; break;
case "-": isGood = isGood && !val; break;
}
}
return isGood;
};
} //else
//# build and return an array of tiddlers
},
getIsInNamedSet: function (setName,forceFunc)
{
var set = this.sets[setName];
if(!set) return null;
return this.getIsInSet(set.definition,forceFunc);
},
getSetTiddlers: function (setDefinition,results)
{
results = results || [];
var check = this.getIsInSet(setDefinition);
if(!check)
return results;
var newTids = [];
if(check instanceof Function)
store.forEachTiddler(function(tName,tiddler) {
if(check(tiddler)) newTids.push(tiddler);
});
else
// check is not a function, but an array of tiddlers
newTids = check;
if(setDefinition.sortField) {
var getComparableCounter = function(tiddler)
{
var c = tiddler.getSCounter(setDefinition.sortField);
return (c != 0 && !c)? -1 : c; // undefined up
};
newTids.sort(function(tiddlerA, tiddlerB)
{
return getComparableCounter(tiddlerA)
- getComparableCounter(tiddlerB);
});
}
for(var i = 0; i < newTids.length; i++)
results.pushUnique(newTids[i]);
return results;
},
getNamedSetTiddlers: function (setName,results)
{
var set = this.sets[setName];
if(!set) return results;
return this.getSetTiddlers(set.definition,results);
},
calcActionStepsInDefinition: function (setDefinition)
{
var parts = setDefinition.parts, i,
singleTokenRE = /(?:(\w*)\[((?:[^\]]|(?:\]\]))*)\])/,
tokenMatch, tokenType, tokenValue;
// for each part
for(i = 0; i < parts.length; i++)
{
// create descriptions of 2 actions sequences:
// one adds a tiddler to the set, another removes the tiddler from it
// process tokens first
if(parts[i].token)
{
tokenMatch = singleTokenRE.exec(parts[i].token);
//# single tokens first; .oO when multiple are needed, implement parsing
if(!tokenMatch) continue;
tokenType = tokenMatch[1];
tokenValue = tokenMatch[2];
// process elementaries with corresponding handlers
if(config.elementarySets[tokenType]) {
//# add "don't recalc if already calced"
parts[i].addAction = config.elementarySets[tokenType]
["+"](tokenValue);
parts[i].dropAction= config.elementarySets[tokenType]
["-"](tokenValue);
continue;
}
//# process non-elementary tokens,
//# ...
}
// next, process "sets" (defined for brackets)
if(parts[i].set)
{
//# add "don't recalc if already calced"
parts[i].addAction = this.getAddToSetAction(parts[i].set);
parts[i].dropAction= this.getDropFromSetAction(parts[i].set);
continue;
}
//# do anything about "do"s?
}
return;
},
getAddToSetAction: function (setDefinition)
{
// combine the already calced actions into one
var re = /([\+\-\*])(\d+)/g, m, i, partActions, actions = "";
while(m = re.exec(setDefinition.combine))
{
i = parseInt(m[2]);
partActions = setDefinition.parts[i];
switch(m[1])
{
case "+":
if(actions || !partActions.addAction) continue;
// unless that's the ~first action~, do nothing
// (we suppose that if one describes a set like
// "this OR that", than add action adds to "this"
actions = partActions.addAction;
break;
case "*":
if(!partActions.addAction) continue;
if(actions) actions += ",,";
actions += partActions.addAction;
break;
case "-":
if(!partActions.dropAction) continue;
if(actions) actions += ",,";
actions += partActions.dropAction;
break;
}
}
return actions;
},
getAddToNamedSetAction: function (setName)
{
var set = this.sets[setName];
if(!set) return null;
return this.getAddToSetAction(set.definition);
},
getDropFromSetAction: function (setDefinition)
{
// combine the already calced actions into one
var re = /([\+\-\*])(\d+)/g, m, i, partActions, actions = "";
while(m = re.exec(setDefinition.combine))
{
i = parseInt(m[2]);
partActions = setDefinition.parts[i];
switch(m[1])
{
// case "*": do nothing (if a tiddler is droped from "a",
// it is dropped from "a OR b"
// case "-": same (consider "a" and "a AND NOT b")
case "+":
if(!partActions.dropAction) continue;
if(actions) actions += ",,";
actions += partActions.dropAction;
break;
}
}
return actions;
},
getDropFromNamedSetAction: function (setName)
{
var set = this.sets[setName];
if(!set) return null;
return this.getDropFromSetAction(set.definition);
},
getSortFieldForNamedSet: function (setName)
{
return this.sets[setName] ? this.sets[setName].definition.sortField : null;
},
parseSetDefinition: function (text)
{
var set = { parts: [], combine: null, sortField: null };
// remember tokens (..[..]..[..]...), substitute them with their numbers
var tokenRegExp = /(?:\w*\[(?:[^\]]|(?:\]\]))*\])+/, // "]]" = escaped "]"
tokenMatch, origTokenText, tokenText,
sortFieldRegExp = /sortField\[(.*)\]/, sortFieldMatch;
while(tokenMatch = tokenRegExp.exec(text))
{
origTokenText = tokenMatch[0];
tokenText = origTokenText[0]=="[" ?
("tiddler"+origTokenText) : origTokenText;
sortFieldMatch = sortFieldRegExp.exec(tokenText);
if(sortFieldMatch) {
text = text.replace(origTokenText,"");
set.sortField = sortFieldMatch[1];
} else {
text = text.replace(origTokenText,set.parts.length);
set.parts.push({ token: tokenText });
}
}
// find first-level brackets, add definitions, substitute in text
var openPosition = text.indexOf("("), i, level = 0, closePosition, setText;
while(openPosition > -1)
{
// find closing bracket position
level = 1; i = openPosition+1;
while(level > 0) {
if(text[i] == "(")
level++;
if(text[i] == ")")
level--;
i++;
}
closePosition = i;
// add definition, parse it recursively, subsititute
setText = text.substring(openPosition+1, closePosition-1)
// substitute numbers in setText back with tokens:
.replace(/\d+/g,function(match){
return set.parts[parseInt(match)].token
});
set.parts.push({ set: this.parseSetDefinition(setText) });
text = text.substring(0,openPosition) + (set.parts.length-1)
+ text.substring(closePosition);
// find next open
openPosition = text.indexOf("(");
}
// find <num> DO <num>, add them to parts, substitute
var doRegExp = /(\d+)\s+DO\s+(\d+)/m, doMatch, target, action;
while(doMatch = doRegExp.exec(text))
{
action = parseInt(doMatch[2]); target = parseInt(doMatch[1]);
action = set.parts[action].token;
target = set.parts[target].token || target;
// add the definition part, substitute the DO expression in the text
set.parts.push({ do: action , to: target });
text = text.replace(doMatch[0],set.parts.length-1);
}
set.combine = "+"+text.replace(/\s+AND\s+/g,"*")
.replace(/\s+NOT\s+/g,"-")
.replace(/\s+OR\s+/g,"+")
.replace(/^ +/,"").replace(/ +$/,"").replace(/ +/g,"+");
this.calcActionStepsInDefinition(set);
return set;
},
handler: function(place,macroName,params,wikifier,paramString,tiddler)
{
// parse params
var parsedParams = paramString.parseParams("name",null,true,false,true),
setName = getParam(parsedParams,"name"),
setText = getParam(parsedParams,"tids"),
setSortField = getParam(parsedParams,"sortField",""),
setTagsLine = getParam(parsedParams,"tags",""),
setTags = setTagsLine.readBracketedList();
if(!setName || !setText)
return;
if(setSortField) setText += " sortField["+ setSortField +"]";
var setDefinition = this.parseSetDefinition(setText);
// show macro text
var w = wikifier, macroTWcode = w.source.substring(w.matchStart,w.nextMatch),
hide = getFlag(parsedParams, "hide", false) || params.contains('hide');
if (!hide)
createTiddlyText(createTiddlyElement(place,"code"),macroTWcode);
// define the set
this.add(setName, setDefinition, setTags);
}
};
// if SharedTiddlersPlugin is installed, make sets "include-aware"
var stp = config.extensions.SharedTiddlersPlugin;
if(stp)
stp.useForReallyEachTiddler(config.macros.defineSet,"getSetTiddlers");
// hijack filterTiddlers so that if there's "set:..." part with an optional terminator
// ":set", then that part is parsed as a definition of a set
TiddlyWiki.prototype.ds_orig_filterTiddlers = TiddlyWiki.prototype.filterTiddlers;
TiddlyWiki.prototype.filterTiddlers = function(filter,results)
{
var beginSetMark = "set:", endSetMark = " modify:";
// set definition starts with "set:", if no such thing, use ordinary filtering
if(filter.indexOf(beginSetMark) != 0)
return this.ds_orig_filterTiddlers(filter,results);
// add tiddlers from the set
results = results || [];
var modifyPos = filter.indexOf(endSetMark), filterAsWell = (modifyPos != "-1"),
setDef = filterAsWell ? filter.substring(4,modifyPos) : filter.substr(4),
tids = config.macros.defineSet.getSetTiddlers(
config.macros.defineSet.parseSetDefinition(setDef),results);
// if necessary, apply the additional filters, return
if(!filterAsWell)
return results;
filter = filter.substr(modifyPos + endSetMark.length);
return this.ds_orig_filterTiddlers(filter,results);
};
config.filters.set = function(results,match)
{
var setName = match[3];
return config.macros.defineSet.getNamedSetTiddlers(setName,results);
};
//-------------------------------------------------------------------------------
// wikify SetsList on startup
//
var readSetsList = function()
{
if(!window.store)
return setTimeout(readSetsList,100);
var setsList = store.fetchTiddler("SetsList"),
setsListText = setsList ? setsList.text : "";
if(setsListText)
wikify(setsListText,document.createElement("div"),null,setsList);
};
setTimeout(readSetsList,100);
//# test why this first timeout is needed (copied from CTP, STP)
//}}}