1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-08-10 07:43:49 +00:00

Introduce new generic filter tracker

The new filter tracker is a generic JS mechanism for tracking changes to the results of filters and changes to the tiddlers identified by those results.

The rationale for the new mechanism is that it is a generalisation of code that is already present in the keyboard manager. The intention is to refactor the keyboard manager to use the filter tracker in due course
This commit is contained in:
Jeremy Ruston 2025-01-05 17:32:15 +00:00
parent ea2426eea1
commit e78d3ae04e
6 changed files with 134 additions and 22 deletions

View File

@ -0,0 +1,87 @@
/*\
title: $:/core/modules/filter-tracker.js
type: application/javascript
module-type: global
Class to track the results of a filter string
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
function FilterTracker(wiki) {
this.wiki = wiki;
this.trackers = [];
this.wiki.addEventListener("change",this.handleChangeEvent.bind(this));
}
FilterTracker.prototype.handleChangeEvent = function(changes) {
this.processTrackers();
this.processChanges(changes);
};
/*
Add a tracker to the filter tracker
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
*/
FilterTracker.prototype.track = function(filterString,fnEnter,fnLeave,fnChange) {
// Add the tracker details
var index = this.trackers.length;
this.trackers.push({
filterString: filterString,
fnEnter: fnEnter,
fnLeave: fnLeave,
fnChange: fnChange,
previousResults: [], // Results from the previous time the tracker was processed
resultValues: {} // Map by title to the value returned by fnEnter
});
// Process the tracker
this.processTracker(index);
};
FilterTracker.prototype.processTrackers = function() {
for(var t=0; t<this.trackers.length; t++) {
this.processTracker(t);
}
};
FilterTracker.prototype.processTracker = function(index) {
var tracker = this.trackers[index],
results = this.wiki.filterTiddlers(tracker.filterString);
// Process the results
$tw.utils.each(results,function(title) {
if(tracker.previousResults.indexOf(title) === -1 && !tracker.resultValues[title]) {
tracker.resultValues[title] = tracker.fnEnter(title) || true;
}
});
$tw.utils.each(tracker.previousResults,function(title) {
if(results.indexOf(title) === -1 && tracker.resultValues[title]) {
tracker.fnLeave(title,tracker.resultValues[title]);
delete tracker.resultValues[title];
}
});
// Update the previous results
tracker.previousResults = results;
};
FilterTracker.prototype.processChanges = function(changes) {
for(var t=0; t<this.trackers.length; t++) {
var tracker = this.trackers[t];
$tw.utils.each(changes,function(change,title) {
if(tracker.previousResults.indexOf(title) !== -1) {
// Call the change function and if it doesn't return a value then keep the old value
tracker.resultValues[title] = tracker.fnChange(title,tracker.resultValues[title]) || tracker.resultValues[title];
}
});
}
};
exports.FilterTracker = FilterTracker;
})();

View File

@ -13,36 +13,55 @@ Initialise $:/info/ tiddlers derived from media queries via
"use strict";
exports.getInfoTiddlerFields = function(updateInfoTiddlersCallback) {
var infoTiddlerFields = [];
if($tw.browser) {
// Get the media query tracker tiddlers
var trackers = $tw.wiki.getTiddlersWithTag("$:/tags/MediaQueryTracker");
$tw.utils.each(trackers,function(title) {
var tiddler = $tw.wiki.getTiddler(title);
// 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
var mqList = window.matchMedia(mediaQuery);
result.mqList = window.matchMedia(mediaQuery);
function getResultTiddlers() {
var value = mqList.matches ? "yes" : "no",
tiddlers = [{title: infoTiddler, text: value}];
var value = result.mqList.matches ? "yes" : "no",
tiddlers = [];
tiddlers.push({title: infoTiddler, text: value});
if(infoTiddlerAlt) {
tiddlers.push({title: infoTiddlerAlt, text: value})
}
return tiddlers;
};
infoTiddlerFields.push.apply(infoTiddlerFields,getResultTiddlers());
mqList.addEventListener("change",function(event) {
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("[all[tiddlers+shadows]tag[$:/tags/MediaQueryTracker]!is[draft]]",fnEnter,fnLeave,fnChange);
}
return infoTiddlerFields;
return [];
};
})();

View File

@ -16,6 +16,9 @@ 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";
exports.startup = function() {
// Load modules
$tw.modules.applyMethods("utils",$tw.utils);
@ -35,6 +38,14 @@ exports.startup = function() {
$tw.macros = $tw.modules.getModulesByTypeAsHashmap("macro");
$tw.wiki.initParsers();
$tw.Commander.initCommands();
// --------------------------
// 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
// --------------------------
// 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);
};
})();

View File

@ -17,9 +17,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() {
@ -57,8 +54,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",

View File

@ -19,8 +19,8 @@ System tiddlers in the namespace `$:/info/` are used to expose information about
|[[$:/info/browser/language]] |<<.from-version "5.1.20">> Language as reported by browser (note that some browsers report two character codes such as `en` while others report full codes such as `en-GB`) |
|[[$:/info/browser/screen/width]] |Screen width in pixels |
|[[$:/info/browser/screen/height]] |Screen height in pixels |
|[[$:/info/browser/darkmode]] |<<.from-version "5.3.6">> Is dark mode preferred? ("yes" or "no") |
|[[$:/info/darkmode]] |<<.deprecated-since "5.3.6">> Alias for $:/info/browser/darkmode |
|[[$:/info/browser/darkmode]] |<<.from-version "5.3.7">> Is dark mode preferred? ("yes" or "no") |
|[[$:/info/darkmode]] |<<.deprecated-since "5.3.7">> Alias for $:/info/browser/darkmode |
|[[$:/info/node]] |Running under [[Node.js]]? ("yes" or "no") |
|[[$:/info/url/full]] |<<.from-version "5.1.14">> Full URL of wiki (eg, ''<<example full>>'') |
|[[$:/info/url/host]] |<<.from-version "5.1.14">> Host portion of URL of wiki (eg, ''<<example host>>'') |

View File

@ -1,8 +1,8 @@
title: MediaQueryTrackerMechanism
tags: Mechanisms
The media query tracker mechanism allows you to define [[custom CSS media queries|https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries]] to be bound to a specified [[info|InfoMechanism]] tiddler. The info tiddler will be dynamically update to reflect the current state of the media query.
<<.from-version "5.3.7">> The media query tracker mechanism allows you to define [[custom CSS media queries|https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries]] to be bound to a specified [[info|InfoMechanism]] tiddler. The info tiddler will be dynamically update to reflect the current state of the media query.
Adding or modifying a tiddler tagged $:/tags/MediaQueryTracker will only take effect when the wiki is reloaded
Adding or modifying a tiddler tagged $:/tags/MediaQueryTracker takes effect immediately.
The media queries are always applied against the main window. This is relevant for viewport related media queries such as `min-width` which will always respect the main window and ignore the sizes of any external windows.