Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce background actions and media query tracker mechanisms #8555

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
87 changes: 87 additions & 0 deletions core/modules/filter-tracker.js
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Member

@pmario pmario Jan 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% sure, but there seem to be no tests if the fn* parameters are undefined of if they actually are function()

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;

})();
67 changes: 67 additions & 0 deletions core/modules/info/mediaquerytracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*\
title: $:/core/modules/info/mediaquerytracker.js
type: application/javascript
module-type: info

Initialise $:/info/ tiddlers derived from media queries via

\*/
(function(){

/*jslint node: true, browser: true */
/*global $tw: false */
"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("[all[tiddlers+shadows]tag[$:/tags/MediaQueryTracker]!is[draft]]",fnEnter,fnLeave,fnChange);
}
return [];
};

})();
7 changes: 0 additions & 7 deletions core/modules/info/platform.js
Original file line number Diff line number Diff line change
@@ -36,13 +36,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 || ""});
}
11 changes: 11 additions & 0 deletions core/modules/startup/load-modules.js
Original file line number Diff line number Diff line change
@@ -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);
};

})();
5 changes: 0 additions & 5 deletions core/modules/startup/startup.js
Original file line number Diff line number Diff line change
@@ -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",
5 changes: 5 additions & 0 deletions core/wiki/config/MediaQueryTrackers/DarkLightPreferred.tid
Original file line number Diff line number Diff line change
@@ -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
7 changes: 4 additions & 3 deletions editions/tw5.com/tiddlers/mechanisms/InfoMechanism.tid
Original file line number Diff line number Diff line change
@@ -4,8 +4,8 @@ tags: Mechanisms
title: InfoMechanism
type: text/vnd.tiddlywiki

\define example(name)
<$transclude tiddler="""$:/info/url/$name$""" mode="inline"/>
\procedure example(name)
<$text text={{{ [[$:/info/url/]addsuffix<name>get[text]] }}} />
\end

System tiddlers in the namespace `$:/info/` are used to expose information about the system (including the current browser) so that WikiText applications can adapt themselves to available features.
@@ -19,6 +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.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>>'') |
@@ -28,4 +30,3 @@ System tiddlers in the namespace `$:/info/` are used to expose information about
|[[$:/info/url/port]] |<<.from-version "5.1.14">> Port portion of URL of wiki (eg, ''<<example port>>'') |
|[[$:/info/url/protocol]] |<<.from-version "5.1.14">> Protocol portion of URL of wiki (eg, ''<<example protocol>>'') |
|[[$:/info/url/search]] |<<.from-version "5.1.14">> Search portion of URL of wiki (eg, ''<<example search>>'') |
|[[$:/info/darkmode]] |<<.from-version "5.1.23">> Is dark mode enabled? ("yes" or "no") |
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
title: MediaQueryTrackerMechanism
tags: Mechanisms

<<.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 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.