Skip to content

Commit

Permalink
Modularize renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
PRO-2684 committed Jul 30, 2024
1 parent e865484 commit 008778f
Show file tree
Hide file tree
Showing 6 changed files with 651 additions and 484 deletions.
109 changes: 109 additions & 0 deletions modules/renderer/css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Description: Utilities related to CSS.
import { log } from "./debug.js";
/**
* Attribute name for the style element to store the path of the CSS file.
*/
const styleDataAttr = "data-transitio-style";
/**
* Get the default value of a select variable, given the arguments.
* @param {Array} varArgs Arguments for the select variable.
* @returns {String} The default value of the select variable.
*/
function getSelectDefaultValue(varArgs) {
// varArgs: [default-index, option1, option2, ...]
// option: [value, label] or value
const defaultIndex = varArgs[0];
const defaultOption = varArgs[defaultIndex + 1];
if (Array.isArray(defaultOption)) {
return defaultOption[0];
} else {
return defaultOption;
}
}
/**
* Construct the value of a variable, to be applied into the CSS.
* @param {Object} varObj Variable object.
* @returns {String} The value of the variable.
*/
function constructVarValue(varObj) {
const value = varObj.value ?? varObj.args[0];
switch (varObj.type) {
case "text":
return `"${CSS.escape(value)}"`;
case "number":
return isNaN(value) ? value : value.toString();
case "percent":
case "percentage":
return isNaN(value) ? value : `${value}%`;
case "checkbox":
// varObj.args: [default-index/boolean, option1, option2, ...]
// option: value
// varObj.value: default-index/boolean
return value ? varObj.args[2] : varObj.args[1];
case "select": {
// varObj.args: [default-index, option1, option2, ...]
// option: [value, label] or value
// varObj.value: value
return varObj.value ?? getSelectDefaultValue(varObj.args);
}
default: // color/colour, raw
return value.toString();
}
}
/**
* Apply variables to the CSS.
* @param {String} css CSS content.
* @param {Object} variables A dictionary of variables.
* @returns {String} The CSS content with variables applied.
*/
function applyVariables(css, variables) {
// Regular expression to match the variable pattern `var(--name)`
const varRegex = /var\(--([^)]+)\)/g;
return css.replace(varRegex, (match, varName) => {
const varObj = variables[varName];
if (!varObj) {
return match;
}
return constructVarValue(varObj);
});
}
/**
* Inject CSS into the document.
* @param {String} path Path of the CSS file.
* @param {String} css CSS content.
* @returns {Element} The style element.
*/
function injectCSS(path, css) {
const style = document.createElement("style");
style.setAttribute(styleDataAttr, path);
style.textContent = css;
document.head.appendChild(style);
return style;
}
/**
* Helper function that applies variables to CSS and injects it into the document (or updates an existing style element).
* @param {String} path Path of the CSS file.
* @param {String} css CSS content.
* @param {Boolean} enabled Whether the CSS shall be enabled.
* @param {Object} meta Metadata of the CSS file.
*/
function cssHelper(path, css, enabled, meta) {
const current = document.querySelector(`style[${styleDataAttr}="${path}"]`);
log("Applying variables to", path, meta.variables)
const processedCSS = enabled ? applyVariables(css, meta.variables) : `/* ${meta.description || "此文件没有描述"} */`;
if (current) {
current.textContent = processedCSS;
} else {
injectCSS(path, processedCSS);
}
}

export {
styleDataAttr,
getSelectDefaultValue,
// constructVarValue,
// applyVariables,
// injectCSS,
cssHelper
};

30 changes: 30 additions & 0 deletions modules/renderer/debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Description: Debugging utilities for the renderer.
let isDebug = false;

/**
* Log to console if debug mode is enabled.
* @param {...any} args Arguments to log
*/
function log(...args) {
if (isDebug) {
console.log("[Transitio]", ...args);
}
}

transitio.queryIsDebug().then((result) => {
isDebug = result;
});

/**
* Show debug hint on settings page.
* @param {Element} view View element
*/
function showDebugHint(view) {
if (isDebug) {
const debug = view.querySelector("#transitio-debug");
debug.style.color = "red";
debug.title = "Debug 模式已激活";
}
}

export { log, showDebugHint };
41 changes: 41 additions & 0 deletions modules/renderer/egg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Description: Easter egg at settings view.

