Initial commit cherry picked from #8702

This commit is contained in:
Jeremy Ruston
2026-02-06 14:11:05 +00:00
parent 1bbb7fd53b
commit 32405c3a71
7 changed files with 320 additions and 22 deletions
+116
View File
@@ -0,0 +1,116 @@
/*\
title: $:/core/modules/background-actions.js
type: application/javascript
module-type: global
Class to dispatch actions when filters change
\*/
"use strict";
class BackgroundActionDispatcher {
constructor(filterTracker, wiki) {
this.filterTracker = filterTracker;
this.wiki = wiki;
this.nextTrackedFilterId = 1;
this.trackedFilters = new Map(); // Use Map for better key management
// Track the filter for the background actions
this.filterTracker.track({
filterString: "[all[tiddlers+shadows]tag[$:/tags/BackgroundAction]!is[draft]]",
fnEnter: title => this.trackFilter(title),
fnLeave: (title, enterValue) => this.untrackFilter(enterValue),
fnChange: (title, enterValue) => {
this.untrackFilter(enterValue);
return this.trackFilter(title);
},
fnProcess: changes => this.process(changes)
});
}
trackFilter(title) {
const tiddler = this.wiki.getTiddler(title);
const id = this.nextTrackedFilterId++;
const tracker = new BackgroundActionTracker({
wiki: this.wiki,
title,
trackFilter: tiddler.fields["track-filter"],
actions: tiddler.fields.text
});
this.trackedFilters.set(id, tracker);
return id;
}
untrackFilter(enterValue) {
const tracker = this.trackedFilters.get(enterValue);
if(tracker) {
tracker.destroy();
}
this.trackedFilters.delete(enterValue);
}
process(changes) {
for(const tracker of this.trackedFilters.values()) {
tracker.process(changes);
}
}
}
/*
Represents an individual tracked filter. Options include:
wiki: wiki to use
title: title of the tiddler being tracked
trackFilter: filter string to track changes
actions: actions to be executed when the filter changes
*/
class BackgroundActionTracker {
constructor({wiki, title, trackFilter, actions}) {
this.wiki = wiki;
this.title = title;
this.trackFilter = trackFilter;
this.actions = actions;
this.filterTracker = new $tw.FilterTracker(this.wiki);
this.hasChanged = false;
this.trackerID = this.filterTracker.track({
filterString: this.trackFilter,
fnEnter: () => { this.hasChanged = true; },
fnLeave: () => { this.hasChanged = true; },
fnProcess: changes => {
if(this.hasChanged) {
this.hasChanged = false;
console.log("Processing background action", this.title);
const tiddler = this.wiki.getTiddler(this.title);
let doActions = true;
if(tiddler && tiddler.fields.platforms) {
doActions = false;
const platforms = $tw.utils.parseStringArray(tiddler.fields.platforms);
if(($tw.browser && platforms.includes("browser")) || ($tw.node && platforms.includes("node"))) {
doActions = true;
}
}
if(doActions) {
this.wiki.invokeActionString(
this.actions,
null,
{
currentTiddler: this.title
},{
parentWidget: $tw.rootWidget
}
);
}
}
}
});
}
process(changes) {
this.filterTracker.handleChangeEvent(changes);
}
destroy() {
this.filterTracker.untrack(this.trackerID);
}
}
exports.BackgroundActionDispatcher = BackgroundActionDispatcher;
+106
View File
@@ -0,0 +1,106 @@
/*\
title: $:/core/modules/filter-tracker.js
type: application/javascript
module-type: global
Class to track the results of a filter string
\*/
"use strict";
class FilterTracker {
constructor(wiki) {
this.wiki = wiki;
this.trackers = new Map();
this.nextTrackerId = 1;
}
handleChangeEvent(changes) {
this.processTrackers();
this.processChanges(changes);
}
/*
Add a tracker to the filter tracker. Returns null if any of the parameters are invalid, or a tracker id if the tracker was added successfully. Options include:
filterString: the filter string to track
fnEnter: function to call when a title enters the filter results. Called even if the tiddler does not actually exist. Called as (title), and should return a truthy value that is stored in the tracker as the "enterValue"
fnLeave: function to call when a title leaves the filter results. Called as (title,enterValue)
fnChange: function to call when a tiddler changes in the filter results. Only called for filter results that identify a tiddler or shadow tiddler. Called as (title,enterValue), and may optionally return a replacement enterValue
fnProcess: function to call each time the tracker is processed, after any enter, leave or change functions are called. Called as (changes)
*/
track(options = {}) {
const {
filterString,
fnEnter,
fnLeave,
fnChange,
fnProcess
} = options;
const id = this.nextTrackerId++;
const tracker = {
id,
filterString,
fnEnter,
fnLeave,
fnChange,
fnProcess,
previousResults: [],
resultValues: {}
};
this.trackers.set(id, tracker);
// Process the tracker
this.processTracker(id);
return id;
}
untrack(id) {
this.trackers.delete(id);
}
processTrackers() {
for(const id of this.trackers.keys()) {
this.processTracker(id);
}
}
processTracker(id) {
const tracker = this.trackers.get(id);
if(!tracker) return;
const results = [];
// Evaluate the filter and remove duplicate results
$tw.utils.each(this.wiki.filterTiddlers(tracker.filterString), title => {
$tw.utils.pushTop(results, title);
});
// Process the newly entered results
results.forEach(title => {
if(!tracker.previousResults.includes(title) && !tracker.resultValues[title] && tracker.fnEnter) {
tracker.resultValues[title] = tracker.fnEnter(title) || true;
}
});
// Process the results that have just left
tracker.previousResults.forEach(title => {
if(!results.includes(title) && tracker.resultValues[title] && tracker.fnLeave) {
tracker.fnLeave(title, tracker.resultValues[title]);
delete tracker.resultValues[title];
}
});
// Update the previous results
tracker.previousResults = results;
}
processChanges(changes) {
for(const tracker of this.trackers.values()) {
Object.keys(changes).forEach(title => {
if(title && tracker.previousResults.includes(title) && tracker.fnChange) {
tracker.resultValues[title] = tracker.fnChange(title, tracker.resultValues[title]) || tracker.resultValues[title];
}
});
if(tracker.fnProcess) {
tracker.fnProcess(changes);
}
}
}
}
exports.FilterTracker = FilterTracker;
+67
View File
@@ -0,0 +1,67 @@
/*\
title: $:/core/modules/info/mediaquerytracker.js
type: application/javascript
module-type: info
Initialise $:/info/ tiddlers derived from media queries via
\*/
"use strict";
exports.getInfoTiddlerFields = function(updateInfoTiddlersCallback) {
if($tw.browser) {
// Functions to start and stop tracking a particular media query tracker tiddler
function track(title) {
var result = {},
tiddler = $tw.wiki.getTiddler(title);
if(tiddler) {
var mediaQuery = tiddler.fields["media-query"],
infoTiddler = tiddler.fields["info-tiddler"],
infoTiddlerAlt = tiddler.fields["info-tiddler-alt"];
if(mediaQuery && infoTiddler) {
// Evaluate and track the media query
result.mqList = window.matchMedia(mediaQuery);
function getResultTiddlers() {
var value = result.mqList.matches ? "yes" : "no",
tiddlers = [];
tiddlers.push({title: infoTiddler, text: value});
if(infoTiddlerAlt) {
tiddlers.push({title: infoTiddlerAlt, text: value});
}
return tiddlers;
};
updateInfoTiddlersCallback(getResultTiddlers());
result.handler = function(event) {
updateInfoTiddlersCallback(getResultTiddlers());
};
result.mqList.addEventListener("change",result.handler);
}
}
return result;
}
function untrack(enterValue) {
if(enterValue.mqList && enterValue.handler) {
enterValue.mqList.removeEventListener("change",enterValue.handler);
}
}
// Track media query tracker tiddlers
function fnEnter(title) {
return track(title);
}
function fnLeave(title,enterValue) {
untrack(enterValue);
}
function fnChange(title,enterValue) {
untrack(enterValue);
return track(title);
}
$tw.filterTracker.track({
filterString: "[all[tiddlers+shadows]tag[$:/tags/MediaQueryTracker]!is[draft]]",
fnEnter: fnEnter,
fnLeave: fnLeave,
fnChange: fnChange
});
}
return [];
};
-7
View File
@@ -33,13 +33,6 @@ exports.getInfoTiddlerFields = function(updateInfoTiddlersCallback) {
// Screen size
infoTiddlerFields.push({title: "$:/info/browser/screen/width", text: window.screen.width.toString()});
infoTiddlerFields.push({title: "$:/info/browser/screen/height", text: window.screen.height.toString()});
// Dark mode through event listener on MediaQueryList
var mqList = window.matchMedia("(prefers-color-scheme: dark)"),
getDarkModeTiddler = function() {return {title: "$:/info/darkmode", text: mqList.matches ? "yes" : "no"};};
infoTiddlerFields.push(getDarkModeTiddler());
mqList.addListener(function(event) {
updateInfoTiddlersCallback([getDarkModeTiddler()]);
});
// Language
infoTiddlerFields.push({title: "$:/info/browser/language", text: navigator.language || ""});
}
+26
View File
@@ -13,6 +13,11 @@ Load core modules
exports.name = "load-modules";
exports.synchronous = true;
// Set to `true` to enable performance instrumentation
var PERFORMANCE_INSTRUMENTATION_CONFIG_TITLE = "$:/config/Performance/Instrumentation";
var widget = require("$:/core/modules/widgets/widget.js");
exports.startup = function() {
// Load modules
$tw.modules.applyMethods("utils",$tw.utils);
@@ -31,6 +36,27 @@ exports.startup = function() {
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
$tw.macros = $tw.modules.getModulesByTypeAsHashmap("macro");
$tw.wiki.initParsers();
// --------------------------
// The rest of the startup process here is not strictly to do with loading modules, but are needed before other startup
// modules are executed. It is easier to put them here than to introduce a new startup module
// --------------------------
// Create a root widget for attaching event handlers. By using it as the parentWidget for another widget tree, one can reuse the event handlers
$tw.rootWidget = new widget.widget({
type: "widget",
children: []
},{
wiki: $tw.wiki,
document: $tw.browser ? document : $tw.fakeDocument
});
// Set up the performance framework
$tw.perf = new $tw.Performance($tw.wiki.getTiddlerText(PERFORMANCE_INSTRUMENTATION_CONFIG_TITLE,"no") === "yes");
// Kick off the filter tracker
$tw.filterTracker = new $tw.FilterTracker($tw.wiki);
$tw.wiki.addEventListener("change",function(changes) {
$tw.filterTracker.handleChangeEvent(changes);
});
// Kick off the background action dispatcher
$tw.backgroundActionDispatcher = new $tw.BackgroundActionDispatcher($tw.filterTracker,$tw.wiki);
if($tw.node) {
$tw.Commander.initCommands();
}
-15
View File
@@ -14,11 +14,6 @@ exports.name = "startup";
exports.after = ["load-modules"];
exports.synchronous = true;
// Set to `true` to enable performance instrumentation
var PERFORMANCE_INSTRUMENTATION_CONFIG_TITLE = "$:/config/Performance/Instrumentation";
var widget = require("$:/core/modules/widgets/widget.js");
exports.startup = function() {
// Minimal browser detection
if($tw.browser) {
@@ -54,16 +49,6 @@ exports.startup = function() {
}
// Initialise version
$tw.version = $tw.utils.extractVersionInfo();
// Set up the performance framework
$tw.perf = new $tw.Performance($tw.wiki.getTiddlerText(PERFORMANCE_INSTRUMENTATION_CONFIG_TITLE,"no") === "yes");
// Create a root widget for attaching event handlers. By using it as the parentWidget for another widget tree, one can reuse the event handlers
$tw.rootWidget = new widget.widget({
type: "widget",
children: []
},{
wiki: $tw.wiki,
document: $tw.browser ? document : $tw.fakeDocument
});
// Kick off the language manager and switcher
$tw.language = new $tw.Language();
$tw.languageSwitcher = new $tw.PluginSwitcher({
@@ -0,0 +1,5 @@
title: $:/core/wiki/config/MediaQueryTrackers/DarkLightPreferred
tags: $:/tags/MediaQueryTracker
media-query: (prefers-color-scheme: dark)
info-tiddler: $:/info/browser/darkmode
info-tiddler-alt: $:/info/darkmode