Welcome to TiddlyWiki created by Jeremy Ruston; Copyright © 2004-2007 Jeremy Ruston, Copyright © 2007-2011 UnaMesa Association
Background: #fff
Foreground: #000
PrimaryPale: #8cf
PrimaryLight: #18f
PrimaryMid: #04b
PrimaryDark: #014
SecondaryPale: #ffc
SecondaryLight: #fe8
SecondaryMid: #db4
SecondaryDark: #841
TertiaryPale: #eee
TertiaryLight: #ccc
TertiaryMid: #999
TertiaryDark: #666
Error: #f88
/***
|Description|Allows to "cook" tiddlers using various "recipes" – automatically (once a "part" is updated) and using a button|
|Version|0.2.5|
|Author|Yakov Litvin|
|Source|not released yet|
***/
//{{{
// creates a hidden section with lines commented by "//"
// to prevent css comments from being treated as end of js comments;
// adds js that grabs that section (of nameOfTiddlerToCook tiddler), removes "//"
// in each line and creates a shadow tiddler treated as css (..)
function wrapAsCssAdder(css, nameOfTiddlerToCook, cssName, options) {
return '// /%\n' +
'/***\n!' + cssName + '\n***/\n' +
css.replace(/^/gm, '//') +
'\n/***\n!end of '+ cssName + '\n***/\n'+
'// %/ //\n' +
'//{{{\n'+
';(function() {\n' +
'var cssName = ' + JSON.stringify(cssName) + ',\n' +
' css = store.getTiddlerText(' + JSON.stringify(nameOfTiddlerToCook) + ' + \"##\" + cssName).replace(/^\\/\\//gm, \"\");\n' +
'css = css.substring(5, css.length - 5); // cut leading \\n***/ and trailing /***\\n of the section\n' +
'config.shadowTiddlers[cssName] = css;\n' +
(options && options.dontApply ? '' :
'store.addNotification(cssName, refreshStyles);\n' +
'store.addNotification("ColorPalette", function(smth, doc) { refreshStyles(cssName, doc) })\n'
) +
'})();\n' +
'//}}}'
}
config.extensions.cookBook = {
// hashmap by tiddlerName of
// - parts: Array of tiddlerTextExpr (tName, tName::slNmae, tName##seName, ::slNmae,)
// - steps: JavaScript expression containing parts[i] to be evaled that returns text,
// null means join("\n") of parts[i] ~contents
// - title: equal to tiddlerName for usage flexibility
recipes: {},
addRecipe: function(title, parts, steps, tags, autoupdate, reinstall, minify, reinstallOnly) {
this.recipes[title] = { title: title, parts: parts, steps: steps, tags: tags, autoupdate: autoupdate, reinstall: reinstall, reinstallOnly: reinstallOnly }
},
getRecipeFor: function(title) { return this.recipes[title] },
getRecipesContaining: function(title) {
var goodPartRegExp = new RegExp("^" + title.escapeRegExp() + "(?:$|::.+|##.+)")
var demandedRecipes = [], t, recipeParts, i;
for(t in this.recipes) {
recipeParts = this.recipes[t].parts;
for(i = 0; i < recipeParts.length; i++)
if(goodPartRegExp.exec(recipeParts[i])) {
demandedRecipes.push(this.recipes[t]);
break;
}
}
return demandedRecipes;
}
};
config.macros.defineRecipe = {
readRecipeList: function() {
if (!window.store) return setTimeout(readRecipeList, 100);
var recipeList = store.fetchTiddler("RecipeList");
if(recipeList && recipeList.text)
wikify(recipeList.text, document.createElement("div"), null, recipeList);
},
handler: function(place, macroName, params, wikifier, paramString, tiddler) {
var pParams = paramString.parseParams("tiddler", null, true, false, true),
checkFlag = function(name, default_val) {
return getParam(pParams, name, default_val) ||
!!((new RegExp("\\s"+name+"(?:\\s|$)")).exec(paramString))
},
tiddlerNames = pParams[0]["tiddler"],
partNames = pParams[0]["parts"],
tags = pParams[0]["tags"] || [],
plugin = checkFlag("plugin"),
recipe = getParam(pParams, "recipe", ""),
autoupdate = checkFlag("autoupdate"),
reinstallOnly= checkFlag("reinstallOnly"),
reinstall = checkFlag("reinstall") || reinstallOnly,
minify = checkFlag("minify");
if(plugin) tags.push("systemConfig");
if(!tiddlerNames) {
createTiddlyError(place,
"Macro error: no 'tiddler' param (click this for details)",
"Put it after the macro name as either 'tiddler:\"tiddlerName\"' or just '\"tiddlerName\"'");
return;
}
if(reinstallOnly) partNames = tiddlerNames;
if(!partNames) {
createTiddlyError(place,"Macro error: no 'parts' param (click this for details)","Put them after the 'tiddler' param as 'parts:\"partName1\" \"partName2\" ...'");
return;
}
var tiddlerName = tiddlerNames[0],
sameTiddlerPartRegExp = /^(::|##).+/, i;
for(i = 0; i < partNames.length; i++)
if(sameTiddlerPartRegExp.exec(partNames[i]))
partNames[i] = tiddlerName + partNames[i];
// show the written macro code:
var w = wikifier,
macroTWcode = w.source.substring(w.matchStart, w.nextMatch),
hide = params.contains('hide');
if (!hide)
createTiddlyText(createTiddlyElement(place, "pre"), macroTWcode);
config.extensions.cookBook.addRecipe(tiddlerName, partNames,
recipe, tags, autoupdate, reinstall, minify, reinstallOnly);
}
};
config.macros.cook = {
cookRecipe: function(recipe, force) {
if(!force && !recipe.autoupdate) return;
var partNames = recipe.parts, parts = {}, i;
for(i = 0; i < partNames.length; i++)
parts[partNames[i]] = [ store.getTiddlerText(partNames[i], "") ];
//# for filtering, add extra parsing here (fill the array with several parts)
var script = "config.macros.cook.f = function(title, partNames, parts) {" +
"var text = '', tid = new Tiddler(title)," +
" oldTid = store.fetchTiddler(title);" +
"tid.creator = oldTid ? oldTid.creator : config.options.txtUserName;" +
"tid.created = oldTid ? oldTid.created : new Date();" +
"tid.modifier = config.options.txtUserName;" +
"tid.modified = new Date();" +
"var oldChangeCount = oldTid ? oldTid.fields.changecount : null;" +
"tid.fields.changecount = oldChangeCount ? (parseInt(oldChangeCount)+1) : 0;\n" +
recipe.steps +
"\nvar partName, namedParts, i;" +
"for(partName in parts) {" +
" namedParts = parts[partName];" +
" for(i = 0; i < namedParts.length; i++)" +
" text += (namedParts[i]+'\\n');" +
"};" +
"if(text) text = text.substr(0,text.length-1);" +
"tid.text = tid.text || text;" +
"return tid;" +
"};"
console.log("in cookRecipe, script is",script);
eval(script);
var tid = this.f(recipe.title, partNames, parts),
modifier = config.options.txtUserName,
modified = new Date();
tid.tags = tid.tags.concat(recipe.tags);
if(recipe.minify) {
tid.text = tid.text.replace(/\r/gm,'\n');
//# implement minification here, use some external lib
}
console.log("in cookRecipe, tid is",tid);
if(!recipe.reinstallOnly) {
store.saveTiddler(tid, recipe.title, null, modifier, modified);
displayMessage("Tiddler \""+ recipe.title +"\" has been cooked");
console.log("Tiddler \""+recipe.title+"\" has been cooked");
//# add msg that tells the results (test this with both ways of cooking)
//# add msg that tells if some part wasn't found
}
if(recipe.reinstall /*&& isPluginEnabled(tid)*/) {
//# do stuff from STP's installPlugin (logging)
var msg;
try {
window.eval(tid.text);
msg = '"'+ tid.title +'" was reinstalled.';
} catch(ex) {
// do react on errors in a helpful way
msg = "Error evaluating "+ tid.title +":\n"+
exceptionText(ex)
} finally {
displayMessage(msg);
console.log(msg);
}
//# do stuff from STP's installPlugin (logging)
}
},
cookRecipeFor: function(title, force) {
var recipe = config.extensions.cookBook.getRecipeFor(title);
if(recipe)
this.cookRecipe(recipe, force);
else
return 'no recipe for "'+ title +'"';
},
cookRecipesContaining: function(title, force) {
var recipes = config.extensions.cookBook.getRecipesContaining(title), i;
for(i = 0; i < recipes.length; i++)
this.cookRecipe(recipes[i], force);
},
handler: function(place, macroName, params, wikifier, paramString, tiddler) {
var pParams = paramString.parseParams("title", null, true, false, true),
title = getParam(pParams, "title", tiddler ? tiddler.title : ""),
label = getParam(pParams, "label", "cook"),
tooltip = getParam(pParams, "tooltip", "");
createTiddlyButton(place, label, tooltip, function() {
var error = config.macros.cook.cookRecipeFor(title, true);
if(error) displayMessage(error);
});
}
};
// hijack in a reinstallable fashion
if(!config.commands.saveTiddler.orig_handler_CTP)
config.commands.saveTiddler.orig_handler_CTP = config.commands.saveTiddler.handler;
config.commands.saveTiddler.handler = function(event, src, title) {
// "pre-saving" of the tiddler's text for cooking to see it
// (code extracted from Story.saveTiddler)
var tiddlerElem = story.getTiddler(title);
if(tiddlerElem) {
var fields = {};
story.gatherSaveFields(tiddlerElem, fields);
var tiddler = store.saveTiddler(title, title, fields.text);
}
config.macros.cook.cookRecipesContaining(title);
//# add the corresponding messages
return config.commands.saveTiddler.orig_handler_CTP.apply(this, arguments);
// it's important not to use "this" here (causes conflicts with CodeMirror and may do so with others)
};
setTimeout(config.macros.defineRecipe.readRecipeList, 100);
//}}}
/***
|''Name''|DarkModePlugin|
|''Description''|This plugin introduces "dark mode" (changes styles) and switching it by the {{{darkMode}}} macro and operating system settings|
|''Documentation''|https://yakovl.github.io/TiddlyWiki_DarkModePlugin/|
|''Author''|Yakov Litvin|
|''Version''|1.3.2|
|''Source''|https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/DarkModePlugin.js|
|''License''|[[MIT|https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/LICENSE]]|
!!!Demo
<<darkMode>>
<<darkMode label:"☀️/🌘">>
!!!Syntax
{{{
<<darkMode>> (<<switchNightMode>> also works, for backward compatibility)
<<darkMode label:"☀️/🌘">>
}}}
!!!Installation
Is as usual: import or copy the plugin with the {{{systemConfig}}} tag, reload. Note: for the plugin to work correctly, you should keep its name (DarkModePlugin).
!!!Optional configuration
When the dark mode is applied, the {{{darkMode}}} class is added to the {{{html}}} element. This allows to add ''styles for dark mode'' only, like this:
{{{
.darkMode code { color:red }
code { color: green }
}}}
Ordinary styles are applied to both modes, but {{{.darkMode}}} ones have higher precedence and "overwrite" the oridinary ones.
The palette applied for the dark mode can be ''customized'' by editing ColorPaletteDark (removing it restores the default values).
!!!Additional notes
Styles of some browser interface bits (like <html><button class="button" onclick='alert("this is known as an alert")'>alert</button</html> are only affected by OS/browser's "dark mode"/theme, so for good UI it is recommended to switch OS dark mode (DarkModePlugin will follow). For Windows users, [[switching by hotkey|https://superuser.com/a/1724237/576393]] may be useful.
The plugin ''adds extra styles'' (see ~FollowDarkMode and ~FewerColors sections) which are not yet configurable.
The option {{{chkDarkMode}}} is now ''deprecated'': later it will be either removed or re-implemented.
!!!Code
***/
//{{{
config.macros.switchNightMode = // backward compatibility
config.macros.darkMode = {
pluginName: "DarkModePlugin",
optionName: "chkDarkMode",
getDarkPaletteText: function() {
return store.getTiddlerText(this.darkPaletteTitle)
},
// this helper may become more complex for custom themes
getMainPaletteTitle: function() {
return "ColorPalette"
},
lightPaletteTitle: "ColorPaletteLight",
darkPaletteTitle: "ColorPaletteDark",
// setDark, setLight, and applyAdjustments are "governed outside": they don't check or change the cookie-parameter
setDark: function() {
var paletteTitle = this.getMainPaletteTitle()
var lightPaletteTiddler = new Tiddler(this.lightPaletteTitle)
lightPaletteTiddler.text = store.getTiddlerText(paletteTitle) || "shadow"
store.saveTiddler(lightPaletteTiddler)
var darkPaletteTiddler = new Tiddler(paletteTitle)
darkPaletteTiddler.text = this.getDarkPaletteText()
// attach the tiddler, recalc slices, invoke notifiers
store.saveTiddler(darkPaletteTiddler)
this.applyAdjustments(true)
},
setLight: function() {
var paletteTitle = this.getMainPaletteTitle()
var lightPaletteText = store.getTiddlerText(this.lightPaletteTitle)
if(!lightPaletteText || lightPaletteText === "shadow")
store.removeTiddler(paletteTitle) // to recalc slices of ColorPalette
else
store.saveTiddler(paletteTitle, paletteTitle, lightPaletteText)
store.deleteTiddler(this.lightPaletteTitle)
this.applyAdjustments(false)
},
applySectionCSS: function(sectionName) {
var sectionText = store.getRecursiveTiddlerText(this.pluginName + "##" + sectionName, "", 1)
var css = sectionText.replace(/^\s*{{{((?:.|\n)*?)}}}\s*$/, "$1")
return setStylesheet(css, sectionName)
},
applyAdjustments: function(isDarkMode) {
if(isDarkMode) {
jQuery('html').addClass('darkMode')
this.applySectionCSS("FollowDarkMode")
this.applySectionCSS("~FewerColors")
} else {
jQuery('html').removeClass('darkMode')
removeStyleSheet("FollowDarkMode")
removeStyleSheet("~FewerColors")
}
},
// "governance" methods
isDarkMode: function() {
return !!store.fetchTiddler(this.lightPaletteTitle)
},
switchMode: function() {
var me = config.macros.darkMode
config.options[me.optionName] = !config.options[me.optionName]
config.options[me.optionName] ? me.setDark() : me.setLight()
// "baking" doesn't work yet..
if(saveOption)
saveOption(me.optionName)
else
saveOptionCookie(me.optionName)
refreshColorPalette()
},
followOsMode: function(followLight) {
// old browsers may fail to detect
var isOsDarkModeDetected = window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
if(isOsDarkModeDetected && !this.isDarkMode()) {
config.options[this.optionName] = false
this.switchMode()
}
if(!isOsDarkModeDetected && this.isDarkMode() && followLight) {
config.options[this.optionName] = true
this.switchMode()
}
},
restoreSavedMode: function() {
if(!this.isDarkMode()) return
// TODO: check if styles are really missing (avoid applying twice)
this.applyAdjustments(true)
config.options[this.optionName] = true
},
handler: function(place, macroName, params, wikifier, paramString, tiddler) {
var pParams = paramString.parseParams("anon", null, true, false, true)
var label = getParam(pParams, "label", "switch")
var tooltip = ""
createTiddlyButton(place, label, tooltip, this.switchMode)
}
}
// We avoid using .init to support installation via SharedTiddlersPlugin, TiddlerInFilePlugin, and reinstalling via CookTiddlerPlugin.
// This also helps to avoid extra refreshing.
;(function(macro) {
// Save the palette as shadow so that one can cusomize it
config.shadowTiddlers[macro.darkPaletteTitle] =
store.getTiddlerText(macro.pluginName + "##DarkModeColorPalette")
// Set dark mode on start if OS dark mode is set or dark mode was saved previously
macro.followOsMode(false)
macro.restoreSavedMode()
// install only once
if(!config.extensions.DarkModePlugin) {
// prevent sites to ask about unsaved changes after switching mode
config.extensions.DarkModePlugin = {
orig_confirmExit: confirmExit
}
window.confirmExit = function() {
if(readOnly) return
return config.extensions.DarkModePlugin.orig_confirmExit ?
config.extensions.DarkModePlugin.orig_confirmExit() : undefined
}
// Detect OS mode change, apply
if(window.matchMedia) window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', function(event) { macro.followOsMode(true) })
}
})(config.macros.darkMode)
//}}}
/***
!!!FollowDarkMode
{{{
input, select, textarea {
color:[[ColorPalette::Foreground]];
background-color:[[ColorPalette::Background]];
}
.darkMode {
color-scheme: dark;
}
}}}
!!!~FewerColors
{{{
.title, h1, h2, h3, h4, h5, h6 {
color: [[ColorPalette::PrimaryDark]];
}
::selection {
background: [[ColorPalette::TertiaryMid]];
}
}}}
!!!DarkModeColorPalette
Background: #000
Foreground: #ddd
~PrimaryPale: #730
~PrimaryLight: #e70
~PrimaryMid: #fb4
~PrimaryDark: #feb
~SecondaryPale: #003
~SecondaryLight: #017
~SecondaryMid: #24b
~SecondaryDark: #7be
~TertiaryPale: #111
~TertiaryLight: #333
~TertiaryMid: #666
~TertiaryDark: #999
Error: #f44
!!!
***/
|Source |https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsCollection.txt|
|Description|This is a central collection for ExtensionsExplorerPlugin. It is meant to gather collections of existing extensions, and also to help new authors make their work more explorable.|
|Version |0.1.1|
Current status is "under construction", meaning that there's a lot of collections and extensions to add. Other things should be considered as well, like quality guidelines, handling forks, etc. Instructions for authors will be published in a separate readme and linked here.
//{{{
[
{
"url": "https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLExtensionsCollection.txt",
"description": "Extensions created or heavily modified by Yakov Litvin",
"type": "collection"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_Extensions/blob/master/TranslationsCollection.txt",
"description": "TiddlyWiki Translations",
"type": "collection"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_Extensions/blob/master/FND/SimpleSearchPlugin.js",
"description": "Displays search results as a simple list of matching tiddlers"
},
{
"url": "https://github.com/PengjuYan/TiddlyWiki_SwitchPalettePlugin/blob/master/SwitchPalettePlugin.js",
"description": "Switches among your color palettes"
}
]
//}}}
/***
|Description|checks and reports updates of installed extensions on startup, introduces a macro/backstage button to explore, install and update extensions|
|Version |0.6.5|
|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.
!!!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]].
***/
//{{{
// 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: {
backstageButtonLabel: "explore extensions",
backstageButtonTooltip: "See if there are any updates or install new ones",
installButtonLabel: "install",
installButtonPrompt: "get and install this extension",
otherActionsPrompt: "show other actions",
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+(\/\/}}})\s*$/.exec(text)
if(match) try {
return JSON.parse(match[2])
} catch (e) {
console.log(`problems with parsing ${centralSourcesListName}:`, e)
return null
}
},
//# use getAvailableRepositories to get lists of extensions
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
},
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 && !readOnly)
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: this.lingo.backstageButtonLabel,
tooltip: this.lingo.backstageButtonTooltip,
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', null, 'actionsCell')
const actionsWrapper = createTiddlyElement(actionsCell, 'div', null, 'actionsWrapper')
if(cells.actionElements.length > 0) {
actionsWrapper.appendChild(cells.actionElements[0])
actionsWrapper.firstChild.classList.add('mainButton')
}
if(cells.actionElements.length > 1) {
const { lingo } = config.macros.extensionsExplorer
const otherActionEls = cells.actionElements.slice(1)
createTiddlyButton(actionsWrapper, '▾',
lingo.otherActionsPrompt,
function(event) {
const popup = Popup.create(actionsWrapper)
for(const e of otherActionEls) {
const li = createTiddlyElement(popup, 'li')
li.appendChild(e)
}
popup.style.minWidth = actionsWrapper.offsetWidth + 'px'
Popup.show()
event.stopPropagation()
return false
},
'button otherActionsButton')
}
$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
}
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)
}
}
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 ? createTiddlyElement(null, 'div', null, 'actionsLabel', this.lingo.noSourceUrlAvailable) :
cachedUpdate ? installUpdateButton :
checkUpdateButton
]
})
}
},
grabAndInstall: function(extension) {
if(!extension) return
if(extension.text) {
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
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?
}
// import preparation
extensionTiddler.tags.pushUnique('systemConfig')
break;
case 'collection':
extensionTiddler.tags.pushUnique(this.collectionTag)
break;
//# add _ tag for themes?
}
// 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,
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
// Add styles
const css = `
.actionsLabel, .actionsCell .button {
padding: 0.2em;
display: inline-block;
border: none;
white-space: normal;
}
td.actionsCell {
padding: 0;
}
.actionsWrapper {
white-space: nowrap;
}
.button.mainButton {
padding-left: 0.7em;
}`
const shadowName = 'ExtensionsExplorerStyles'
if(!config.shadowTiddlers[shadowName]) {
config.shadowTiddlers[shadowName] = css
store.addNotification(shadowName, refreshStyles)
store.addNotification("ColorPalette", function(_, doc) { refreshStyles(shadowName, doc) })
}
//}}}
ResponsiveThemePlugin is [[cooked|RecipeList]] from +++[components]
* ResponsiveThemeSkeleton
** extending MarkupPreHead (pre-head is built on the next save, used on reload)
* ResponsivePageTemplate
* (no custom ViewTemplate or EditTemplate yet)
* ResponsiveStyleSheet
* TopLineMenu + TopLineMenuMiddle
=== +++[Some of finished tasks]
* (+) create "central column", colorize outer part
* (+) remake main menu using flex, add classes that make Holy Grail CSS easy to understand
* (+) set min height of #contentWrapper to 100vh
* (+) make backstage hover over header, avoid shifting content down
* (+) get rid of .headerShadow and gradient macro (see core)
* (+) add viewport
===; Other todos:
* (+) pack backstage elements (#backstageButton, #backstageArea, #backstage, but not #backstageCloak – should be full width) into a single container ([[via JS|ResponsiveThemeSkeleton]])
** (+) fix: now clock is over the whole backstage (see [[here|https://stackoverflow.com/q/31747334/3995261]]; [[here|https://css-tricks.com/full-bleed/]])
*** not going to solve the page horizontal scrollbar on backstage open soon
** (+) put that container inside a common #fullContentWrapper (#contentWrapper in there, too), position #messageArea relatively to the right edge of #fullContentWrapper
*** ok: now #messageArea is shown outside the #fullContentWrapper
*** may be do the same in the core
** (+) #backstageCloak shouldn't add vertical scroll when the content doesn't overflow
** (+) limit container width, like max-width: 80em;
*** todo: make optional
* (+) +++[pack all components]
* (+) use ResponsiveThemeSkeleton, ResponsivePageTemplate, and RecipeList
* (+) pack ResponsiveStyleSheet via tweaked {{{wrapAsCssAdder}}}, clean StyleSheet
** (+) make StyleSheet updates (for custom styles) applied at once, like usually
* (+) pack populating MarkupPreHead
* (+) fix: in 2.9.2, messages' styles are not applied (add {{{class="messageArea"}}})
* (+) fix: Source slice value is displayed partially (text before {{{;}}} is parsed as CSS)
=== into ResponsiveThemePlugin
** (+) pack TopLineMenu + TopLineMenuMiddle
*** (+) using a hack: reuse {{{wrapAsCssAdder}}}, back-propagate the "4 → 5" fix
*** update CookTiddlerPlugin: should have a separate ~{{{wrapAsShadow}}} helper
* +++[hide message on click]
review what I'm using on my mobile nodes:
//{{{
// TODO: should be click outside #messageArea
jQuery(document.body).bind("click", function() {
clearMessage()
return true
})
// is this to avoid scenarios when a click on saveChanges closes the message generated by saving?
if(!config.extensions.postponeMsg) {
config.extensions.postponeMsg = true
var orig_displayMessage = displayMessage
displayMessage = function(a, b) {
// TODO: use .apply instead?
setTimeout(() => orig_displayMessage(a, b), 100)
}
}
//}}}
===
* note: TiddlersBarPlugin requires editing ResponsivePageTemplate
* [jumper: besides viewport and other responsive, make sure it stays in the middle vertically, like on desktop (reproduced on desktop in dev tools in the [[repo|https://yakovl.github.io/TiddlyWiki_JumpKeysPlugin/]])]
* remake header + main m. + main + sidebar using grid, check backstage and messages
* MainMenu, ''sidebar'':
** (+) hide on narrow screens (iteration 1)
** (+) add a sticky TopLineMenu (via PageTemplate), buttons to open the menus
*** ''try to fix'': on ctrl + home in an edited tiddler, the cursor may get invisible as the sticky menu hovers over it, plus doesn't play well with jumping
** (+) make the middle content transcluded from another tiddler (TopLineMenuMiddle)
*** (+) fix: TLMM content is only shown +++[after editing TLMM] but not on startup, and refreshing (like on clicking DMP button) hides it ===
** (+) add closing of menus on click elsewhere when they're hovering
** make the paddings consistent (same as the content, which may be reduced)
** fix: lists in sidebar shouldn't get outside the main container in the mobile mode
** optionally: remember state in options (so that one can hide MainMenu permanently)
** consider showing both menus as ~sticky (to the top), with a scrollbar: auto to main and sidebar lists
** consider adding backstage button in the topline as well; save button? other elements from sidebar?
** later: animate toggling
** alternatives: sticky menu floating on the left/right/both (takes less space, avoids the hover over cursor problem)
* add a button/option to toggle header? to toggle full width? to adjust font size? move backstage button into top line? (TopLineMenuMiddle can be used for that, as well as for dark mode switch)
* fix: on mobile, when no tiddlers are open, there are shadows in unexpected positions
* may be worth noting in docs: [[orientation quirk|https://groups.google.com/g/tiddlywikiclassic/c/5ZQYT_-mOS8 on iPhone]] may be solved with {{{body { -webkit-text-size-adjust: none ; } }}}
Design considerations:
* +++[breakpoints]
* bootstrap uses X-small <576px, Small <768px, Medium <992px, (992px≤ Large <1200px, Extra large ≥1200px, Extra extra large ≥1400px)
* let's try <768px for mobile: `@media (max-width: 768px) {`
===
* font size: _
** (+) for .siteTitle, decrease down to 1.5em
** main: need larger for pm (and pn?!)
* page header: _
* tiddler subtitle: _
* backstage: _
* tiddler toolbar: _
* tags, tagging:
** hide "no tags" (test with intellitagger: the edit button may be shown)
** move to the tiddler subtitle?
* some nice references:
** well-readable: https://vas3k.blog/notes/moderation/, https://vk.com/@yakov_litvin-strannyi-vishlist-2020, Vivaldi read mode, may be FCC
* remove background color from header and margins?
* combining with other themes: _
Delivery:
* (+) [[pre-release|https://groups.google.com/g/tiddlywikiclassic/c/ZkELTvfb6hA]]
* ''add to ~GitHub and EEP collection (or make EEP work with Tiddlyhost)''; probably host via GH pages and redirect from here
** if keeping this as the main demo – how to sync with GH? can TIFP be used for that?
* announce as a full release, ''link the thread here'' (in MainMenu)
* my personal/pre-releases collection; personal nodes + sync (d.eco, studies!)
* make "Hub listed"; turn into a Tiddlyhost template?
* add to classic.tiddlywiki.com? to other sites? (probably to the plugin repos)
* add some bits to the core? starting with viewport?
Infra:
* (+) upgrade to 2.10.1
** make a backup (and facilitate for TH)
** facilitate upgrading @TH
* (+) save [[options|SystemSettings]] (username, no backup, autosave, insert tabs)
** (+) make saving work [[properly on Tiddlyhost|ThostGoodSavingPlugin]] (saving indicator, QSP)
* (+) EEP, central, my collection, DMP
/***
|Description|Adds an interface and hotkeys for jumping between tiddlers and more|
|Source |https://github.com/YakovL/TiddlyWiki_JumpKeysPlugin/blob/master/JumpKeysPlugin.js|
|Author |Yakov Litvin|
|Version |1.2.0|
|License |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
!!!Usage
The plugin works more or less like the tab switching in a browser: press {{{ctrl + j}}} or the "jump" command in tiddler toolbar to open the jumping interface and:
* hold {{{ctrl}}} and press {{{j}}} or ↑/↓ arrows to select a tiddler (if more than one is open);
* unhold {{{ctrl}}} or click a row to jump.
It also substitutes the jump toolbar command dropdown with the same jumper interface.
As a development of the idea, it also supports hotkeys for some other actions on the tiddler, selected in the jumping interface. Currently, they are:
* {{{x}}} to close the selected tiddler;
* {{{e}}} to edit it.
***/
//{{{
if(!config.jumper) config.jumper = {}
merge(config.jumper, {
getOpenTiddlersData: function() {
const list = []
story.forEachTiddler(function(title, tiddlerElement) {
list.push({
title: title, element: tiddlerElement,
isEditable: !!jQuery(tiddlerElement).has('.editor').length,
isShadow: tiddlerElement.classList.contains('shadow'),
isMissing: tiddlerElement.classList.contains('missing')
})
})
this.sortInAccordWithTouchedTiddlersStack(list)
return list
},
getOpenTiddlerDataByIndex: function(index) {
const list = this.getOpenTiddlersData()
if(index >= list.length || index < 0) return null
return list[index]
},
jumpToAnOpenTiddler: function(index) {
const tiddlerData = this.getOpenTiddlerDataByIndex(index)
if(!tiddlerData) return
// for compatibility with TiddlersBarPlugin
if(config.options.chkDisableTabsBar !== undefined)
story.displayTiddler(null, tiddlerData.title)
if(tiddlerData.isEditable) {
const $editor = jQuery(tiddlerData.element).find('.editor textarea')
// works with CodeMirror as well!
$editor.focus()
} else {
window.scrollTo(0, ensureVisible(tiddlerData.element))
// remove focus from element edited previously
// (also fixes a problem with handsontable that steals focus on pressing ctrl)
// will be substitited with focusing an editor when one is to be focused
if(document.activeElement) document.activeElement.blur()
}
this.pushTouchedTiddler({ title: tiddlerData.title })
},
callCommand: function(toolbarCommandName, index) {
const tiddlerData = this.getOpenTiddlerDataByIndex(index)
if(!tiddlerData) return
const command = config.commands[toolbarCommandName]
if(!command || !command.handler) return
// disable animation so that this methods finishes after closeTiddler etc finishes
const chkAnimate = config.options.chkAnimate
config.options.chkAnimate = false
command.handler(null/*event*/, null/*src*/, tiddlerData.title)
config.options.chkAnimate = chkAnimate
},
touchedTiddlersStack: [], // of { title: string }
pushTouchedTiddler: function(tiddlerStackElement) {
this.removeTouchedTiddler(tiddlerStackElement)
this.touchedTiddlersStack.push(tiddlerStackElement)
},
removeTouchedTiddler: function(tiddlerStackElement) {
this.touchedTiddlersStack = this.touchedTiddlersStack
.filter(item => item.title != tiddlerStackElement.title)
},
sortInAccordWithTouchedTiddlersStack: function(itemsWithTitles) {
for(var i = 0; i < this.touchedTiddlersStack.length; i++) {
var touchedTitle = this.touchedTiddlersStack[i].title
for(var j = 0; j < itemsWithTitles.length; j++)
if(itemsWithTitles[j].title == touchedTitle)
itemsWithTitles.unshift(
itemsWithTitles.splice(j, 1)[0]
)
}
},
css: store.getTiddlerText("JumpKeysPlugin##Jumper styles", "")
.replace("//{{{", "/*{{{*/").replace("//}}}", "/*}}}*/"),
modalClass: 'jump-modal',
itemClass: 'jump-modal__item',
selectedItemClass: 'jump-modal__item_selected',
modal: null,
isJumperOpen: function() {
return !!this.modal
},
showJumper: function() {
const openTiddlersData = this.getOpenTiddlersData()
if(openTiddlersData.length < 2) return false
if(!this.isJumperOpen()) {
// TODO: try "modal" element
this.modal = createTiddlyElement(document.body, 'div', null, this.modalClass)
this.refreshJumper()
return true
} else
return false
// return value indicates whether the modal was opened by this call
},
refreshJumper: function() {
if(!this.isJumperOpen()) return
const openTiddlersData = this.getOpenTiddlersData()
const $modal = jQuery(this.modal)
.empty()
const list = createTiddlyElement(this.modal, 'div', null, this.modalClass + '__list')
//# find where are we (inside an editor; focus inside tiddlerElement;
// scroll between .. and ..)
for(let i = 0; i < openTiddlersData.length; i++) {
var listItem = createTiddlyElement(list, 'div', null,
this.itemClass + (i != 1 ? '' :
' ' + this.selectedItemClass) +
(openTiddlersData[i].isShadow ?
' ' + this.itemClass + '_shadow' :
openTiddlersData[i].isMissing ?
' ' + this.itemClass + '_missing' : '') +
(openTiddlersData[i].isEditable ?
' ' + this.itemClass + '_editable' : ''),
openTiddlersData[i].title)
listItem.onclick = () => {
this.selectByIndex(i)
this.hideJumperAndJump()
}
}
//# or append list after forming
},
hideJumper: function() {
if(!this.isJumperOpen()) return
this.modal.parentElement.removeChild(this.modal)
//# ..or hide? (keep isJumperOpen coherent)
this.modal = null
},
isCtrlHold: false,
getSelectedIndex: function() {
if(!this.isJumperOpen() || !this.modal.firstElementChild) return -1
return Array.from(this.modal.firstElementChild.children)
.findIndex(option => option.classList.contains(this.selectedItemClass))
},
selectByIndex: function(index) {
if(!this.isJumperOpen() || !this.modal.firstElementChild) return
const list = this.modal.firstElementChild
jQuery(list.children[this.getSelectedIndex()])
.removeClass(this.selectedItemClass)
const option = list.children[index]
jQuery(option).addClass(this.selectedItemClass)
const stickOutBottom = option.offsetTop + option.offsetHeight - list.offsetHeight
const stickOutTop = list.scrollTop - option.offsetTop
if(stickOutBottom > 0) list.scrollTop += stickOutBottom
if(stickOutTop > 0) list.scrollTop -= stickOutTop
},
selectPrev: function() {
var currentIndex = this.getSelectedIndex()
var optionsCount = this.getOpenTiddlersData().length
this.selectByIndex((currentIndex - 1 + optionsCount) % optionsCount)
},
selectNext: function() {
var currentIndex = this.getSelectedIndex()
var optionsCount = this.getOpenTiddlersData().length
this.selectByIndex((currentIndex + 1) % optionsCount)
},
hideJumperAndJump: function() {
if(!this.isJumperOpen()) return
const index = this.getSelectedIndex()
this.jumpToAnOpenTiddler(index)
this.hideJumper()
},
handleKeydown: function(e) {
const self = config.jumper
if(e.key === 'Control') self.isCtrlHold = true
if(self.isCtrlHold) self.handleKeydownOnCtrlHold(e)
},
// next: make configurable via UI
defaultCommandsKeys: {
x: "closeTiddler",
e: "editTiddler"
},
getCommandsKeys: function() {
const json = store.getTiddlerText('JumpKeysSettings')
try {
return JSON.parse(json)
} catch (error) {
//# how/where to notify? ..probably after modifying JumpKeysSettings
// return this.defaultCommandsKeys
}
},
handleKeyup: function(e) {
const self = config.jumper
if(e.key === 'Control') {
self.isCtrlHold = false
self.hideJumperAndJump()
return
}
const normalizedKeyCode = !e.originalEvent.code ? null :
/^(Key)?(\w+)$/.exec(e.originalEvent.code)[2].toLowerCase()
const commandsKeys = self.getCommandsKeys()
if(self.isCtrlHold && self.isJumperOpen() && normalizedKeyCode in commandsKeys) {
const index = self.getSelectedIndex()
self.callCommand(commandsKeys[normalizedKeyCode], index)
const numberOfOpen = self.getOpenTiddlersData().length
if(numberOfOpen < 1) { // or < 2 ?
self.hideJumper()
} else {
self.refreshJumper()
self.selectByIndex(index < numberOfOpen ? index : index - 1)
}
if(e.preventDefault) e.preventDefault()
return false // prevent _
}
},
handleKeydownOnCtrlHold: function(e) {
// make this work in different keyboard locale layouts:
if(e.originalEvent.code == "KeyJ") {
if(!this.showJumper()) this.selectNext()
if(e.preventDefault) e.preventDefault()
return false // prevent _
}
if(!this.isJumperOpen()) return
switch(e.key) {
case 'ArrowUp': this.selectPrev(); break
case 'ArrowDown': this.selectNext(); break
case 'ArrowLeft': this.hideJumper(); break
default: return
}
if(e.preventDefault) e.preventDefault()
return false // prevent _
},
substituteJumpCommand: function() {
config.commands.jump.type = null
config.commands.jump.handler = function() {
config.jumper.showJumper()
}
}
})
config.shadowTiddlers['JumpKeysStyleSheet'] = config.jumper.css
config.shadowTiddlers['JumpKeysSettings'] = JSON.stringify(config.jumper.defaultCommandsKeys, null, 2)
// reinstall-safe decorating and setting handlers
if(!config.jumper.orig_story_displayTiddler) {
config.jumper.orig_story_displayTiddler = story.displayTiddler
store.addNotification('JumpKeysStyleSheet', refreshStyles)
store.addNotification("ColorPalette", (unused, doc) => refreshStyles('JumpKeysStyleSheet', doc))
jQuery(document)
.on('click', event => {
const element = config.jumper.modal
if (element && !element.contains(event.target))
config.jumper.hideJumper()
})
//# these are not updated on reinstalling
.on('keydown', config.jumper.handleKeydown)
.on('keyup', config.jumper.handleKeyup)
// avoid stucking ctrl as "hold" on ctrl + f etc
//# doesn't seem to work anymore
window.addEventListener('blur', () => config.jumper.isCtrlHold = false)
config.jumper.substituteJumpCommand()
}
// a very simplistic implementation:
story.displayTiddler = function(srcElement, tiddler, template, animate, unused, customFields, toggle, animationSrc) {
config.jumper.pushTouchedTiddler({
title: (tiddler instanceof Tiddler) ? tiddler.title : tiddler
//# ...: template == DEFAULT_EDIT_TEMPLATE
})
return config.jumper.orig_story_displayTiddler.apply(this, arguments)
}
//}}}
/***
!!!Jumper styles
//{{{
.jump-modal {
position: fixed;
top: 50vh;
left: 50vw;
transform: translate(-50%, -50%);
z-index: 100;
max-width: 80vw;
max-height: 80vh;
box-sizing: border-box;
box-shadow: 1px 1px 10px #ccc;
border-radius: 1em;
background: [[ColorPalette::Background]];
padding: 1em;
display: flex;
}
.jump-modal__list {
position: relative;
overflow: auto;
list-style: none;
padding: 0;
margin: 0;
}
.jump-modal__item {
padding: 0.3em 0.8em;
border-radius: 0.5em;
margin-bottom: 0.5em;
cursor: pointer;
}
.jump-modal__item_shadow {
font-weight: bold;
font-style: italic;
}
.jump-modal__item_missing {
font-style: italic;
}
.jump-modal__item_editable {
text-decoration: underline;
}
.jump-modal__item:hover {
background: [[ColorPalette::SecondaryPale]];
}
.jump-modal__item_selected,
.jump-modal__item_selected:hover {
background: [[ColorPalette::SecondaryLight]];
}
.darkMode .jump-modal__item:hover {
background: rgba(0,0,255,0.35);;
}
.jump-modal__item:last-child {
margin-bottom: 0;
}
.jump-modal ::-webkit-scrollbar {
background-color: transparent;
width: 1.5em;
}
.jump-modal ::-webkit-scrollbar-thumb {
background: [[ColorPalette::TertiaryLight]];
border-radius: 1em;
width: 1em;
border-left: 0.5em solid [[ColorPalette::Background]];
}
//}}}
!!!
***/
[[GettingStarted]]
~TiddlyWiki v<<version>>
<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--}}}-->
/***
|Name|NestedSlidersPlugin|
|Source|http://yakovlitvin.pro/TW/pre-releases/NestedSlidersPlugin_patched.txt|
|Documentation|http://TiddlyTools.com/Classic/#NestedSlidersPluginInfo|
|Version|2.4.10|
|Tweaks|codestyle and meta only, dropped support of TW 2.1|
|Author|Yakov Litvin|
|Original Author|Eric Shulman|
|License|http://www.TiddlyTools.com/#LegalStatements|
|~CoreVersion|2.2|
|Type|plugin|
|Description|show content in nest-able sliding/floating panels, without creating separate tiddlers for each panel's content|
!!!!!Configuration
<<<
<<option chkFloatingSlidersAnimate>> allow floating sliders to animate when opening/closing
Note: for floating slider animation to occur you must also allow animation in general (see [[AdvancedOptions]]).
<<<
!!!!!Code
***/
//{{{
version.extensions.NestedSlidersPlugin= {major: 2, minor: 4, revision: 9, date: new Date(2008,11,15)};
// options for deferred rendering of sliders that are not initially displayed
if (config.options.chkFloatingSlidersAnimate===undefined)
config.options.chkFloatingSlidersAnimate=false; // avoid clipping problems in IE
// default styles for 'floating' class
setStylesheet(".floatingPanel { position:absolute; z-index:10; padding:0.5em; margin:0em; \
background-color:#eee; color:#000; border:1px solid #000; text-align:left; }","floatingPanelStylesheet");
// if removeCookie() function is not defined by TW core, define it here.
if (window.removeCookie===undefined) {
window.removeCookie=function(name) {
document.cookie = name+'=; expires=Thu, 01-Jan-1970 00:00:01 UTC; path=/;';
}
}
config.formatters.push( {
name: "nestedSliders",
match: "\\n?\\+{3}",
terminator: "\\s*\\={3}\\n?",
lookahead: "\\n?\\+{3}(\\+)?(\\([^\\)]*\\))?(\\!*)?(\\^(?:[^\\^\\*\\@\\[\\>]*\\^)?)?(\\*)?(\\@)?(?:\\{\\{([\\w]+[\\s\\w]*)\\{)?(\\[[^\\]]*\\])?(\\[[^\\]]*\\])?(?:\\}{3})?(\\#[^:]*\\:)?(\\>)?(\\.\\.\\.)?\\s*",
handler: function(w)
{
lookaheadRegExp = new RegExp(this.lookahead,"mg");
lookaheadRegExp.lastIndex = w.matchStart;
var lookaheadMatch = lookaheadRegExp.exec(w.source)
if(lookaheadMatch && lookaheadMatch.index == w.matchStart)
{
var defopen=lookaheadMatch[1];
var cookiename=lookaheadMatch[2];
var header=lookaheadMatch[3];
var panelwidth=lookaheadMatch[4];
var transient=lookaheadMatch[5];
var hover=lookaheadMatch[6];
var buttonClass=lookaheadMatch[7];
var label=lookaheadMatch[8];
var openlabel=lookaheadMatch[9];
var panelID=lookaheadMatch[10];
var blockquote=lookaheadMatch[11];
var deferred=lookaheadMatch[12];
// location for rendering button and panel
var place=w.output;
// default to closed, no cookie, no accesskey, no alternate text/tip
var show="none"; var cookie=""; var key="";
var closedtext=">"; var closedtip="";
var openedtext="<"; var openedtip="";
// extra "+", default to open
if (defopen) show="block";
// cookie, use saved open/closed state
if (cookiename) {
cookie=cookiename.trim().slice(1,-1);
cookie="chkSlider"+cookie;
if (config.options[cookie]==undefined)
{ config.options[cookie] = (show=="block") }
show=config.options[cookie]?"block":"none";
}
// parse label/tooltip/accesskey: [label=X|tooltip]
if (label) {
var parts=label.trim().slice(1,-1).split("|");
closedtext=parts.shift();
if (closedtext.substr(closedtext.length-2,1)=="=")
{ key=closedtext.substr(closedtext.length-1,1); closedtext=closedtext.slice(0,-2); }
openedtext=closedtext;
if (parts.length) closedtip=openedtip=parts.join("|");
else { closedtip="show "+closedtext; openedtip="hide "+closedtext; }
}
// parse alternate label/tooltip: [label|tooltip]
if (openlabel) {
var parts=openlabel.trim().slice(1,-1).split("|");
openedtext=parts.shift();
if (parts.length) openedtip=parts.join("|");
else openedtip="hide "+openedtext;
}
var title=show=='block'?openedtext:closedtext;
var tooltip=show=='block'?openedtip:closedtip;
// create the button
if (header) { // use "Hn" header format instead of button/link
var lvl=(header.length>5)?5:header.length;
var btn = createTiddlyElement(createTiddlyElement(place,"h"+lvl,null,null,null),"a",null,buttonClass,title);
btn.onclick=onClickNestedSlider;
btn.setAttribute("href","javascript:;");
btn.setAttribute("title",tooltip);
}
else
var btn = createTiddlyButton(place,title,tooltip,onClickNestedSlider,buttonClass);
btn.innerHTML=title; // enables use of HTML entities in label
// set extra button attributes
btn.setAttribute("closedtext",closedtext);
btn.setAttribute("closedtip",closedtip);
btn.setAttribute("openedtext",openedtext);
btn.setAttribute("openedtip",openedtip);
btn.sliderCookie = cookie; // save the cookiename (if any) in the button object
btn.defOpen=defopen!=null; // save default open/closed state (boolean)
btn.keyparam=key; // save the access key letter ("" if none)
if (key.length) {
btn.setAttribute("accessKey",key); // init access key
btn.onfocus=function(){this.setAttribute("accessKey",this.keyparam);}; // **reclaim** access key on focus
}
btn.setAttribute("hover",hover?"true":"false");
btn.onmouseover=function(ev) {
// optional 'open on hover' handling
if (this.getAttribute("hover")=="true" && this.sliderPanel.style.display=='none') {
document.onclick.call(document,ev); // close transients
onClickNestedSlider(ev); // open this slider
}
// mouseover on button aligns floater position with button
if (window.adjustSliderPos) window.adjustSliderPos(this.parentNode,this,this.sliderPanel);
}
// create slider panel
var panelClass=panelwidth?"floatingPanel":"sliderPanel";
if (panelID) panelID=panelID.slice(1,-1); // trim off delimiters
var panel=createTiddlyElement(place,"div",panelID,panelClass,null);
panel.button = btn; // so the slider panel know which button it belongs to
btn.sliderPanel=panel; // so the button knows which slider panel it belongs to
panel.defaultPanelWidth=(panelwidth && panelwidth.length>2)?panelwidth.slice(1,-1):"";
panel.setAttribute("transient",transient=="*"?"true":"false");
panel.style.display = show;
panel.style.width=panel.defaultPanelWidth;
panel.onmouseover=function(event) // mouseover on panel aligns floater position with button
{ if (window.adjustSliderPos) window.adjustSliderPos(this.parentNode,this.button,this); }
// render slider (or defer until shown)
w.nextMatch = lookaheadMatch.index + lookaheadMatch[0].length;
if ((show=="block")||!deferred) {
// render now if panel is supposed to be shown or NOT deferred rendering
w.subWikify(blockquote?createTiddlyElement(panel,"blockquote"):panel,this.terminator);
// align floater position with button
if (window.adjustSliderPos) window.adjustSliderPos(place,btn,panel);
}
else {
var src = w.source.substr(w.nextMatch);
var endpos=findMatchingDelimiter(src,"+++","===");
panel.setAttribute("raw",src.substr(0,endpos));
panel.setAttribute("blockquote",blockquote?"true":"false");
panel.setAttribute("rendered","false");
w.nextMatch += endpos+3;
if (w.source.substr(w.nextMatch,1)=="\n") w.nextMatch++;
}
}
}
}
)
function findMatchingDelimiter(src,starttext,endtext) {
var startpos = 0;
var endpos = src.indexOf(endtext);
// check for nested delimiters
while (src.substring(startpos,endpos-1).indexOf(starttext)!=-1) {
// count number of nested 'starts'
var startcount=0;
var temp = src.substring(startpos,endpos-1);
var pos=temp.indexOf(starttext);
while (pos!=-1) { startcount++; pos=temp.indexOf(starttext,pos+starttext.length); }
// set up to check for additional 'starts' after adjusting endpos
startpos=endpos+endtext.length;
// find endpos for corresponding number of matching 'ends'
while (startcount && endpos!=-1) {
endpos = src.indexOf(endtext,endpos+endtext.length);
startcount--;
}
}
return (endpos==-1)?src.length:endpos;
}
//}}}
//{{{
window.onClickNestedSlider=function(e)
{
if (!e) var e = window.event;
var theTarget = resolveTarget(e);
while (theTarget && theTarget.sliderPanel==undefined) theTarget=theTarget.parentNode;
if (!theTarget) return false;
var theSlider = theTarget.sliderPanel;
var isOpen = theSlider.style.display!="none";
// if SHIFT-CLICK, dock panel first (see [[MoveablePanelPlugin]])
if (e.shiftKey && config.macros.moveablePanel) config.macros.moveablePanel.dock(theSlider,e);
// toggle label
theTarget.innerHTML=isOpen?theTarget.getAttribute("closedText"):theTarget.getAttribute("openedText");
// toggle tooltip
theTarget.setAttribute("title",isOpen?theTarget.getAttribute("closedTip"):theTarget.getAttribute("openedTip"));
// deferred rendering (if needed)
if (theSlider.getAttribute("rendered")=="false") {
var place=theSlider;
if (theSlider.getAttribute("blockquote")=="true")
place=createTiddlyElement(place,"blockquote");
wikify(theSlider.getAttribute("raw"),place);
theSlider.setAttribute("rendered","true");
}
// show/hide the slider
if(config.options.chkAnimate && (!hasClass(theSlider,'floatingPanel') || config.options.chkFloatingSlidersAnimate))
anim.startAnimating(new Slider(theSlider,!isOpen,e.shiftKey || e.altKey,"none"));
else
theSlider.style.display = isOpen ? "none" : "block";
// reset to default width (might have been changed via plugin code)
theSlider.style.width=theSlider.defaultPanelWidth;
// align floater panel position with target button
if (!isOpen && window.adjustSliderPos) window.adjustSliderPos(theSlider.parentNode,theTarget,theSlider);
// if showing panel, set focus to first 'focus-able' element in panel
if (theSlider.style.display!="none") {
var ctrls=theSlider.getElementsByTagName("*");
for (var c=0; c<ctrls.length; c++) {
var t=ctrls[c].tagName.toLowerCase();
if ((t=="input" && ctrls[c].type!="hidden") || t=="textarea" || t=="select")
{ try{ ctrls[c].focus(); } catch(err){;} break; }
}
}
var cookie=theTarget.sliderCookie;
if (cookie && cookie.length) {
config.options[cookie]=!isOpen;
if (config.options[cookie]!=theTarget.defOpen) window.saveOptionCookie(cookie);
else window.removeCookie(cookie); // remove cookie if slider is in default display state
}
// prevent SHIFT-CLICK from being processed by browser (opens blank window... yuck!)
// prevent clicks *within* a slider button from being processed by browser
// but allow plain click to bubble up to page background (to close transients, if any)
if (e.shiftKey || theTarget!=resolveTarget(e))
{ e.cancelBubble=true; if (e.stopPropagation) e.stopPropagation(); }
Popup.remove(); // close open popup (if any)
return false;
}
//}}}
//{{{
// click in document background closes transient panels
document.nestedSliders_savedOnClick=document.onclick;
document.onclick=function(ev) {
if (!ev) var ev=window.event; var target=resolveTarget(ev);
if (document.nestedSliders_savedOnClick)
var retval=document.nestedSliders_savedOnClick.apply(this,arguments);
// if click was inside a popup... leave transient panels alone
var p=target; while (p) if (hasClass(p,"popup")) break; else p=p.parentNode;
if (p) return retval;
// if click was inside transient panel (or something contained by a transient panel), leave it alone
var p=target; while (p) {
if ((hasClass(p,"floatingPanel")||hasClass(p,"sliderPanel"))&&p.getAttribute("transient")=="true") break;
p=p.parentNode;
}
if (p) return retval;
// otherwise, find and close all transient panels...
var all=document.all?document.all:document.getElementsByTagName("DIV");
for (var i=0; i<all.length; i++) {
// if it is not a transient panel, or the click was on the button that opened this panel, don't close it.
if (all[i].getAttribute("transient")!="true" || all[i].button==target) continue;
// otherwise, if the panel is currently visible, close it by clicking it's button
if (all[i].style.display!="none") window.onClickNestedSlider({target:all[i].button})
if (!hasClass(all[i],"floatingPanel")&&!hasClass(all[i],"sliderPanel")) all[i].style.display="none";
}
return retval;
};
//}}}
//{{{
// adjust floating panel position based on button position
if (window.adjustSliderPos==undefined) window.adjustSliderPos=function(place,btn,panel) {
if (hasClass(panel,"floatingPanel") && !hasClass(panel,"undocked")) {
// see [[MoveablePanelPlugin]] for use of 'undocked'
var rightEdge=document.body.offsetWidth-1;
var panelWidth=panel.offsetWidth;
var left=0;
var top=btn.offsetHeight;
if (place.style.position=="relative" && findPosX(btn)+panelWidth>rightEdge) {
left-=findPosX(btn)+panelWidth-rightEdge; // shift panel relative to button
if (findPosX(btn)+left<0) left=-findPosX(btn); // stay within left edge
}
if (place.style.position!="relative") {
var left=findPosX(btn);
var top=findPosY(btn)+btn.offsetHeight;
var p=place; while (p && !hasClass(p,'floatingPanel')) p=p.parentNode;
if (p) { left-=findPosX(p); top-=findPosY(p); }
if (left+panelWidth>rightEdge) left=rightEdge-panelWidth;
if (left<0) left=0;
}
panel.style.left=left+"px"; panel.style.top=top+"px";
}
}
//}}}
//{{{
// TW2.2+
// hijack Morpher stop handler so sliderPanel/floatingPanel overflow is visible after animation has completed
if (version.major+.1*version.minor+.01*version.revision>=2.2) {
Morpher.prototype.coreStop = Morpher.prototype.stop;
Morpher.prototype.stop = function() {
this.coreStop.apply(this,arguments);
var e=this.element;
if (hasClass(e,"sliderPanel")||hasClass(e,"floatingPanel")) {
// adjust panel overflow and position after animation
e.style.overflow = "visible";
if (window.adjustSliderPos) window.adjustSliderPos(e.parentNode,e.button,e);
}
};
}
//}}}
/***
|Description|Enables Ctrl + S hotkey to save changes (and not browser default action)|
|Version |1.1|
|Author |Yakov Litvin|
|Source |https://github.com/YakovL/TiddlyWiki_QuickSavePlugin/blob/master/QuickSavePlugin.js|
|License |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
***/
//{{{
jQuery(document).on('keydown', null, function($e) {
if(readOnly) return
if($e.ctrlKey && $e.originalEvent.code === 'KeyS') {
saveChanges()
if($e.preventDefault) $e.preventDefault()
return false
}
})
//}}}
<<defineRecipe [[ResponsiveThemePlugin]]
parts:[[ResponsiveThemeSkeleton]] [[ResponsivePageTemplate]] [[ResponsiveStyleSheet]] [[TopLineMenu]]
recipe:'
const pt = parts["ResponsivePageTemplate"][0]
parts["ResponsivePageTemplate"][0] = ""
const ptStartMarker = "!Page Template\n"
const skeleton = parts["ResponsiveThemeSkeleton"][0]
const ptStartIndex = skeleton.indexOf(ptStartMarker) + ptStartMarker.length
parts["ResponsiveThemeSkeleton"][0] = skeleton.substring(0, ptStartIndex) + pt + "\n"
+ skeleton.substring(ptStartIndex)
parts["ResponsiveStyleSheet"][0] = wrapAsCssAdder(parts["ResponsiveStyleSheet"][0], tid.title, "ResponsiveStyleSheet", { dontApply: true })
// a hacky use of wrapAsCssAdder, should be a separate helper instead
parts["TopLineMenu"][0] = wrapAsCssAdder(parts["TopLineMenu"][0], tid.title, "TopLineMenu", { dontApply: true })
'
plugin:true
autoupdate:true
reinstall:true
>>
<!--{{{-->
<header class='header' role='banner'>
<div class='headerForeground'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
</header>
<div id='topLineMenu' refresh='content' tiddler='TopLineMenu'></div>
<div class="body">
<nav id='mainMenu' class='body__nav' role='navigation' refresh='content' tiddler='MainMenu'></nav>
<main id='displayArea' class='body__main' role='main'>
<div id='messageArea' class="messageArea"></div>
<div id='tiddlerDisplay'></div>
</main>
<aside id='sidebar' class='body__sidebar'>
<div id='sidebarOptions' role='navigation' refresh='content' tiddler='SideBarOptions'></div>
<div id='sidebarTabs' role='complementary' refresh='content' force='true' tiddler='SideBarTabs'></div>
</aside>
</div>
<!--}}}-->
/*{{{*/
body {
/* prevent scroll on backstage clock, right? ..better set width by JS instead
overflow-x: hidden; */
/* increased compared to core; should be increased further */
font-size: .8em;
}
#fullContentWrapper {
box-shadow: 0px 1px 4px [[ColorPalette::TertiaryMid]];
position: relative;
/* to position #messageArea, see https://stackoverflow.com/a/67776640/3995261
contain: content; */
max-width: 80em;
margin-inline-start: auto;
margin-inline-end: auto;
}
#backstageWrapper {
position: absolute;
top: 0;
left: 0;
right: 0;
}
#contentWrapper {
min-height: 100vh;
background: [[ColorPalette::Background]];
}
#backstageCloak {
/* from https://css-tricks.com/full-bleed/ */
width: 100vw;
left: 50%;
right: 50%;
margin-left: -50vw;
margin-right: -50vw;
top: 0;
}
#backstagePanel {
width: unset;
/* 0 auto doesn't work here */
margin: 0;
}
.header {
background: -moz-linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
background: linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
}
.headerForeground {
padding: 3em 1em 1em;
position: relative;
text-shadow: -1px -1px [[ColorPalette::Foreground]];
}
.siteTitle {
/* decreased compared to the core; may be decreased more */
font-size: 2.5em;
}
@media (max-width: 768px) {
.siteTitle { font-size: 1.8em; }
}
#topLineMenu {
position: sticky;
top: 0;
z-index: 1;
box-shadow: 0px 1px 4px [[ColorPalette::TertiaryMid]];
background: [[ColorPalette::Background]];
padding: .5em;
/*padding-block-start: .5em;
padding-inline-start: .5em;
inline-size: max-content;*/
}
.topLineMenu__wrapper {
display: flex;
align-items: center;
}
.topLineMenu__center {
flex: 1;
text-align: center;
padding: 0 1em;
}
#topLineMenu a.button {
padding: 0.3em 0.5em;
}
/* remake columns using flex */
.body {
display: flex;
/* to allow MainMenu hover */
position: relative;
}
/* undoing styles for body__nav, body__sidebar; body__main */
#mainMenu, #sidebar {
position: relative;
}
#mainMenu {
padding: 1em;
}
#sidebar {
margin-inline-start: 1em;
padding-block-start: 1em;
font-size: 1em;
}
#sidebarOptions {
/* overwriting defaults */
padding-top: 0;
}
#displayArea {
/* prevent stretching the body horizontally (by code blocks etc), idea from:
https://chaiyihein.medium.com/fixing-flexbox-child-element-overflows-the-power-of-min-width-1c7af87314da */
min-width: 0;
margin: 0;
}
.body__main {
flex: 1;
}
@media (max-width: 768px) {
#mainMenu, #sidebar {
display: none;
position: absolute;
top: 0;
bottom: 0;
background: [[ColorPalette::Background]];
box-shadow: 1px 2px 5px [[ColorPalette::TertiaryMid]];
/* height: ?? (full?) stick to the top? */
}
#mainMenu {
left: 0;
}
#sidebar {
right: 0;
}
}
.button_button {
background: [[ColorPalette::Background]];
border: 1px solid [[ColorPalette::SecondaryMid]];
padding: 0;
line-height: 1;
border-radius: 5px;
}
.button_button svg {
vertical-align: middle;
/* accessability */
min-width: 24px;
min-height: 24px;
}
.button__shape {
fill: none;
stroke: [[ColorPalette::Foreground]];
stroke-width: 3;
}
[[StyleSheet]]
/*}}}*/
/***
|Name |ResponsiveThemePlugin|
|Description |A plugin + theme to make TW responsive (desktop/mobile)|
|Version |0.6.3|
|Author |Yakov Litvin|
|Source |https://responsive.tiddlyhost.com#ResponsiveThemePlugin (won't work for checking/updating via EEP, will be changed)|
|License |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
|PageTemplate|ResponsiveThemePlugin##Page Template|
|StyleSheet |ResponsiveStyleSheet|
|Other config|[[TopLineMenuMiddle]] TopLineMenu|
!Installation & configuration
Install this as a usual plugin: copy, tag with {{{systemConfig}}}, save, reload. The only difference is that after the first reload, MarkupPreHead will be adjusted automatically, and to apply it, you have to save and reload again.
These parts can be edited to customize the theme:
* ResponsiveStyleSheet, StyleSheet are both applied, customizing the latter is preferrable
* TopLineMenu, TopLineMenuMiddle define the content in the top menu, customizing the latter (empty by default) is preferrable
As this is both a plugin and a theme, it's currently not possible to combine this with another theme.
!Page Template
<!--{{{-->
<header class='header' role='banner'>
<div class='headerForeground'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
</header>
<div id='topLineMenu' refresh='content' tiddler='TopLineMenu'></div>
<div class="body">
<nav id='mainMenu' class='body__nav' role='navigation' refresh='content' tiddler='MainMenu'></nav>
<main id='displayArea' class='body__main' role='main'>
<div id='messageArea' class="messageArea"></div>
<div id='tiddlerDisplay'></div>
</main>
<aside id='sidebar' class='body__sidebar'>
<div id='sidebarOptions' role='navigation' refresh='content' tiddler='SideBarOptions'></div>
<div id='sidebarTabs' role='complementary' refresh='content' force='true' tiddler='SideBarTabs'></div>
</aside>
</div>
<!--}}}-->
!Code
***/
//{{{
config.options.txtTheme = 'ResponsiveThemePlugin'
// a fix: when name is set to TiddlerTitle##SectionName, should still find it
// TODO: move this to the core, add version check here
TiddlyWiki.prototype.notify = function(title, doBlanket)
{
if(!this.notificationLevel) {
for(var i = 0; i < this.namedNotifications.length; i++) {
var n = this.namedNotifications[i];
var nTitle = n.name
if(nTitle) {
var separatorIndex = nTitle.indexOf(config.textPrimitives.sectionSeparator)
if(separatorIndex > -1) nTitle = nTitle.substring(0, separatorIndex)
}
if((n.name == null && doBlanket) || (nTitle == title))
n.notify(n.name || title);
}
}
};
var isInstalled = !!document.getElementById('fullContentWrapper')
if(!isInstalled) {
// Wrap backstage elements into a single element #backstageWrapper,
// wrap #backstageWrapper and #contentWrapper into #fullContentWrapper
// (as contentWrapper's innerHTML is defined in refreshPageTemplate, we can't just move backstageWrapper inside contentWrapper, so we create a common wrapper instead)
const commonWrapper = createTiddlyElement(null, 'div', 'fullContentWrapper')
const backstageWrapper = createTiddlyElement(commonWrapper, 'div', 'backstageWrapper')
const contentWrapper = document.getElementById('contentWrapper')
contentWrapper.parentNode.insertBefore(commonWrapper, contentWrapper)
commonWrapper.appendChild(contentWrapper)
for(let id of ['backstageButton', 'backstageArea', 'backstage', 'backstageCloak']) {
// move inside wrapper
backstageWrapper.appendChild(document.getElementById(id))
}
// make sure editing StyleSheet for custom styles causes updating css at once
store.addNotification("StyleSheet", function(title, doc) {
refreshStyles("ResponsiveStyleSheet", doc)
})
}
const viewportHtml = '<meta name="viewport" content="width=device-width, initial-scale=1" />'
const preHeadMarkup = store.getTiddlerText('MarkupPreHead')
if(preHeadMarkup.indexOf(viewportHtml) == -1) {
const preHeadTiddler = store.fetchTiddler('MarkupPreHead') || new Tiddler('MarkupPreHead')
const closeMarkerPosition = preHeadMarkup.indexOf('<!--}}}-->')
// TODO: test
preHeadTiddler.text = closeMarkerPosition == -1 ? preHeadMarkup + '\n' + viewportHtml
: preHeadMarkup.substring(0, closeMarkerPosition) + viewportHtml + '\n'
+ preHeadMarkup.substring(closeMarkerPosition)
store.saveTiddler(preHeadTiddler)
}
// close menus on click elsewhere
jQuery('body').on('click', function(event) {
// except on mobile
if(!window.matchMedia || !window.matchMedia("(max-width: 768px)").matches) return
const $mainMenu = jQuery('#mainMenu')
const $sidebar = jQuery('#sidebar')
if(!isDescendant(event.target, $mainMenu[0])) $mainMenu.hide()
if(!isDescendant(event.target, $sidebar[0])) $sidebar.hide()
})
//}}}
// /%
/***
!ResponsiveStyleSheet
***/
///*{{{*/
//body {
// /* prevent scroll on backstage clock, right? ..better set width by JS instead
// overflow-x: hidden; */
// /* increased compared to core; should be increased further */
// font-size: .8em;
//}
//
//#fullContentWrapper {
// box-shadow: 0px 1px 4px [[ColorPalette::TertiaryMid]];
// position: relative;
// /* to position #messageArea, see https://stackoverflow.com/a/67776640/3995261
// contain: content; */
//
// max-width: 80em;
// margin-inline-start: auto;
// margin-inline-end: auto;
//}
//
//#backstageWrapper {
// position: absolute;
// top: 0;
// left: 0;
// right: 0;
//}
//#contentWrapper {
// min-height: 100vh;
// background: [[ColorPalette::Background]];
//}
//#backstageCloak {
// /* from https://css-tricks.com/full-bleed/ */
// width: 100vw;
// left: 50%;
// right: 50%;
// margin-left: -50vw;
// margin-right: -50vw;
// top: 0;
//}
//
//#backstagePanel {
// width: unset;
// /* 0 auto doesn't work here */
// margin: 0;
//}
//
//.header {
// background: -moz-linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
// background: linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
//}
//.headerForeground {
// padding: 3em 1em 1em;
// position: relative;
// text-shadow: -1px -1px [[ColorPalette::Foreground]];
//}
//.siteTitle {
// /* decreased compared to the core; may be decreased more */
// font-size: 2.5em;
//}
//@media (max-width: 768px) {
// .siteTitle { font-size: 1.8em; }
//}
//
//#topLineMenu {
// position: sticky;
// top: 0;
// z-index: 1;
// box-shadow: 0px 1px 4px [[ColorPalette::TertiaryMid]];
// background: [[ColorPalette::Background]];
// padding: .5em;
///*padding-block-start: .5em;
//padding-inline-start: .5em;
//inline-size: max-content;*/
//}
//.topLineMenu__wrapper {
// display: flex;
// align-items: center;
//}
//.topLineMenu__center {
// flex: 1;
// text-align: center;
// padding: 0 1em;
//}
//#topLineMenu a.button {
// padding: 0.3em 0.5em;
//}
//
///* remake columns using flex */
//.body {
// display: flex;
// /* to allow MainMenu hover */
// position: relative;
//}
///* undoing styles for body__nav, body__sidebar; body__main */
//#mainMenu, #sidebar {
// position: relative;
//}
//#mainMenu {
// padding: 1em;
//}
//#sidebar {
// margin-inline-start: 1em;
// padding-block-start: 1em;
// font-size: 1em;
//}
//#sidebarOptions {
// /* overwriting defaults */
// padding-top: 0;
//}
//#displayArea {
// /* prevent stretching the body horizontally (by code blocks etc), idea from:
// https://chaiyihein.medium.com/fixing-flexbox-child-element-overflows-the-power-of-min-width-1c7af87314da */
// min-width: 0;
// margin: 0;
//}
//.body__main {
// flex: 1;
//}
//
//@media (max-width: 768px) {
// #mainMenu, #sidebar {
// display: none;
// position: absolute;
// top: 0;
// bottom: 0;
// background: [[ColorPalette::Background]];
// box-shadow: 1px 2px 5px [[ColorPalette::TertiaryMid]];
// /* height: ?? (full?) stick to the top? */
// }
// #mainMenu {
// left: 0;
// }
// #sidebar {
// right: 0;
// }
//}
//
//.button_button {
// background: [[ColorPalette::Background]];
// border: 1px solid [[ColorPalette::SecondaryMid]];
// padding: 0;
// line-height: 1;
// border-radius: 5px;
//}
//.button_button svg {
// vertical-align: middle;
// /* accessability */
// min-width: 24px;
// min-height: 24px;
//}
//.button__shape {
// fill: none;
// stroke: [[ColorPalette::Foreground]];
// stroke-width: 3;
//}
//
//[[StyleSheet]]
///*}}}*/
/***
!end of ResponsiveStyleSheet
***/
// %/ //
//{{{
;(function() {
var cssName = "ResponsiveStyleSheet",
css = store.getTiddlerText("ResponsiveThemePlugin" + "##" + cssName).replace(/^\/\//gm, "");
css = css.substring(5, css.length - 5); // cut leading \n***/ and trailing /***\n of the section
config.shadowTiddlers[cssName] = css;
})();
//}}}
// /%
/***
!TopLineMenu
***/
//{{topLineMenu__wrapper{
//<html>
// <button class="button button_button" title="toggle main menu" onclick='
// const $nav = jQuery(".body__nav")
// $nav.is(":hidden") ? $nav.show() : $nav.hide()
// if(event && event.stopPropagation) event.stopPropagation()
// '>
// <svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 100 100">
// <rect x="20" width="60" y="20" height="10" rx="5" ry="5" class="button__shape" />
// <rect x="20" width="60" y="45" height="10" rx="5" ry="5" class="button__shape" />
// <rect x="20" width="60" y="70" height="10" rx="5" ry="5" class="button__shape" />
// </svg>
// </button>
//</html>
//{{topLineMenu__center{
//<<tiddler [[TopLineMenuMiddle]]>>}}}
//<html>
// <button class="button button_button" title="toggle sidebar" onclick='
// const $nav = jQuery(".body__sidebar")
// $nav.is(":hidden") ? $nav.show() : $nav.hide()
// if(event && event.stopPropagation) event.stopPropagation()
// '>
// <svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 100 100">
// <circle cx="50" cy="25" r="7" class="button__shape" />
// <circle cx="50" cy="50" r="7" class="button__shape" />
// <circle cx="50" cy="75" r="7" class="button__shape" />
// </svg>
// </button>
//</html>
//}}}
/***
!end of TopLineMenu
***/
// %/ //
//{{{
;(function() {
var cssName = "TopLineMenu",
css = store.getTiddlerText("ResponsiveThemePlugin" + "##" + cssName).replace(/^\/\//gm, "");
css = css.substring(5, css.length - 5); // cut leading \n***/ and trailing /***\n of the section
config.shadowTiddlers[cssName] = css;
})();
//}}}
/***
|Name |ResponsiveThemePlugin|
|Description |A plugin + theme to make TW responsive (desktop/mobile)|
|Version |0.6.3|
|Author |Yakov Litvin|
|Source |https://responsive.tiddlyhost.com#ResponsiveThemePlugin (won't work for checking/updating via EEP, will be changed)|
|License |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
|PageTemplate|ResponsiveThemePlugin##Page Template|
|StyleSheet |ResponsiveStyleSheet|
|Other config|[[TopLineMenuMiddle]] TopLineMenu|
!Installation & configuration
Install this as a usual plugin: copy, tag with {{{systemConfig}}}, save, reload. The only difference is that after the first reload, MarkupPreHead will be adjusted automatically, and to apply it, you have to save and reload again.
These parts can be edited to customize the theme:
* ResponsiveStyleSheet, StyleSheet are both applied, customizing the latter is preferrable
* TopLineMenu, TopLineMenuMiddle define the content in the top menu, customizing the latter (empty by default) is preferrable
As this is both a plugin and a theme, it's currently not possible to combine this with another theme.
!Page Template
!Code
***/
//{{{
config.options.txtTheme = 'ResponsiveThemePlugin'
// a fix: when name is set to TiddlerTitle##SectionName, should still find it
// TODO: move this to the core, add version check here
TiddlyWiki.prototype.notify = function(title, doBlanket)
{
if(!this.notificationLevel) {
for(var i = 0; i < this.namedNotifications.length; i++) {
var n = this.namedNotifications[i];
var nTitle = n.name
if(nTitle) {
var separatorIndex = nTitle.indexOf(config.textPrimitives.sectionSeparator)
if(separatorIndex > -1) nTitle = nTitle.substring(0, separatorIndex)
}
if((n.name == null && doBlanket) || (nTitle == title))
n.notify(n.name || title);
}
}
};
var isInstalled = !!document.getElementById('fullContentWrapper')
if(!isInstalled) {
// Wrap backstage elements into a single element #backstageWrapper,
// wrap #backstageWrapper and #contentWrapper into #fullContentWrapper
// (as contentWrapper's innerHTML is defined in refreshPageTemplate, we can't just move backstageWrapper inside contentWrapper, so we create a common wrapper instead)
const commonWrapper = createTiddlyElement(null, 'div', 'fullContentWrapper')
const backstageWrapper = createTiddlyElement(commonWrapper, 'div', 'backstageWrapper')
const contentWrapper = document.getElementById('contentWrapper')
contentWrapper.parentNode.insertBefore(commonWrapper, contentWrapper)
commonWrapper.appendChild(contentWrapper)
for(let id of ['backstageButton', 'backstageArea', 'backstage', 'backstageCloak']) {
// move inside wrapper
backstageWrapper.appendChild(document.getElementById(id))
}
// make sure editing StyleSheet for custom styles causes updating css at once
store.addNotification("StyleSheet", function(title, doc) {
refreshStyles("ResponsiveStyleSheet", doc)
})
}
const viewportHtml = '<meta name="viewport" content="width=device-width, initial-scale=1" />'
const preHeadMarkup = store.getTiddlerText('MarkupPreHead')
if(preHeadMarkup.indexOf(viewportHtml) == -1) {
const preHeadTiddler = store.fetchTiddler('MarkupPreHead') || new Tiddler('MarkupPreHead')
const closeMarkerPosition = preHeadMarkup.indexOf('<!--}}}-->')
// TODO: test
preHeadTiddler.text = closeMarkerPosition == -1 ? preHeadMarkup + '\n' + viewportHtml
: preHeadMarkup.substring(0, closeMarkerPosition) + viewportHtml + '\n'
+ preHeadMarkup.substring(closeMarkerPosition)
store.saveTiddler(preHeadTiddler)
}
// close menus on click elsewhere
jQuery('body').on('click', function(event) {
// except on mobile
if(!window.matchMedia || !window.matchMedia("(max-width: 768px)").matches) return
const $mainMenu = jQuery('#mainMenu')
const $sidebar = jQuery('#sidebar')
if(!isDescendant(event.target, $mainMenu[0])) $mainMenu.hide()
if(!isDescendant(event.target, $sidebar[0])) $sidebar.hide()
})
//}}}
/***
|Description|highlights saving button (bold red by default) and the document title (adds a leading "*") when there are unsaved changes (also toggles {{{hasUnsavedChanges}}} class of the root element for hackability)|
|Version |1.5.1|
|Author |Yakov Litvin|
|Source |https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js|
|License |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
<<option chkShowDirtyStory>> show unsaved if any tiddler is opened for editing
Styles applied to unsaved TW can be adjusted in StyleSheetUnsaved
***/
//{{{
config.macros.showDirtyPlugin = {
// styles that highlight save button when there's something to save
showDirtyCss: ".saveChangesButton { font-weight: bold; color: red !important; }",
styleSheetName: "suggestSavingOnDirty",
containerClassName: "hasUnsavedChanges",
showDrity: function(dirty) {
const css = store.getTiddlerText('StyleSheetUnsaved')
if(dirty) {
jQuery('html').addClass(this.containerClassName)
setStylesheet(css, this.styleSheetName)
document.title = "*" + getPageTitle()
} else {
jQuery('html').removeClass(this.containerClassName)
removeStyleSheet(this.styleSheetName)
document.title = getPageTitle()
}
},
checkDirty: function() {
return store.isDirty() ||
(config.options.chkShowDirtyStory && story.areAnyDirty())
},
init: function() {
config.shadowTiddlers.StyleSheetUnsaved = this.showDirtyCss
// add the "saveChangesButton" class to the save changes button
config.macros.saveChanges.SCM_orig_handler = config.macros.saveChanges.handler
config.macros.saveChanges.handler = function(place, macroName, params) {
this.SCM_orig_handler.apply(this, arguments)
place.lastChild.classList.add("saveChangesButton")
}
// regularly check and indicate unsaved
setInterval(function() {
const isDirty = config.macros.showDirtyPlugin.checkDirty()
config.macros.showDirtyPlugin.showDrity(isDirty)
}, 500)
}
}
//}}}
a temporal dev and demo polygon; may get to the core at some point
txtUserName: Yakov Litvin
chkSaveBackups: false
chkAutoSave: true
chkInsertTabs: true
/***
|Version|0.4.0|
|Source|https://yllab.tiddlyhost.com#ThostGoodSavingPlugin|
|Requires|ThostUploadPlugin|
|~|should be run after ThostUploadPlugin to update SideBarOptions properly|
should we make "save to tiddlyhost" have the same label as the usual button ("save changes")?
* test download: <<downloadMain>>
* test upload: <<uploadMain>>
***/
//{{{
// keep for downloading functionality
config.orig_tHost_saveChanges = window.saveChanges
window.saveChanges = function(onlyIfDirty, tiddlers) {
config.macros.thostUpload.action()
}
config.macros.downloadMain = {
handler: function(place, macroName, params, wikifier, paramString, tiddler) {
const label = "save from web"
const tooltip = "download this TiddlyWiki"
createTiddlyButton(place, label, tooltip, function() {
config.orig_tHost_saveChanges()
})
}
}
config.macros.uploadMain = {
handler: function(place, macroName, params, wikifier, paramString, tiddler) {
const label = "upload"
const tooltip = "upload a TiddlyWiki (a backup) – it will substitute the current one; to see the changes, reload the page"
createTiddlyButton(place, label, tooltip, function() {
config.macros.uploadMain.upload()
})
},
getFileText: async function() {
if(window.showOpenFilePicker) {
const pickerOptions = { types: [ { accept: { 'text/html': ['.html', '.hta'] } } ] }
const handles = await window.showOpenFilePicker(pickerOptions)
if(!handles[0]) return null
const file = await handles[0].getFile()
return await file.text()
} else {
const tempFileInput = document.createElement('input')
tempFileInput.type = "file"
tempFileInput.style.display = "none"
document.body.appendChild(tempFileInput)
const cleanUp = () => document.body.removeChild(tempFileInput)
return new Promise((resolve, reject) => {
tempFileInput.addEventListener('change', function handleFileSelect(event) {
const selectedFile = event.target.files[0]
if(selectedFile) {
const reader = new FileReader()
reader.onload = function (fileEvent) {
cleanUp()
resolve(fileEvent.target.result)
}
reader.readAsText(selectedFile)
} else {
cleanUp()
resolve(null)
}
})
tempFileInput.click()
})
}
},
upload: async function() {
const newHtml = await this.getFileText()
if(!newHtml) return alert("Looks like the file were not picked or it's empty")
// this will help if one tries to upload a non-TW file
const posDiv = locateStoreArea(newHtml)
if((posDiv[0] == -1) || (posDiv[1] == -1)) return alert(config.messages.invalidFileError.format([localPath]))
displayMessage("Started uploading...")
const uploadParams = ['https://' + config.options.txtThostSiteName + '.tiddlyhost.com']
bidix.thostUpload.httpUpload(uploadParams, newHtml, function reportResult(status, params, responseText, url, xhr) {
if(status) {
displayMessage(bidix.thostUpload.messages.mainSaved)
store.setDirty(false)
displayMessage("To see the changes applied, reload the page")
} else {
alert(bidix.thostUpload.messages.mainFailed)
displayMessage(bidix.thostUpload.messages.mainFailed)
}
}, uploadParams)
}
}
// TODO: decide if using backstage instead is more proper
config.shadowTiddlers.SideBarOptions = config.shadowTiddlers.SideBarOptions.replace(
/(<<saveChanges>><<thostUpload>>)/,
"<<thostUpload>><<downloadMain>>")
//}}}
/***
|Name |ThostUploadPlugin |
|Description |Support saving to Tiddlyhost.com |
|Version |1.0.1 |
|Date |March 06, 2021 |
|Source |https://github.com/tiddlyhost/tiddlyhost-com/tree/main/rails/tw_content/plugins |
|Author |BidiX, Simon Baird, Yakov Litvin |
|License |BSD open source license |
|~CoreVersion |2.9.2 |
***/
//{{{
version.extensions.ThostUploadPlugin = { major: 1, minor: 0, revision: 1 };
//
// Environment
//
if (!window.bidix) window.bidix = {};
// To change these defaults, create a tiddler named "ThostOptions" with tag
// "systemConfig" and the following content:
// window.bidix = { "editModeAlways": false, "uploadButtonAlways": false };
// Set false if you want the chkHttpReadOnly cookie to decide whether to
// render in read-only mode or edit mode when you're not logged in or when
// the site is being viewed by others. Default true.
if (!("editModeAlways" in bidix)) { bidix.editModeAlways = true; }
// Set false to hide the "upload to tiddlyhost" button when you're not logged
// in or when the site is being viewed by others. Default true.
if (!("uploadButtonAlways" in bidix)) { bidix.uploadButtonAlways = true; }
// For debugging. Default false.
if (!("debugMode" in bidix)) { bidix.debugMode = false; }
//
// Upload Macro
//
config.macros.thostUpload = {
handler: function(place,macroName,params) {
createTiddlyButton(place, "save to tiddlyhost",
"save this TiddlyWiki to a site on Tiddlyhost.com",
this.action, null, null, this.accessKey);
},
action: function(params) {
var siteName = config.options.txtThostSiteName.trim();
if (!siteName) {
alert("Tiddlyhost site name is missing!");
clearMessage();
}
else {
bidix.thostUpload.uploadChanges('https://' + siteName + '.tiddlyhost.com');
}
return false;
}
};
//
// Upload functions
//
if (!bidix.thostUpload) bidix.thostUpload = {};
if (!bidix.thostUpload.messages) bidix.thostUpload.messages = {
invalidFileError: "The original file '%0' does not appear to be a valid TiddlyWiki",
mainSaved: "Main TiddlyWiki file uploaded",
mainFailed: "Failed to upload main TiddlyWiki file. Your changes have not been saved",
loadOriginalHttpPostError: "Can't get original file",
aboutToSaveOnHttpPost: 'About to upload on %0 ...',
storePhpNotFound: "The store script '%0' was not found."
};
bidix.thostUpload.uploadChanges = function(storeUrl) {
var callback = function(status, uploadParams, original, url, xhr) {
if (!status) {
displayMessage(bidix.thostUpload.messages.loadOriginalHttpPostError);
return;
}
if (bidix.debugMode) {
alert(original.substr(0,500)+"\n...");
}
var posDiv = locateStoreArea(original);
if ((posDiv[0] == -1) || (posDiv[1] == -1)) {
alert(config.messages.invalidFileError.format([localPath]));
return;
}
bidix.thostUpload.uploadMain(uploadParams, original, posDiv);
};
clearMessage();
// get original
var uploadParams = [storeUrl];
var originalPath = document.location.toString();
var dest = 'index.html';
displayMessage(bidix.thostUpload.messages.aboutToSaveOnHttpPost.format([dest]));
if (bidix.debugMode) {
alert("about to execute Http - GET on "+originalPath);
}
var r = doHttp("GET", originalPath, null, null, null, null, callback, uploadParams, null);
if (typeof r == "string") {
displayMessage(r);
}
return r;
};
bidix.thostUpload.uploadMain = function(uploadParams, original, posDiv) {
var callback = function(status, params, responseText, url, xhr) {
if (status) {
displayMessage(bidix.thostUpload.messages.mainSaved);
store.setDirty(false);
}
else {
alert(bidix.thostUpload.messages.mainFailed);
displayMessage(bidix.thostUpload.messages.mainFailed);
}
};
var revised = updateOriginal(original, posDiv);
bidix.thostUpload.httpUpload(uploadParams, revised, callback, uploadParams);
};
bidix.thostUpload.httpUpload = function(uploadParams, data, callback, params) {
var localCallback = function(status, params, responseText, url, xhr) {
if (xhr.status == 404) {
alert(bidix.thostUpload.messages.storePhpNotFound.format([url]));
}
var saveNotOk = responseText.charAt(0) != '0';
if (bidix.debugMode || saveNotOk) {
alert(responseText);
}
if (saveNotOk) {
status = null;
}
callback(status, params, responseText, url, xhr);
};
// do httpUpload
var boundary = "---------------------------"+"AaB03x";
var uploadFormName = "UploadPlugin";
// compose headers data
var sheader = "";
sheader += "--" + boundary + "\r\nContent-disposition: form-data; name=\"";
sheader += uploadFormName +"\"\r\n\r\n";
sheader += "backupDir=x" +
";user=x" +
";password=x" +
";uploaddir=x";
if (bidix.debugMode) {
sheader += ";debug=1";
}
sheader += ";;\r\n";
sheader += "\r\n" + "--" + boundary + "\r\n";
sheader += "Content-disposition: form-data; name=\"userfile\"; filename=\"index.html\"\r\n";
sheader += "Content-Type: text/html;charset=UTF-8" + "\r\n";
sheader += "Content-Length: " + data.length + "\r\n\r\n";
// compose trailer data
var strailer = "";
strailer = "\r\n--" + boundary + "--\r\n";
data = sheader + data + strailer;
if (bidix.debugMode) {
alert("about to execute Http - POST on " + uploadParams[0]+ "\n with \n" + data.substr(0,500) + " ... ");
}
var r = doHttp("POST", uploadParams[0], data,
"multipart/form-data; ;charset=UTF-8; boundary=" + boundary, 'x','x', localCallback, params, null);
if (typeof r == "string") {
displayMessage(r);
}
return r;
};
// a fix for versions before 2.9.2 (updateOriginal used conversions irrelevant for Tiddlyhost)
convertUnicodeToFileFormat = function(s) { return s };
//
// Site config
//
bidix.initOption = function(name,value) {
if (!config.options[name]) {
config.options[name] = value;
}
};
merge(config.optionsDesc, {
txtThostSiteName: "Site name for uploads to Tiddlyhost.com",
});
bidix.initOption('txtThostSiteName','responsive');
//
// Tiddlyhost stuff
//
bidix.ownerLoggedIn = (config.shadowTiddlers.TiddlyHostIsLoggedIn &&
config.shadowTiddlers.TiddlyHostIsLoggedIn == "yes")
if (bidix.editModeAlways || bidix.ownerLoggedIn) {
// If user is logged in to Tiddlyhost and viewing their own site then
// we disregard the original value of the chkHttpReadOnly cookie
config.options.chkHttpReadOnly = false
// window.readOnly gets set before plugins are loaded, so we need to
// set it here to make sure TW is editable, unlike window.showBackstage
// which is set after
window.readOnly = false
}
if (bidix.uploadButtonAlways || bidix.ownerLoggedIn) {
// Add the 'save to tiddlyhost' button after the regular save button
config.shadowTiddlers.SideBarOptions = config.shadowTiddlers.SideBarOptions
.replace(/(<<saveChanges>>)/,"$1<<thostUpload>>");
}
//}}}
{{topLineMenu__wrapper{
<html>
<button class="button button_button" title="toggle main menu" onclick='
const $nav = jQuery(".body__nav")
$nav.is(":hidden") ? $nav.show() : $nav.hide()
if(event && event.stopPropagation) event.stopPropagation()
'>
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 100 100">
<rect x="20" width="60" y="20" height="10" rx="5" ry="5" class="button__shape" />
<rect x="20" width="60" y="45" height="10" rx="5" ry="5" class="button__shape" />
<rect x="20" width="60" y="70" height="10" rx="5" ry="5" class="button__shape" />
</svg>
</button>
</html>
{{topLineMenu__center{
<<tiddler [[TopLineMenuMiddle]]>>}}}
<html>
<button class="button button_button" title="toggle sidebar" onclick='
const $nav = jQuery(".body__sidebar")
$nav.is(":hidden") ? $nav.show() : $nav.hide()
if(event && event.stopPropagation) event.stopPropagation()
'>
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 100 100">
<circle cx="50" cy="25" r="7" class="button__shape" />
<circle cx="50" cy="50" r="7" class="button__shape" />
<circle cx="50" cy="75" r="7" class="button__shape" />
</svg>
</button>
</html>
}}}
<<search>> <<darkMode label:"☀️/🌘">>
|Source |https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLExtensionsCollection.txt|
|Version|0.2.5|
//{{{
[
{
"url": "https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLPrereleasesAndExperimentsCollection.txt",
"description": "Pre-releases and experiments by Yakov Litvin",
"type": "collection"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_EncryptionPlugin/blob/master/EncryptionPlugin.js",
"description": "Save selected parts of content with encryption"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_TwFormulaPlugin/blob/master/TwFormulaPlugin.js",
"description": "Render beautiful formulas using LaTeX syntax (also provides WYSIWYGish editing with MathQuill)"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_GraphTools/blob/master/VisGraphPlugin.js",
"description": "View and edit graphs in a WYSIWYGish manner with the <<graph>> macro (see additional installation steps inside the plugin)"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_JumpKeysPlugin/blob/master/JumpKeysPlugin.js",
"description": "Jump between tiddlers and do more via hotkeys and a UI similar to what browsers use for tabs"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_QuickSavePlugin/blob/master/QuickSavePlugin.js",
"description": "Save changes via Ctrl + S hotkey (without getting browser default action)"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_ContinuousSavingPlugin/blob/main/ContinuousSavingPlugin.js",
"description": "Makes loading and saving work via just one file picking per session"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js",
"description": "See when TW has unsaved changes: in the tab/window title (adds '*'), on the saveChanges button (bold red), and more"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/DarkModePlugin.js",
"description": "Introduces \"dark mode\" (styles) and switching it by the darkMode macro and operating system settings"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_TiddlerInFilePlugin/blob/master/TiddlerInFilePlugin.js",
"description": "Store specific tiddlers as external files and more"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_ExtraFilters/blob/master/ExtraFiltersPlugin.js",
"description": "Various additional filters, including 'all', 'and', 'not', 'tagTree', 'unclassified', 'taggedOnly', 'hasPart', 'from', 'sortByText', and more"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_FromPlaceToPlacePlugin/blob/master/FromPlaceToPlacePlugin.js",
"description": "Open a tiddler or a page in place of the current one (as opposed to opening in addition) via hotkeys"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_SimplifiedUpgradingPlugin/blob/master/SimplifiedUpgradingPlugin.js",
"description": "Fixes core upgrading for (at least) Timimi or MTS 1.7.0 and above, adds optional upgrade autocheck on start and more"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsExplorerPlugin.js",
"description": "Notifies about extensions updates, introduces a macro and a backstage button to explore, install and update extensions"
},
{
"url": "https://yakovl.github.io/TiddlyWiki_SharedTiddlersPlugin/#SharedTiddlersPlugin",
"description": "Allows to use tiddlers from other TiddlyWikis (with or without importing them)"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_ImageGalleries/blob/master/FancyBox2Plugin.js",
"description": "Adds a macro to create image galleries"
},
{
"url": "https://github.com/YakovL/TiddlyWiki_MarkdeepPlugin/blob/master/MarkdeepPlugin.js",
"description": "Create diagrams using the Markdeep syntax"
}
]
//}}}