在单击事件侦听器中调用 AJAX 后切换暗模式按钮停止工作(所有逻辑都包含在 IIFE 中)
Toggling Dark mode button stops working after AJAX calls in click event listener (all logic wrapped in an IIFE)
对于我的个人博客网站 (https://victorfeight.com/) 在 Cloudflare 页面后面提供服务,我将 Eleventy 与自定义脚本一起使用,其中包括暗模式切换逻辑、带有 AJAX 的类似 SPA 的页面切换、以及使用 Elasticlunr 和 JavaScript 的搜索功能。由于我是 JavaScript Web 开发的新手并且选择不使用框架来实现 SPA 页面切换,我决定将所有逻辑实现到一个文件中,包装在一个 IIFE 中(传入 window 和文档)。我想知道这是否是一起实现这种逻辑的标准方法,或者我是否应该使用 webpack 或更现代的解决方案。我还想知道是否可以在每页加载暗 mode/light 模式脚本逻辑和搜索模式逻辑,尽管使用 AJAX url-切换逻辑。
第一个问题:某些点击事件处理逻辑似乎打断了我在同一文件中引入的新暗模式切换。
我在此处为暗模式实现了逻辑:https://jec.fyi/blog/supporting-dark-mode
我使用此处找到的逻辑实现了搜索功能:https://www.belter.io/eleventy-search/
我的 Liquid 模板中有以下 HTML,默认加载:
<div class="col-xl-6 px-0">
<div class="p-0 p-md-0 m-0 text-white">
<object type="text/html"
style="width: 100%;height: 30rem;min-width: 378px;" id="icosahedron"
data="scripts/icosahedron.html">
</object>
</div>
<!-- <iframe style="width: 100%;height: 30rem;min-width: 378px;" scrolling="no" id="icosahedron"
src="scripts/icosahedron.html" frameBorder="0" allowfullscreen></iframe> -->
</div>
我有 scripts/icosahedron.html 的轻量模式版本,它通过 scripts/icosahedron_dark.html 加载 scripts/script.js(轻量版本)加载 scripts/script_dark.js(深色版本)。
我想根据 light-mode/dark-mode 按钮的按下情况选择性地切换加载哪个脚本。
我为页面切换实现的逻辑(这似乎中断了包含在同一个 IIFE 中的暗模式切换)我从这里找到:
https://github.com/learosema/eleventy-mini-spa
/**
* Load content into page without a whole page reload
* @param {string} href URL to route to
* @param {boolean} pushState whether to call history.pushState or not
*/
function load(href, pushState) {
const container = $("main");
const xhr = new XMLHttpRequest();
xhr.onload = function () {
fetchJSON();
const d = xhr.responseXML;
const dTitle = d.title || "";
const dContainer = $("main", d);
container.innerHTML = (dContainer && dContainer.innerHTML) || "";
document.title = dTitle;
if (pushState) {
history.pushState({}, dTitle, href);
}
container.focus();
window.scrollTo(0, 0);
};
xhr.onerror = function () {
// fallback to normal link behaviour
document.location.href = href;
return;
};
xhr.open("GET", href);
xhr.responseType = "document";
xhr.send();
}
function $(sel, con) {
return (con || document).querySelector(sel);
}
/**
* Search for a parent anchor tag outside a clicked event target
*
* @param {HTMLElement} el the clicked event target.
* @param {number} maxNests max number of levels to go up.
* @returns the anchor tag or null
*/
function findAnchorTag(el, maxNests = 3) {
for (let i = maxNests; el && i > 0; --i, el = el.parentNode) {
if (el.nodeName === "A") {
return el;
}
}
return null;
}
const links = document.getElementsByClassName("nav-link");
// Loop through the buttons and add the active class to the current/clicked button
for (var i = 0; i < links.length; i++) {
links[i].addEventListener("click", function () {
var current = document.getElementsByClassName("active");
if (current[0]) {
current[0].className = current[0].className.replace(" active", "");
this.className += " active";
}
});
}
window.addEventListener("click", function (evt) {
let baseUrl = $('meta[name="x-base-url"]')?.getAttribute("content") || "/";
const el = findAnchorTag(evt.target);
const href = el?.getAttribute("href");
if (el && href) {
if (
href.startsWith("#") ||
el.getAttribute("target") === "_blank" ||
/\.\w+$/.test(href)
) {
// eleventy urls in this configuration do not have extensions like .html
// if they have, or if target _blank is set, or they are a hash link,
// then do nothing.
return;
}
if (href.startsWith("/")) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
// if the URL starts with the base url, do the SPA handling
if (href.startsWith(baseUrl)) {
evt.preventDefault();
load(href, true);
}
}
});
window.addEventListener("popstate", function (e) {
load(document.location.pathname, false);
});
fetchJSON();
作为旁注,我发现我也必须在每个页面切换上重复我的 fetchJSON() 函数,才能使搜索正常工作,我想知道什么是最佳实践,因为我也是获取“Uncaught (in promise) TypeError: document.getElementById(...) is null fetchJSON https://victorfeight.com/scripts/mini-spa.js:9" 在调用搜索逻辑之前在错误堆栈中加载和加载。
请注意,我已将以下内容添加到单击事件侦听器中进行测试,但它似乎什么也没做:
if (href.startsWith("/")) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
点击处理逻辑如下:
window.addEventListener("click", function (evt) {
let baseUrl = $('meta[name="x-base-url"]')?.getAttribute("content") || "/";
const el = findAnchorTag(evt.target);
const href = el?.getAttribute("href");
if (el && href) {
if (
href.startsWith("#") ||
el.getAttribute("target") === "_blank" ||
/\.\w+$/.test(href)
) {
// eleventy urls in this configuration do not have extensions like .html
// if they have, or if target _blank is set, or they are a hash link,
// then do nothing.
return;
}
if (href.startsWith("/")) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
// if the URL starts with the base url, do the SPA handling
if (href.startsWith(baseUrl)) {
evt.preventDefault();
load(href, true);
}
}
});
我试过的,这是我的全彩模式逻辑:
"use strict";
const icosa = document.getElementById("icosahedron");
function changeColor() {
const bodyEl = document.body;
const themeStylesheet = document.getElementById("theme");
const themeToggle = document.getElementById("moon-1");
const DARK = "dark";
const LIGHT = "light";
const COLOR_SCHEME_CHANGED = "colorSchemeChanged";
themeToggle.addEventListener("click", () => {
const isDark = bodyEl.classList.toggle("dark-mode");
const mode = isDark ? DARK : LIGHT;
sessionStorage.setItem("jec.color-scheme", mode);
if (isDark) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
themeToggle.className = "far fa-sun fa-2x";
themeToggle.title = themeToggle.title.replace(DARK, LIGHT);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(LIGHT, DARK);
} else {
icosa.setAttribute("data", "scripts/icosahedron.html");
themeToggle.className = "far fa-moon fa-2x";
themeToggle.title = themeToggle.title.replace(LIGHT, DARK);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(DARK, LIGHT);
}
themeToggle.dispatchEvent(
new CustomEvent(COLOR_SCHEME_CHANGED, { detail: mode })
);
});
}
changeColor();
function init() {
const DARK = "dark";
const LIGHT = "light";
const isSystemDarkMode =
matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
let mode = sessionStorage.getItem("jec.color-scheme");
if (!mode && isSystemDarkMode) {
mode = DARK;
} else {
mode = mode || LIGHT;
}
if (mode === DARK) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
document.getElementById("moon-1").click();
}
}
// run the code
init();
const pressEnter = (e) => {
const searchField = document.getElementById("searchField");
if (e.key === "Enter") {
if (searchField) {
searchField && searchField.blur();
}
}
};
我为使颜色模式工作而添加的内容,在我的 init() 函数中,我修改了数据属性以指向正确的文件:
if (mode === DARK) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
并且在我相关的改色函数中:
if (isDark) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
themeToggle.className = "far fa-sun fa-2x";
themeToggle.title = themeToggle.title.replace(DARK, LIGHT);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(LIGHT, DARK);
} else {
icosa.setAttribute("data", "scripts/icosahedron.html");
现在逻辑在首页上起作用了。但是,一旦我使用顶部导航切换页面并返回主页,选择性加载脚本的逻辑就会停止工作,我假设范围有问题。
所以我的问题是,我怎样才能让这个暗模式脚本切换逻辑与这个 link 页面切换逻辑很好地协同工作?将所有三个脚本的逻辑包装在一个 IIFE 中是最好的方法,还是我应该寻找其他技术来更好地模块化它?目前,通过 Cloudflare 页面提供的生产构建根本不加载选择性暗模式脚本逻辑,只有本地加载。包含的屏幕截图来自本地。我还对如何在暗模式切换时为我的搜索背景图像有选择地加载 CSS 感兴趣。我很感激任何建议。
这是完整的脚本,为了完成:
(function (window, document) {
"use strict";
const icosa = document.getElementById("icosahedron");
function changeColor() {
const bodyEl = document.body;
const themeStylesheet = document.getElementById("theme");
const themeToggle = document.getElementById("moon-1");
const DARK = "dark";
const LIGHT = "light";
const COLOR_SCHEME_CHANGED = "colorSchemeChanged";
themeToggle.addEventListener("click", () => {
const isDark = bodyEl.classList.toggle("dark-mode");
const mode = isDark ? DARK : LIGHT;
sessionStorage.setItem("jec.color-scheme", mode);
if (isDark) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
themeToggle.className = "far fa-sun fa-2x";
themeToggle.title = themeToggle.title.replace(DARK, LIGHT);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(LIGHT, DARK);
} else {
icosa.setAttribute("data", "scripts/icosahedron.html");
themeToggle.className = "far fa-moon fa-2x";
themeToggle.title = themeToggle.title.replace(LIGHT, DARK);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(DARK, LIGHT);
}
themeToggle.dispatchEvent(
new CustomEvent(COLOR_SCHEME_CHANGED, { detail: mode })
);
});
}
changeColor();
function init() {
const DARK = "dark";
const LIGHT = "light";
const isSystemDarkMode =
matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
let mode = sessionStorage.getItem("jec.color-scheme");
if (!mode && isSystemDarkMode) {
mode = DARK;
} else {
mode = mode || LIGHT;
}
if (mode === DARK) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
document.getElementById("moon-1").click();
}
}
// run the code
init();
const pressEnter = (e) => {
const searchField = document.getElementById("searchField");
if (e.key === "Enter") {
if (searchField) {
searchField && searchField.blur();
}
}
};
const search = (e) => {
const cardHolder = document.getElementById("card-holder");
if (e.target.value) {
cardHolder.style.display = "none";
}
const results = window.searchIndex.search(e.target.value, {
bool: "AND",
expand: true,
});
const resEl = document.getElementById("searchResults");
const noResultsEl = document.getElementById("noResultsFound");
resEl.innerHTML = "";
if (Object.keys(results).length !== 0) {
noResultsEl.style.display = "none";
cardHolder.style.display = "none";
results.map((r) => {
const { id, title, categories, excerpt, date } = r.doc;
const el = document.createElement("div");
el.setAttribute(
"class",
"archive-card card border border-light shadow-0"
);
el.style.display = "flex";
resEl.appendChild(el);
const header = document.createElement("div");
header.setAttribute("id", "archive-header");
header.setAttribute("class", "card-header border-0");
el.appendChild(header);
const h3 = document.createElement("h3");
h3.setAttribute("class", "card-title pb-3");
h3.style.textDecoration = "underline";
header.appendChild(h3);
const a = document.createElement("a");
a.setAttribute("href", id);
a.setAttribute("class", "text-black");
a.textContent = title;
h3.appendChild(a);
const dateSection = document.createElement("div");
dateSection.setAttribute("class", "category-section text-muted");
var dateString = date;
dateSection.innerHTML += dateString;
header.appendChild(dateSection);
console.log(categories);
var catString = "";
for (let i = 0; i < categories.length; i++) {
if (i === 0) {
catString += ` [<a href="/category/${categories[i]}">${categories[i]}</a>, `;
} else if (i != categories.length - 1) {
catString += `<a href="/category/${categories[i]}">${categories[i]}</a>, `;
} else {
catString += `<a href="/category/${categories[i]}">${categories[i]}</a>]`;
}
}
// header.innerHTML += catString;
dateSection.innerHTML += catString;
const cardBody = document.createElement("div");
header.setAttribute("class", "card-body");
el.appendChild(cardBody);
const excerptP = document.createElement("p");
excerptP.setAttribute("class", "card-text");
excerptP.innerHTML += excerpt;
cardBody.appendChild(excerptP);
});
} else {
noResultsEl.style.display = "block";
cardHolder.style.display = "block";
}
};
function fetchJSON() {
fetch("/search-index.json").then((response) =>
response.json().then((rawIndex) => {
window.searchIndex = elasticlunr.Index.load(rawIndex);
document
.getElementById("searchField")
.addEventListener("keyup", search);
document
.getElementById("searchField")
.addEventListener("keydown", pressEnter);
})
);
}
fetchJSON();
/**
* Load content into page without a whole page reload
* @param {string} href URL to route to
* @param {boolean} pushState whether to call history.pushState or not
*/
function load(href, pushState) {
const container = $("main");
const xhr = new XMLHttpRequest();
xhr.onload = function () {
fetchJSON();
const d = xhr.responseXML;
const dTitle = d.title || "";
const dContainer = $("main", d);
container.innerHTML = (dContainer && dContainer.innerHTML) || "";
document.title = dTitle;
if (pushState) {
history.pushState({}, dTitle, href);
}
container.focus();
window.scrollTo(0, 0);
};
xhr.onerror = function () {
// fallback to normal link behaviour
document.location.href = href;
return;
};
xhr.open("GET", href);
xhr.responseType = "document";
xhr.send();
}
function $(sel, con) {
return (con || document).querySelector(sel);
}
/**
* Search for a parent anchor tag outside a clicked event target
*
* @param {HTMLElement} el the clicked event target.
* @param {number} maxNests max number of levels to go up.
* @returns the anchor tag or null
*/
function findAnchorTag(el, maxNests = 3) {
for (let i = maxNests; el && i > 0; --i, el = el.parentNode) {
if (el.nodeName === "A") {
return el;
}
}
return null;
}
const links = document.getElementsByClassName("nav-link");
// Loop through the buttons and add the active class to the current/clicked button
for (var i = 0; i < links.length; i++) {
links[i].addEventListener("click", function () {
var current = document.getElementsByClassName("active");
if (current[0]) {
current[0].className = current[0].className.replace(" active", "");
this.className += " active";
}
});
}
window.addEventListener("click", function (evt) {
let baseUrl = $('meta[name="x-base-url"]')?.getAttribute("content") || "/";
const el = findAnchorTag(evt.target);
const href = el?.getAttribute("href");
if (el && href) {
if (
href.startsWith("#") ||
el.getAttribute("target") === "_blank" ||
/\.\w+$/.test(href)
) {
// eleventy urls in this configuration do not have extensions like .html
// if they have, or if target _blank is set, or they are a hash link,
// then do nothing.
return;
}
if (href.startsWith("/")) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
// if the URL starts with the base url, do the SPA handling
if (href.startsWith(baseUrl)) {
evt.preventDefault();
load(href, true);
}
}
});
window.addEventListener("popstate", function (e) {
load(document.location.pathname, false);
});
fetchJSON();
})(window, document);
让我迈出这一步。
第一期:点击事件处理逻辑
我 re-implemented 我所有的 javascript 逻辑都放在单独的文件中,现在 Rollup.js 缩小了。
这是我的新汇总构建配置:
import path from "path";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import replace from "@rollup/plugin-replace";
import { terser } from "rollup-plugin-terser";
let fileDest = `mdb.min`;
const external = ["@popperjs/core"];
const plugins = [terser()];
const globals = {
"@popperjs/core": "Popper",
};
export default [
{
input: path.resolve(__dirname, `_11ty_scripts/js/mdb.free.js`),
output: {
file: path.resolve(__dirname, `scripts/${fileDest}.js`),
format: "umd",
name: "bootstrap",
globals,
},
external,
plugins,
},
{
input: ["_11ty_scripts/search.js"],
output: [
{
file: "scripts/search.min.js",
format: "cjs",
sourcemap: true,
plugins: [terser()],
},
],
},
{
input: ["_11ty_scripts/main.js"],
output: [
{
file: "scripts/min.js",
format: "iife",
sourcemap: true,
plugins: [terser()],
},
],
},
{
input: ["_11ty_scripts/colorToggle.js"],
output: [
{
file: "scripts/colorToggle.min.js",
format: "cjs",
sourcemap: true,
plugins: [terser()],
},
],
},
];
这是捆绑了 mdb.free.js 的自定义版本,它使用一些静态分析和 tree-shaking 从结果中删除未使用的代码,然后通过 terser() 缩小。
我将 search.js 和 main.js 分开到它们自己的外部文件中。我从 https://github.com/google/eleventy-high-performance-blog 中获取的很多 main.js 逻辑,我正在用悬停时预取链接替换我原来的 spa-like ajax 开关:
const exposed = {};
if (location.search) {
var a = document.createElement("a");
a.href = location.href;
a.search = "";
history.replaceState(null, null, a.href);
}
function tweet_(url) {
open(
"https://twitter.com/intent/tweet?url=" + encodeURIComponent(url),
"_blank"
);
}
function tweet(anchor) {
tweet_(anchor.getAttribute("href"));
}
expose("tweet", tweet);
function share(anchor) {
var url = anchor.getAttribute("href");
event.preventDefault();
tweet_(url);
//if (navigator.share) {
// navigator.share({
// url: url,
// });
//} else if (navigator.clipboard) {
// navigator.clipboard.writeText(url);
// message("Article URL copied to clipboard.");
//} else {
// tweet_(url);
//}
}
expose("share", share);
function message(msg) {
var dialog = document.getElementById("message");
dialog.textContent = msg;
dialog.setAttribute("open", "");
setTimeout(function () {
dialog.removeAttribute("open");
}, 3000);
}
function prefetch(e) {
if (e.target.tagName != "A") {
return;
}
if (e.target.origin != location.origin) {
return;
}
/**
* Return the given url with no fragment
* @param {string} url potentially containing a fragment
* @return {string} url without fragment
*/
const removeUrlFragment = (url) => url.split("#")[0];
if (
removeUrlFragment(window.location.href) === removeUrlFragment(e.target.href)
) {
return;
}
var l = document.createElement("link");
l.rel = "prefetch";
l.href = e.target.href;
document.head.appendChild(l);
}
document.documentElement.addEventListener("mouseover", prefetch, {
capture: true,
passive: true,
});
document.documentElement.addEventListener("touchstart", prefetch, {
capture: true,
passive: true,
});
const GA_ID = document.documentElement.getAttribute("ga-id");
window.ga =
window.ga ||
function () {
if (!GA_ID) {
return;
}
(ga.q = ga.q || []).push(arguments);
};
ga.l = +new Date();
ga("create", GA_ID, "auto");
ga("set", "transport", "beacon");
var timeout = setTimeout(
(onload = function () {
clearTimeout(timeout);
ga("send", "pageview");
}),
1000
);
var ref = +new Date();
function ping(event) {
var now = +new Date();
if (now - ref < 1000) {
return;
}
ga("send", {
hitType: "event",
eventCategory: "page",
eventAction: event.type,
eventLabel: Math.round((now - ref) / 1000),
});
ref = now;
}
addEventListener("pagehide", ping);
addEventListener("visibilitychange", ping);
/**
* Injects a script into document.head
* @param {string} src path of script to be injected in <head>
* @return {Promise} Promise object that resolves on script load event
*/
const dynamicScriptInject = (src) => {
return new Promise((resolve) => {
const script = document.createElement("script");
script.src = src;
script.type = "text/javascript";
document.head.appendChild(script);
script.addEventListener("load", () => {
resolve(script);
});
});
};
// Script web-vitals.js will be injected dynamically if user opts-in to sending CWV data.
const sendWebVitals = document.currentScript.getAttribute("data-cwv-src");
if (/web-vitals.js/.test(sendWebVitals)) {
dynamicScriptInject(`${window.location.origin}/scripts/web-vitals.js`)
.then(() => {
webVitals.getCLS(sendToGoogleAnalytics);
webVitals.getFID(sendToGoogleAnalytics);
webVitals.getLCP(sendToGoogleAnalytics);
})
.catch((error) => {
console.error(error);
});
}
addEventListener(
"click",
function (e) {
var button = e.target.closest("button");
if (!button) {
return;
}
ga("send", {
hitType: "event",
eventCategory: "button",
eventAction: button.getAttribute("aria-label") || button.textContent,
});
},
true
);
var selectionTimeout;
addEventListener(
"selectionchange",
function () {
clearTimeout(selectionTimeout);
var text = String(document.getSelection()).trim();
if (text.split(/[\s\n\r]+/).length < 3) {
return;
}
selectionTimeout = setTimeout(function () {
ga("send", {
hitType: "event",
eventCategory: "selection",
eventAction: text,
});
}, 2000);
},
true
);
function expose(name, fn) {
exposed[name] = fn;
}
addEventListener("click", (e) => {
const handler = e.target.closest("[on-click]");
if (!handler) {
return;
}
e.preventDefault();
const name = handler.getAttribute("on-click");
const fn = exposed[name];
if (!fn) {
throw new Error("Unknown handler" + name);
}
fn(handler);
});
function removeBlurredImage(img) {
// Ensure the browser doesn't try to draw the placeholder when the real image is present.
img.style.backgroundImage = "none";
}
document.body.addEventListener(
"load",
(e) => {
if (e.target.tagName != "IMG") {
return;
}
removeBlurredImage(e.target);
},
/* capture */ "true"
);
for (let img of document.querySelectorAll("img")) {
if (img.complete) {
removeBlurredImage(img);
}
}
这一切都通过汇总汇总到一个 IIFE 中,并在每个页面上提供。
那里发生了很多事情,但本质上它是用于 Twitter 共享和预取链接的逻辑,尽管它确实添加了一些点击处理逻辑,我的 colorToggle 逻辑需要小心回避这些逻辑。
对于 colorToggle,我重新实现了使用 javascript 在两个 href 样式表之间交换的原始逻辑,现在在 CSS 中的两个 color-mode 属性之间交换并记住 localStorage 的选择。这显着提高了速度并允许平滑的页面更改。这一切都通过汇总打包成 CommonJS 格式,并与其他 IIFE 一起在每个页面上提供。
// This code assumes a Light Mode default
function checkColor() {
if (
/* This condition checks whether the user has set a site preference for dark mode OR a OS-level preference for Dark Mode AND no site preference */
localStorage.getItem('color-mode') === 'dark' ||
(window.matchMedia('(prefers-color-scheme: dark)').matches &&
!localStorage.getItem('color-mode'))) {
// if true, set the site to Dark Mode
document.documentElement.setAttribute('color-mode', 'dark')
}
}
checkColor();
window.addEventListener('DOMContentLoaded', (event) => {
// const icosa = document.getElementById("icosahedron");
if (
/* This condition checks whether the user has set a site preference for dark mode OR a OS-level preference for Dark Mode AND no site preference */
localStorage.getItem('color-mode') === 'dark' ||
(window.matchMedia('(prefers-color-scheme: dark)').matches &&
!localStorage.getItem('color-mode'))) {
// if true, set the site to Dark Mode
// icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
const toggleColorMode = e => {
// Switch to Light Mode
if (e.currentTarget.classList.contains("light--hidden")) {
// Sets the custom HTML attribute
document.documentElement.setAttribute("color-mode", "light");
// icosa.setAttribute("data", "scripts/icosahedron.html");
//Sets the user's preference in local storage
localStorage.setItem("color-mode", "light")
return;
}
/* Switch to Dark Mode
Sets the custom HTML attribute */
document.documentElement.setAttribute("color-mode", "dark");
// icosa.setAttribute("data", "scripts/icosahedron_dark.html");
// Sets the user's preference in local storage
localStorage.setItem("color-mode", "dark");
};
// Get the buttons in the DOM
let toggleIcons = document.querySelectorAll("#darkmode, #lightmode");
// Set up event listeners
toggleIcons.forEach(i => {
i.addEventListener("click", toggleColorMode);
});
});
最后,问题出在我的 javascript 试图在图标加载到 DOM 之前找到它。所以我添加了一个 DOMContentLoaded 函数,它在选择图标之前等待图标加载。我在顶部添加了一个函数,它使用 prefers-color-scheme 和 localStorage 匹配来检查系统是暗模式还是亮模式,并将这个脚本放在我的 HTML 头上。
为了浏览器兼容性,我的搜索功能全部封装在 IIFE 中:
(function (window, document) {
"use strict";
const cardHolder = document.getElementById("card-holder");
const searchField = document.getElementById("searchField");
const pressEnter = (e) => {
if (e.key === "Enter") {
searchField && searchField.blur();
}
};
const search = (e) => {
if (e.target.value) {
cardHolder.style.display = "none";
}
const results = window.searchIndex.search(e.target.value, {
bool: "AND",
expand: true,
});
const resEl = document.getElementById("searchResults");
const noResultsEl = document.getElementById("noResultsFound");
resEl.innerHTML = "";
if (Object.keys(results).length !== 0) {
noResultsEl.style.display = "none";
cardHolder.style.display = "none";
results.map((r) => {
const { id, title, categories, excerpt, date } = r.doc;
const el = document.createElement("div");
el.setAttribute(
"class",
"archive-card card border border-light shadow-0"
);
el.style.display = "flex";
resEl.appendChild(el);
const header = document.createElement("div");
header.setAttribute("id", "archive-header");
header.setAttribute("class", "card-header border-0");
el.appendChild(header);
const h3 = document.createElement("h3");
h3.setAttribute("class", "card-title pb-3");
h3.style.textDecoration = "underline";
header.appendChild(h3);
const a = document.createElement("a");
a.setAttribute("href", id);
a.setAttribute("class", "text-black");
a.textContent = title;
h3.appendChild(a);
const dateSection = document.createElement("div");
dateSection.setAttribute("class", "category-section text-muted");
var dateString = date;
dateSection.innerHTML += dateString;
header.appendChild(dateSection);
console.log(categories);
var catString = "";
for (let i = 0; i < categories.length; i++) {
if (i === 0) {
catString += ` [<a href="/category/${categories[i]}">${categories[i]}</a>, `;
} else if (i != categories.length - 1) {
catString += `<a href="/category/${categories[i]}">${categories[i]}</a>, `;
} else {
catString += `<a href="/category/${categories[i]}">${categories[i]}</a>]`;
}
}
// header.innerHTML += catString;
dateSection.innerHTML += catString;
const cardBody = document.createElement("div");
header.setAttribute("class", "card-body");
el.appendChild(cardBody);
const excerptP = document.createElement("p");
excerptP.setAttribute("class", "card-text");
excerptP.innerHTML += excerpt;
cardBody.appendChild(excerptP);
});
} else {
noResultsEl.style.display = "block";
cardHolder.style.display = "block";
}
};
fetch("/search-index.json").then((response) =>
response.json().then((rawIndex) => {
window.searchIndex = elasticlunr.Index.load(rawIndex);
document.getElementById("searchField").addEventListener("keyup", search);
document.getElementById("searchField").addEventListener("keydown", pressEnter);
})
);
})(window, document);
Rollup 将其转换为 CommonJS 并将其与我的其他 JS 一起缩小,仅在搜索页面上提供服务。
现在唯一不起作用的是自动切换嵌入的 HTML 对象的背景颜色,该对象有一个 HTML 脚本加载 Three.js 动画,但这有点既然其他一切都正常,那就最好把它留给自己的问题。
编辑:
忘记提及,Cloudflare Pages 无意中提供了一个外部 Javascript 文件夹,这是我在使用 Chrome 开发工具比较两个站点文件夹(本地和远程)后发现的。在清除 Cloudflare 页面缓存和刷新、删除 cookie 和站点数据后,颜色在页面更改时保存(Cloudflare 提供的其他 JS 文件导致我的 colorToggle 功能出现范围混乱问题)。
回答我关于切换嵌入式 HTML 背景颜色的问题,其中有一个 HTML 脚本加载 Three.js 动画与第三方 CSS。
我遵循了此处答案的建议:Putting three.js animation inside of div
我能够删除嵌入对象中对 HTML 和 CSS 的需要,并自己添加 CSS,然后深入研究 Three.js 代码并我自己添加 dom 元素。
现在它已嵌入并动态更改颜色。
对于我的个人博客网站 (https://victorfeight.com/) 在 Cloudflare 页面后面提供服务,我将 Eleventy 与自定义脚本一起使用,其中包括暗模式切换逻辑、带有 AJAX 的类似 SPA 的页面切换、以及使用 Elasticlunr 和 JavaScript 的搜索功能。由于我是 JavaScript Web 开发的新手并且选择不使用框架来实现 SPA 页面切换,我决定将所有逻辑实现到一个文件中,包装在一个 IIFE 中(传入 window 和文档)。我想知道这是否是一起实现这种逻辑的标准方法,或者我是否应该使用 webpack 或更现代的解决方案。我还想知道是否可以在每页加载暗 mode/light 模式脚本逻辑和搜索模式逻辑,尽管使用 AJAX url-切换逻辑。
第一个问题:某些点击事件处理逻辑似乎打断了我在同一文件中引入的新暗模式切换。 我在此处为暗模式实现了逻辑:https://jec.fyi/blog/supporting-dark-mode 我使用此处找到的逻辑实现了搜索功能:https://www.belter.io/eleventy-search/
我的 Liquid 模板中有以下 HTML,默认加载:
<div class="col-xl-6 px-0">
<div class="p-0 p-md-0 m-0 text-white">
<object type="text/html"
style="width: 100%;height: 30rem;min-width: 378px;" id="icosahedron"
data="scripts/icosahedron.html">
</object>
</div>
<!-- <iframe style="width: 100%;height: 30rem;min-width: 378px;" scrolling="no" id="icosahedron"
src="scripts/icosahedron.html" frameBorder="0" allowfullscreen></iframe> -->
</div>
我有 scripts/icosahedron.html 的轻量模式版本,它通过 scripts/icosahedron_dark.html 加载 scripts/script.js(轻量版本)加载 scripts/script_dark.js(深色版本)。
我想根据 light-mode/dark-mode 按钮的按下情况选择性地切换加载哪个脚本。
我为页面切换实现的逻辑(这似乎中断了包含在同一个 IIFE 中的暗模式切换)我从这里找到: https://github.com/learosema/eleventy-mini-spa
/**
* Load content into page without a whole page reload
* @param {string} href URL to route to
* @param {boolean} pushState whether to call history.pushState or not
*/
function load(href, pushState) {
const container = $("main");
const xhr = new XMLHttpRequest();
xhr.onload = function () {
fetchJSON();
const d = xhr.responseXML;
const dTitle = d.title || "";
const dContainer = $("main", d);
container.innerHTML = (dContainer && dContainer.innerHTML) || "";
document.title = dTitle;
if (pushState) {
history.pushState({}, dTitle, href);
}
container.focus();
window.scrollTo(0, 0);
};
xhr.onerror = function () {
// fallback to normal link behaviour
document.location.href = href;
return;
};
xhr.open("GET", href);
xhr.responseType = "document";
xhr.send();
}
function $(sel, con) {
return (con || document).querySelector(sel);
}
/**
* Search for a parent anchor tag outside a clicked event target
*
* @param {HTMLElement} el the clicked event target.
* @param {number} maxNests max number of levels to go up.
* @returns the anchor tag or null
*/
function findAnchorTag(el, maxNests = 3) {
for (let i = maxNests; el && i > 0; --i, el = el.parentNode) {
if (el.nodeName === "A") {
return el;
}
}
return null;
}
const links = document.getElementsByClassName("nav-link");
// Loop through the buttons and add the active class to the current/clicked button
for (var i = 0; i < links.length; i++) {
links[i].addEventListener("click", function () {
var current = document.getElementsByClassName("active");
if (current[0]) {
current[0].className = current[0].className.replace(" active", "");
this.className += " active";
}
});
}
window.addEventListener("click", function (evt) {
let baseUrl = $('meta[name="x-base-url"]')?.getAttribute("content") || "/";
const el = findAnchorTag(evt.target);
const href = el?.getAttribute("href");
if (el && href) {
if (
href.startsWith("#") ||
el.getAttribute("target") === "_blank" ||
/\.\w+$/.test(href)
) {
// eleventy urls in this configuration do not have extensions like .html
// if they have, or if target _blank is set, or they are a hash link,
// then do nothing.
return;
}
if (href.startsWith("/")) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
// if the URL starts with the base url, do the SPA handling
if (href.startsWith(baseUrl)) {
evt.preventDefault();
load(href, true);
}
}
});
window.addEventListener("popstate", function (e) {
load(document.location.pathname, false);
});
fetchJSON();
作为旁注,我发现我也必须在每个页面切换上重复我的 fetchJSON() 函数,才能使搜索正常工作,我想知道什么是最佳实践,因为我也是获取“Uncaught (in promise) TypeError: document.getElementById(...) is null fetchJSON https://victorfeight.com/scripts/mini-spa.js:9" 在调用搜索逻辑之前在错误堆栈中加载和加载。
请注意,我已将以下内容添加到单击事件侦听器中进行测试,但它似乎什么也没做:
if (href.startsWith("/")) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
点击处理逻辑如下:
window.addEventListener("click", function (evt) {
let baseUrl = $('meta[name="x-base-url"]')?.getAttribute("content") || "/";
const el = findAnchorTag(evt.target);
const href = el?.getAttribute("href");
if (el && href) {
if (
href.startsWith("#") ||
el.getAttribute("target") === "_blank" ||
/\.\w+$/.test(href)
) {
// eleventy urls in this configuration do not have extensions like .html
// if they have, or if target _blank is set, or they are a hash link,
// then do nothing.
return;
}
if (href.startsWith("/")) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
// if the URL starts with the base url, do the SPA handling
if (href.startsWith(baseUrl)) {
evt.preventDefault();
load(href, true);
}
}
});
我试过的,这是我的全彩模式逻辑:
"use strict";
const icosa = document.getElementById("icosahedron");
function changeColor() {
const bodyEl = document.body;
const themeStylesheet = document.getElementById("theme");
const themeToggle = document.getElementById("moon-1");
const DARK = "dark";
const LIGHT = "light";
const COLOR_SCHEME_CHANGED = "colorSchemeChanged";
themeToggle.addEventListener("click", () => {
const isDark = bodyEl.classList.toggle("dark-mode");
const mode = isDark ? DARK : LIGHT;
sessionStorage.setItem("jec.color-scheme", mode);
if (isDark) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
themeToggle.className = "far fa-sun fa-2x";
themeToggle.title = themeToggle.title.replace(DARK, LIGHT);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(LIGHT, DARK);
} else {
icosa.setAttribute("data", "scripts/icosahedron.html");
themeToggle.className = "far fa-moon fa-2x";
themeToggle.title = themeToggle.title.replace(LIGHT, DARK);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(DARK, LIGHT);
}
themeToggle.dispatchEvent(
new CustomEvent(COLOR_SCHEME_CHANGED, { detail: mode })
);
});
}
changeColor();
function init() {
const DARK = "dark";
const LIGHT = "light";
const isSystemDarkMode =
matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
let mode = sessionStorage.getItem("jec.color-scheme");
if (!mode && isSystemDarkMode) {
mode = DARK;
} else {
mode = mode || LIGHT;
}
if (mode === DARK) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
document.getElementById("moon-1").click();
}
}
// run the code
init();
const pressEnter = (e) => {
const searchField = document.getElementById("searchField");
if (e.key === "Enter") {
if (searchField) {
searchField && searchField.blur();
}
}
};
我为使颜色模式工作而添加的内容,在我的 init() 函数中,我修改了数据属性以指向正确的文件:
if (mode === DARK) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
并且在我相关的改色函数中:
if (isDark) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
themeToggle.className = "far fa-sun fa-2x";
themeToggle.title = themeToggle.title.replace(DARK, LIGHT);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(LIGHT, DARK);
} else {
icosa.setAttribute("data", "scripts/icosahedron.html");
现在逻辑在首页上起作用了。但是,一旦我使用顶部导航切换页面并返回主页,选择性加载脚本的逻辑就会停止工作,我假设范围有问题。
所以我的问题是,我怎样才能让这个暗模式脚本切换逻辑与这个 link 页面切换逻辑很好地协同工作?将所有三个脚本的逻辑包装在一个 IIFE 中是最好的方法,还是我应该寻找其他技术来更好地模块化它?目前,通过 Cloudflare 页面提供的生产构建根本不加载选择性暗模式脚本逻辑,只有本地加载。包含的屏幕截图来自本地。我还对如何在暗模式切换时为我的搜索背景图像有选择地加载 CSS 感兴趣。我很感激任何建议。
这是完整的脚本,为了完成:
(function (window, document) {
"use strict";
const icosa = document.getElementById("icosahedron");
function changeColor() {
const bodyEl = document.body;
const themeStylesheet = document.getElementById("theme");
const themeToggle = document.getElementById("moon-1");
const DARK = "dark";
const LIGHT = "light";
const COLOR_SCHEME_CHANGED = "colorSchemeChanged";
themeToggle.addEventListener("click", () => {
const isDark = bodyEl.classList.toggle("dark-mode");
const mode = isDark ? DARK : LIGHT;
sessionStorage.setItem("jec.color-scheme", mode);
if (isDark) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
themeToggle.className = "far fa-sun fa-2x";
themeToggle.title = themeToggle.title.replace(DARK, LIGHT);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(LIGHT, DARK);
} else {
icosa.setAttribute("data", "scripts/icosahedron.html");
themeToggle.className = "far fa-moon fa-2x";
themeToggle.title = themeToggle.title.replace(LIGHT, DARK);
if (themeStylesheet)
themeStylesheet.href = themeStylesheet.href.replace(DARK, LIGHT);
}
themeToggle.dispatchEvent(
new CustomEvent(COLOR_SCHEME_CHANGED, { detail: mode })
);
});
}
changeColor();
function init() {
const DARK = "dark";
const LIGHT = "light";
const isSystemDarkMode =
matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
let mode = sessionStorage.getItem("jec.color-scheme");
if (!mode && isSystemDarkMode) {
mode = DARK;
} else {
mode = mode || LIGHT;
}
if (mode === DARK) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
document.getElementById("moon-1").click();
}
}
// run the code
init();
const pressEnter = (e) => {
const searchField = document.getElementById("searchField");
if (e.key === "Enter") {
if (searchField) {
searchField && searchField.blur();
}
}
};
const search = (e) => {
const cardHolder = document.getElementById("card-holder");
if (e.target.value) {
cardHolder.style.display = "none";
}
const results = window.searchIndex.search(e.target.value, {
bool: "AND",
expand: true,
});
const resEl = document.getElementById("searchResults");
const noResultsEl = document.getElementById("noResultsFound");
resEl.innerHTML = "";
if (Object.keys(results).length !== 0) {
noResultsEl.style.display = "none";
cardHolder.style.display = "none";
results.map((r) => {
const { id, title, categories, excerpt, date } = r.doc;
const el = document.createElement("div");
el.setAttribute(
"class",
"archive-card card border border-light shadow-0"
);
el.style.display = "flex";
resEl.appendChild(el);
const header = document.createElement("div");
header.setAttribute("id", "archive-header");
header.setAttribute("class", "card-header border-0");
el.appendChild(header);
const h3 = document.createElement("h3");
h3.setAttribute("class", "card-title pb-3");
h3.style.textDecoration = "underline";
header.appendChild(h3);
const a = document.createElement("a");
a.setAttribute("href", id);
a.setAttribute("class", "text-black");
a.textContent = title;
h3.appendChild(a);
const dateSection = document.createElement("div");
dateSection.setAttribute("class", "category-section text-muted");
var dateString = date;
dateSection.innerHTML += dateString;
header.appendChild(dateSection);
console.log(categories);
var catString = "";
for (let i = 0; i < categories.length; i++) {
if (i === 0) {
catString += ` [<a href="/category/${categories[i]}">${categories[i]}</a>, `;
} else if (i != categories.length - 1) {
catString += `<a href="/category/${categories[i]}">${categories[i]}</a>, `;
} else {
catString += `<a href="/category/${categories[i]}">${categories[i]}</a>]`;
}
}
// header.innerHTML += catString;
dateSection.innerHTML += catString;
const cardBody = document.createElement("div");
header.setAttribute("class", "card-body");
el.appendChild(cardBody);
const excerptP = document.createElement("p");
excerptP.setAttribute("class", "card-text");
excerptP.innerHTML += excerpt;
cardBody.appendChild(excerptP);
});
} else {
noResultsEl.style.display = "block";
cardHolder.style.display = "block";
}
};
function fetchJSON() {
fetch("/search-index.json").then((response) =>
response.json().then((rawIndex) => {
window.searchIndex = elasticlunr.Index.load(rawIndex);
document
.getElementById("searchField")
.addEventListener("keyup", search);
document
.getElementById("searchField")
.addEventListener("keydown", pressEnter);
})
);
}
fetchJSON();
/**
* Load content into page without a whole page reload
* @param {string} href URL to route to
* @param {boolean} pushState whether to call history.pushState or not
*/
function load(href, pushState) {
const container = $("main");
const xhr = new XMLHttpRequest();
xhr.onload = function () {
fetchJSON();
const d = xhr.responseXML;
const dTitle = d.title || "";
const dContainer = $("main", d);
container.innerHTML = (dContainer && dContainer.innerHTML) || "";
document.title = dTitle;
if (pushState) {
history.pushState({}, dTitle, href);
}
container.focus();
window.scrollTo(0, 0);
};
xhr.onerror = function () {
// fallback to normal link behaviour
document.location.href = href;
return;
};
xhr.open("GET", href);
xhr.responseType = "document";
xhr.send();
}
function $(sel, con) {
return (con || document).querySelector(sel);
}
/**
* Search for a parent anchor tag outside a clicked event target
*
* @param {HTMLElement} el the clicked event target.
* @param {number} maxNests max number of levels to go up.
* @returns the anchor tag or null
*/
function findAnchorTag(el, maxNests = 3) {
for (let i = maxNests; el && i > 0; --i, el = el.parentNode) {
if (el.nodeName === "A") {
return el;
}
}
return null;
}
const links = document.getElementsByClassName("nav-link");
// Loop through the buttons and add the active class to the current/clicked button
for (var i = 0; i < links.length; i++) {
links[i].addEventListener("click", function () {
var current = document.getElementsByClassName("active");
if (current[0]) {
current[0].className = current[0].className.replace(" active", "");
this.className += " active";
}
});
}
window.addEventListener("click", function (evt) {
let baseUrl = $('meta[name="x-base-url"]')?.getAttribute("content") || "/";
const el = findAnchorTag(evt.target);
const href = el?.getAttribute("href");
if (el && href) {
if (
href.startsWith("#") ||
el.getAttribute("target") === "_blank" ||
/\.\w+$/.test(href)
) {
// eleventy urls in this configuration do not have extensions like .html
// if they have, or if target _blank is set, or they are a hash link,
// then do nothing.
return;
}
if (href.startsWith("/")) {
icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
// if the URL starts with the base url, do the SPA handling
if (href.startsWith(baseUrl)) {
evt.preventDefault();
load(href, true);
}
}
});
window.addEventListener("popstate", function (e) {
load(document.location.pathname, false);
});
fetchJSON();
})(window, document);
让我迈出这一步。
第一期:点击事件处理逻辑
我 re-implemented 我所有的 javascript 逻辑都放在单独的文件中,现在 Rollup.js 缩小了。
这是我的新汇总构建配置:
import path from "path";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import replace from "@rollup/plugin-replace";
import { terser } from "rollup-plugin-terser";
let fileDest = `mdb.min`;
const external = ["@popperjs/core"];
const plugins = [terser()];
const globals = {
"@popperjs/core": "Popper",
};
export default [
{
input: path.resolve(__dirname, `_11ty_scripts/js/mdb.free.js`),
output: {
file: path.resolve(__dirname, `scripts/${fileDest}.js`),
format: "umd",
name: "bootstrap",
globals,
},
external,
plugins,
},
{
input: ["_11ty_scripts/search.js"],
output: [
{
file: "scripts/search.min.js",
format: "cjs",
sourcemap: true,
plugins: [terser()],
},
],
},
{
input: ["_11ty_scripts/main.js"],
output: [
{
file: "scripts/min.js",
format: "iife",
sourcemap: true,
plugins: [terser()],
},
],
},
{
input: ["_11ty_scripts/colorToggle.js"],
output: [
{
file: "scripts/colorToggle.min.js",
format: "cjs",
sourcemap: true,
plugins: [terser()],
},
],
},
];
这是捆绑了 mdb.free.js 的自定义版本,它使用一些静态分析和 tree-shaking 从结果中删除未使用的代码,然后通过 terser() 缩小。
我将 search.js 和 main.js 分开到它们自己的外部文件中。我从 https://github.com/google/eleventy-high-performance-blog 中获取的很多 main.js 逻辑,我正在用悬停时预取链接替换我原来的 spa-like ajax 开关:
const exposed = {};
if (location.search) {
var a = document.createElement("a");
a.href = location.href;
a.search = "";
history.replaceState(null, null, a.href);
}
function tweet_(url) {
open(
"https://twitter.com/intent/tweet?url=" + encodeURIComponent(url),
"_blank"
);
}
function tweet(anchor) {
tweet_(anchor.getAttribute("href"));
}
expose("tweet", tweet);
function share(anchor) {
var url = anchor.getAttribute("href");
event.preventDefault();
tweet_(url);
//if (navigator.share) {
// navigator.share({
// url: url,
// });
//} else if (navigator.clipboard) {
// navigator.clipboard.writeText(url);
// message("Article URL copied to clipboard.");
//} else {
// tweet_(url);
//}
}
expose("share", share);
function message(msg) {
var dialog = document.getElementById("message");
dialog.textContent = msg;
dialog.setAttribute("open", "");
setTimeout(function () {
dialog.removeAttribute("open");
}, 3000);
}
function prefetch(e) {
if (e.target.tagName != "A") {
return;
}
if (e.target.origin != location.origin) {
return;
}
/**
* Return the given url with no fragment
* @param {string} url potentially containing a fragment
* @return {string} url without fragment
*/
const removeUrlFragment = (url) => url.split("#")[0];
if (
removeUrlFragment(window.location.href) === removeUrlFragment(e.target.href)
) {
return;
}
var l = document.createElement("link");
l.rel = "prefetch";
l.href = e.target.href;
document.head.appendChild(l);
}
document.documentElement.addEventListener("mouseover", prefetch, {
capture: true,
passive: true,
});
document.documentElement.addEventListener("touchstart", prefetch, {
capture: true,
passive: true,
});
const GA_ID = document.documentElement.getAttribute("ga-id");
window.ga =
window.ga ||
function () {
if (!GA_ID) {
return;
}
(ga.q = ga.q || []).push(arguments);
};
ga.l = +new Date();
ga("create", GA_ID, "auto");
ga("set", "transport", "beacon");
var timeout = setTimeout(
(onload = function () {
clearTimeout(timeout);
ga("send", "pageview");
}),
1000
);
var ref = +new Date();
function ping(event) {
var now = +new Date();
if (now - ref < 1000) {
return;
}
ga("send", {
hitType: "event",
eventCategory: "page",
eventAction: event.type,
eventLabel: Math.round((now - ref) / 1000),
});
ref = now;
}
addEventListener("pagehide", ping);
addEventListener("visibilitychange", ping);
/**
* Injects a script into document.head
* @param {string} src path of script to be injected in <head>
* @return {Promise} Promise object that resolves on script load event
*/
const dynamicScriptInject = (src) => {
return new Promise((resolve) => {
const script = document.createElement("script");
script.src = src;
script.type = "text/javascript";
document.head.appendChild(script);
script.addEventListener("load", () => {
resolve(script);
});
});
};
// Script web-vitals.js will be injected dynamically if user opts-in to sending CWV data.
const sendWebVitals = document.currentScript.getAttribute("data-cwv-src");
if (/web-vitals.js/.test(sendWebVitals)) {
dynamicScriptInject(`${window.location.origin}/scripts/web-vitals.js`)
.then(() => {
webVitals.getCLS(sendToGoogleAnalytics);
webVitals.getFID(sendToGoogleAnalytics);
webVitals.getLCP(sendToGoogleAnalytics);
})
.catch((error) => {
console.error(error);
});
}
addEventListener(
"click",
function (e) {
var button = e.target.closest("button");
if (!button) {
return;
}
ga("send", {
hitType: "event",
eventCategory: "button",
eventAction: button.getAttribute("aria-label") || button.textContent,
});
},
true
);
var selectionTimeout;
addEventListener(
"selectionchange",
function () {
clearTimeout(selectionTimeout);
var text = String(document.getSelection()).trim();
if (text.split(/[\s\n\r]+/).length < 3) {
return;
}
selectionTimeout = setTimeout(function () {
ga("send", {
hitType: "event",
eventCategory: "selection",
eventAction: text,
});
}, 2000);
},
true
);
function expose(name, fn) {
exposed[name] = fn;
}
addEventListener("click", (e) => {
const handler = e.target.closest("[on-click]");
if (!handler) {
return;
}
e.preventDefault();
const name = handler.getAttribute("on-click");
const fn = exposed[name];
if (!fn) {
throw new Error("Unknown handler" + name);
}
fn(handler);
});
function removeBlurredImage(img) {
// Ensure the browser doesn't try to draw the placeholder when the real image is present.
img.style.backgroundImage = "none";
}
document.body.addEventListener(
"load",
(e) => {
if (e.target.tagName != "IMG") {
return;
}
removeBlurredImage(e.target);
},
/* capture */ "true"
);
for (let img of document.querySelectorAll("img")) {
if (img.complete) {
removeBlurredImage(img);
}
}
这一切都通过汇总汇总到一个 IIFE 中,并在每个页面上提供。 那里发生了很多事情,但本质上它是用于 Twitter 共享和预取链接的逻辑,尽管它确实添加了一些点击处理逻辑,我的 colorToggle 逻辑需要小心回避这些逻辑。
对于 colorToggle,我重新实现了使用 javascript 在两个 href 样式表之间交换的原始逻辑,现在在 CSS 中的两个 color-mode 属性之间交换并记住 localStorage 的选择。这显着提高了速度并允许平滑的页面更改。这一切都通过汇总打包成 CommonJS 格式,并与其他 IIFE 一起在每个页面上提供。
// This code assumes a Light Mode default
function checkColor() {
if (
/* This condition checks whether the user has set a site preference for dark mode OR a OS-level preference for Dark Mode AND no site preference */
localStorage.getItem('color-mode') === 'dark' ||
(window.matchMedia('(prefers-color-scheme: dark)').matches &&
!localStorage.getItem('color-mode'))) {
// if true, set the site to Dark Mode
document.documentElement.setAttribute('color-mode', 'dark')
}
}
checkColor();
window.addEventListener('DOMContentLoaded', (event) => {
// const icosa = document.getElementById("icosahedron");
if (
/* This condition checks whether the user has set a site preference for dark mode OR a OS-level preference for Dark Mode AND no site preference */
localStorage.getItem('color-mode') === 'dark' ||
(window.matchMedia('(prefers-color-scheme: dark)').matches &&
!localStorage.getItem('color-mode'))) {
// if true, set the site to Dark Mode
// icosa.setAttribute("data", "scripts/icosahedron_dark.html");
}
const toggleColorMode = e => {
// Switch to Light Mode
if (e.currentTarget.classList.contains("light--hidden")) {
// Sets the custom HTML attribute
document.documentElement.setAttribute("color-mode", "light");
// icosa.setAttribute("data", "scripts/icosahedron.html");
//Sets the user's preference in local storage
localStorage.setItem("color-mode", "light")
return;
}
/* Switch to Dark Mode
Sets the custom HTML attribute */
document.documentElement.setAttribute("color-mode", "dark");
// icosa.setAttribute("data", "scripts/icosahedron_dark.html");
// Sets the user's preference in local storage
localStorage.setItem("color-mode", "dark");
};
// Get the buttons in the DOM
let toggleIcons = document.querySelectorAll("#darkmode, #lightmode");
// Set up event listeners
toggleIcons.forEach(i => {
i.addEventListener("click", toggleColorMode);
});
});
最后,问题出在我的 javascript 试图在图标加载到 DOM 之前找到它。所以我添加了一个 DOMContentLoaded 函数,它在选择图标之前等待图标加载。我在顶部添加了一个函数,它使用 prefers-color-scheme 和 localStorage 匹配来检查系统是暗模式还是亮模式,并将这个脚本放在我的 HTML 头上。
为了浏览器兼容性,我的搜索功能全部封装在 IIFE 中:
(function (window, document) {
"use strict";
const cardHolder = document.getElementById("card-holder");
const searchField = document.getElementById("searchField");
const pressEnter = (e) => {
if (e.key === "Enter") {
searchField && searchField.blur();
}
};
const search = (e) => {
if (e.target.value) {
cardHolder.style.display = "none";
}
const results = window.searchIndex.search(e.target.value, {
bool: "AND",
expand: true,
});
const resEl = document.getElementById("searchResults");
const noResultsEl = document.getElementById("noResultsFound");
resEl.innerHTML = "";
if (Object.keys(results).length !== 0) {
noResultsEl.style.display = "none";
cardHolder.style.display = "none";
results.map((r) => {
const { id, title, categories, excerpt, date } = r.doc;
const el = document.createElement("div");
el.setAttribute(
"class",
"archive-card card border border-light shadow-0"
);
el.style.display = "flex";
resEl.appendChild(el);
const header = document.createElement("div");
header.setAttribute("id", "archive-header");
header.setAttribute("class", "card-header border-0");
el.appendChild(header);
const h3 = document.createElement("h3");
h3.setAttribute("class", "card-title pb-3");
h3.style.textDecoration = "underline";
header.appendChild(h3);
const a = document.createElement("a");
a.setAttribute("href", id);
a.setAttribute("class", "text-black");
a.textContent = title;
h3.appendChild(a);
const dateSection = document.createElement("div");
dateSection.setAttribute("class", "category-section text-muted");
var dateString = date;
dateSection.innerHTML += dateString;
header.appendChild(dateSection);
console.log(categories);
var catString = "";
for (let i = 0; i < categories.length; i++) {
if (i === 0) {
catString += ` [<a href="/category/${categories[i]}">${categories[i]}</a>, `;
} else if (i != categories.length - 1) {
catString += `<a href="/category/${categories[i]}">${categories[i]}</a>, `;
} else {
catString += `<a href="/category/${categories[i]}">${categories[i]}</a>]`;
}
}
// header.innerHTML += catString;
dateSection.innerHTML += catString;
const cardBody = document.createElement("div");
header.setAttribute("class", "card-body");
el.appendChild(cardBody);
const excerptP = document.createElement("p");
excerptP.setAttribute("class", "card-text");
excerptP.innerHTML += excerpt;
cardBody.appendChild(excerptP);
});
} else {
noResultsEl.style.display = "block";
cardHolder.style.display = "block";
}
};
fetch("/search-index.json").then((response) =>
response.json().then((rawIndex) => {
window.searchIndex = elasticlunr.Index.load(rawIndex);
document.getElementById("searchField").addEventListener("keyup", search);
document.getElementById("searchField").addEventListener("keydown", pressEnter);
})
);
})(window, document);
Rollup 将其转换为 CommonJS 并将其与我的其他 JS 一起缩小,仅在搜索页面上提供服务。
现在唯一不起作用的是自动切换嵌入的 HTML 对象的背景颜色,该对象有一个 HTML 脚本加载 Three.js 动画,但这有点既然其他一切都正常,那就最好把它留给自己的问题。
编辑: 忘记提及,Cloudflare Pages 无意中提供了一个外部 Javascript 文件夹,这是我在使用 Chrome 开发工具比较两个站点文件夹(本地和远程)后发现的。在清除 Cloudflare 页面缓存和刷新、删除 cookie 和站点数据后,颜色在页面更改时保存(Cloudflare 提供的其他 JS 文件导致我的 colorToggle 功能出现范围混乱问题)。
回答我关于切换嵌入式 HTML 背景颜色的问题,其中有一个 HTML 脚本加载 Three.js 动画与第三方 CSS。
我遵循了此处答案的建议:Putting three.js animation inside of div
我能够删除嵌入对象中对 HTML 和 CSS 的需要,并自己添加 CSS,然后深入研究 Three.js 代码并我自己添加 dom 元素。 现在它已嵌入并动态更改颜色。