/** Function to setup the easter egg at the settings view.
* @param {HTMLElement} logo - The logo element.
* @returns {void}
*/
function setupEasterEgg(logo) {
const title = document.querySelector(".setting-title");
function lumos() {
document.body.classList.remove("q-theme-tokens-dark");
document.body.classList.add("q-theme-tokens-light");
document.body.setAttribute("q-theme", "light");
title.classList.add("lumos");
setTimeout(() => {
title.classList.remove("lumos");
}, 2000);
}
function nox() {
document.body.classList.remove("q-theme-tokens-light");
document.body.classList.add("q-theme-tokens-dark");
document.body.setAttribute("q-theme", "dark");
title.classList.add("nox");
setTimeout(() => {
title.classList.remove("nox");
}, 2000);
}
function currentTheme() {
return document.body.getAttribute("q-theme");
}
logo.addEventListener("animationend", () => {
document.startViewTransition(() => {
if (currentTheme() == "light") {
nox();
} else {
lumos();
}
});
});
}

export { setupEasterEgg };
97 changes: 97 additions & 0 deletions modules/renderer/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Description: Search module for settings view.
import { log } from "./debug.js";

/** Search `keyword` in the `el` and highlight the matched text.
* @param {Highlight} highlight The highlight object.
* @param {HTMLElement} el The element to search.
* @param {string} keyword The keyword to search.
* @returns {boolean} Returns `true` if a match is found.
*/
function searchAndHighlight(highlight, el, keyword) {
if (!el) return false;
const textContent = el.textContent.toLowerCase();
let isMatch = false;
let startIndex = 0;
let index;
while ((index = textContent.indexOf(keyword, startIndex)) !== -1) {
const range = new Range();
range.setStart(el.firstChild, index);
range.setEnd(el.firstChild, index + keyword.length);
highlight.add(range);
isMatch = true;
startIndex = index + keyword.length;
}
return isMatch;
}
/** Search all `keywords` in the `details` and highlight the matched text.
* @param {Highlight} highlight The highlight object.
* @param {HTMLElement} detail The `details` element to search.
* @param {Set<string>} keywords The keywords to search.
* @returns {boolean} Returns `true` if all keywords are found in the `details`.
*/
function searchAllAndHighlight(highlight, detail, keywords) {
const settingItem = detail.querySelector("summary > setting-item");
const nameEl = settingItem.querySelector("setting-text");
const descEl = settingItem.querySelector("setting-text[data-type='secondary']");
let matches = 0;
for (const keyword of keywords) {
const nameMatch = searchAndHighlight(highlight, nameEl, keyword);
const descMatch = searchAndHighlight(highlight, descEl, keyword);
if (nameMatch || descMatch) {
matches++;
}
}
return matches === keywords.size;
}
/** Perform search and hide the `details` that doesn't match the search.
* @param {Highlight} highlight The highlight object.
* @param {string} text The search text.
* @param {HTMLElement} container The container element.
* @returns {void}
*/
function doSearch(highlight, text, container) { // Main function for searching
log("Search", text);
highlight.clear(); // Clear previous highlights
const items = container.querySelectorAll("details");
const searchWords = new Set( // Use Set to remove duplicates
text.toLowerCase() // Convert to lowercase
.split(" ") // Split by space
.map(word => word.trim()) // Remove leading and trailing spaces
.filter(word => word.length > 0) // Remove empty strings
);
items.forEach((detail) => { // Iterate through all `details`
const isMatch = searchAllAndHighlight(highlight, detail, searchWords);
detail.toggleAttribute(searchHiddenDataAttr, !isMatch); // Hide the `details` if it doesn't match
});
}

/** Setup the search bar for the settings view.
* @param {HTMLElement} view The settings view.
* @returns {void}
*/
function setupSearch(view) {
const inputTags = ["INPUT", "SELECT", "TEXTAREA"];
const search = view.querySelector("#transitio-search");
const container = view.querySelector("setting-section.snippets > setting-panel > setting-list");
const highlight = new Highlight();
CSS.highlights.set("transitio-search-highlight", highlight);
document.addEventListener("keydown", (e) => {
if (!view.checkVisibility()) return; // The setting window is not visible
if (document.activeElement === search) { // The search bar is focused
// Escape closes the window
if (e.key === "Enter") { // Search
search.scrollIntoView();
}
} else if (!inputTags.includes(document.activeElement.tagName)) { // Not focusing on some other input element
// Focus on the search bar when "Enter" or "Ctrl + F" is pressed
if (e.key === "Enter" || (e.ctrlKey && e.key === "f")) {
e.preventDefault();
search.focus();
search.scrollIntoView();
}
}
});
search.addEventListener("change", () => { doSearch(highlight, search.value, container); });
}

export { setupSearch };
Loading

0 comments on commit 008778f

Please sign in to comment.