feat(theme): add style switcher and centralize diff logic
Extract duplicated diff rendering logic into shared/diff-page.js Implement theme switcher component across all templates
This commit is contained in:
293
shared/diff-page.js
Normal file
293
shared/diff-page.js
Normal file
@@ -0,0 +1,293 @@
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
const DEFAULT_THEMES = [
|
||||
{ id: "light", label: "轻量", path: "index.html" },
|
||||
{ id: "cyberpunk", label: "赛博", path: "cyberpunk/index.html" },
|
||||
{ id: "macos", label: "macOS", path: "macos/index.html" },
|
||||
{ id: "matrix", label: "Matrix", path: "matrix/index.html" },
|
||||
{ id: "notion", label: "Notion", path: "notion/index.html" },
|
||||
{ id: "synthwave", label: "蒸汽波", path: "synthwave/index.html" },
|
||||
{ id: "exe", label: "EXE", path: "exe/index.html" },
|
||||
];
|
||||
|
||||
function resolveThemeUrl(targetPath, currentPath) {
|
||||
try {
|
||||
const url = new URL(global.location.href);
|
||||
if (url.pathname.endsWith(currentPath)) {
|
||||
url.pathname =
|
||||
url.pathname.slice(0, -currentPath.length) + targetPath;
|
||||
return url.toString();
|
||||
}
|
||||
return new URL(targetPath, global.location.href).toString();
|
||||
} catch (error) {
|
||||
return targetPath;
|
||||
}
|
||||
}
|
||||
|
||||
function initStyleSwitcher(config) {
|
||||
const host = document.querySelector(config.hostSelector || "[data-style-switcher]");
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
|
||||
const themes = config.themes || DEFAULT_THEMES;
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "style-switcher";
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.className = "style-switcher-label";
|
||||
label.textContent = config.switcherLabel || "风格";
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.className = "style-switcher-select";
|
||||
select.setAttribute(
|
||||
"aria-label",
|
||||
config.switcherAriaLabel || "切换界面风格",
|
||||
);
|
||||
|
||||
themes.forEach((theme) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = resolveThemeUrl(theme.path, config.currentThemePath);
|
||||
option.textContent = theme.label;
|
||||
option.selected = theme.id === config.themeId;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
select.addEventListener("change", () => {
|
||||
global.location.href = select.value;
|
||||
});
|
||||
|
||||
wrapper.appendChild(label);
|
||||
wrapper.appendChild(select);
|
||||
host.replaceChildren(wrapper);
|
||||
}
|
||||
|
||||
function getElement(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function initDiffPage(config) {
|
||||
const leftTextarea = getElement(config.ids?.leftTextarea || "leftTextarea");
|
||||
const rightTextarea = getElement(
|
||||
config.ids?.rightTextarea || "rightTextarea",
|
||||
);
|
||||
const compareBtn = getElement(config.ids?.compareBtn || "compareBtn");
|
||||
const swapBtn = getElement(config.ids?.swapBtn || "swapBtn");
|
||||
const exampleBtn = getElement(config.ids?.exampleBtn || "exampleBtn");
|
||||
const clearLeftBtn = getElement(config.ids?.clearLeftBtn || "clearLeftBtn");
|
||||
const clearRightBtn = getElement(
|
||||
config.ids?.clearRightBtn || "clearRightBtn",
|
||||
);
|
||||
const languageSelect = getElement(
|
||||
config.ids?.languageSelect || "languageSelect",
|
||||
);
|
||||
const diffOutput = getElement(config.ids?.diffOutput || "diff-output");
|
||||
const diffStats = getElement(config.ids?.diffStats || "diffStats");
|
||||
const viewOptions = document.querySelectorAll(
|
||||
config.selectors?.viewOptions || ".view-option",
|
||||
);
|
||||
|
||||
if (
|
||||
!leftTextarea ||
|
||||
!rightTextarea ||
|
||||
!compareBtn ||
|
||||
!swapBtn ||
|
||||
!exampleBtn ||
|
||||
!clearLeftBtn ||
|
||||
!clearRightBtn ||
|
||||
!languageSelect ||
|
||||
!diffOutput ||
|
||||
!diffStats ||
|
||||
viewOptions.length === 0
|
||||
) {
|
||||
throw new Error("Diff page initialization failed: missing required DOM nodes.");
|
||||
}
|
||||
|
||||
const messages = config.messages || {};
|
||||
const example = config.example || {};
|
||||
const normalizeLanguage =
|
||||
config.normalizeLanguage || ((language) => language);
|
||||
const formatStats =
|
||||
config.formatStats ||
|
||||
((result) => {
|
||||
if (result.identical) {
|
||||
return "No changes";
|
||||
}
|
||||
return `+${result.added} / -${result.deleted}`;
|
||||
});
|
||||
|
||||
let currentView =
|
||||
config.initialView ||
|
||||
Array.from(viewOptions).find((button) => button.classList.contains("active"))
|
||||
?.getAttribute("data-view") ||
|
||||
"side-by-side";
|
||||
|
||||
function setOutput(html, statsHtml) {
|
||||
diffOutput.innerHTML = html;
|
||||
diffStats.innerHTML = statsHtml || "";
|
||||
}
|
||||
|
||||
function getSelectedLanguage() {
|
||||
return normalizeLanguage(languageSelect.value);
|
||||
}
|
||||
|
||||
function getHighlightConfig() {
|
||||
const language = getSelectedLanguage();
|
||||
if (language === "plaintext") {
|
||||
return false;
|
||||
}
|
||||
return { enabled: true, language: language };
|
||||
}
|
||||
|
||||
function generateUnifiedDiff(original, modified) {
|
||||
return Diff.createTwoFilesPatch(
|
||||
config.fileLabels?.left || "Original",
|
||||
config.fileLabels?.right || "Modified",
|
||||
original || "",
|
||||
modified || "",
|
||||
"",
|
||||
"",
|
||||
{ context: config.contextLines || 4 },
|
||||
);
|
||||
}
|
||||
|
||||
function applyHighlighting() {
|
||||
const selectedLanguage = getSelectedLanguage();
|
||||
if (selectedLanguage === "plaintext" || !global.hljs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const codeBlocks = diffOutput.querySelectorAll("code");
|
||||
codeBlocks.forEach((block) => {
|
||||
block.classList.forEach((className) => {
|
||||
if (className.startsWith("language-")) {
|
||||
block.classList.remove(className);
|
||||
}
|
||||
});
|
||||
block.classList.add("language-" + selectedLanguage);
|
||||
delete block.dataset.highlighted;
|
||||
try {
|
||||
global.hljs.highlightElement(block);
|
||||
} catch (error) {}
|
||||
});
|
||||
}
|
||||
|
||||
function renderDiff() {
|
||||
let diffString = "";
|
||||
try {
|
||||
diffString = generateUnifiedDiff(leftTextarea.value, rightTextarea.value);
|
||||
} catch (error) {
|
||||
setOutput(
|
||||
typeof messages.generateError === "function"
|
||||
? messages.generateError(error)
|
||||
: "<div>Diff generation failed.</div>",
|
||||
messages.generateErrorStats || "",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let diffHtml = "";
|
||||
try {
|
||||
diffHtml = Diff2Html.html(diffString, {
|
||||
drawFileList: false,
|
||||
matching: "lines",
|
||||
outputFormat: currentView,
|
||||
highlight: getHighlightConfig(),
|
||||
renderNothingWhenEmpty: false,
|
||||
});
|
||||
} catch (error) {
|
||||
setOutput(
|
||||
typeof messages.renderError === "function"
|
||||
? messages.renderError(error)
|
||||
: "<div>Diff rendering failed.</div>",
|
||||
messages.renderErrorStats || "",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
diffOutput.innerHTML = diffHtml;
|
||||
applyHighlighting();
|
||||
|
||||
const added = diffOutput.querySelectorAll(".d2h-ins").length;
|
||||
const deleted = diffOutput.querySelectorAll(".d2h-del").length;
|
||||
const identical = added === 0 && deleted === 0;
|
||||
|
||||
diffStats.innerHTML = formatStats({
|
||||
added: added,
|
||||
deleted: deleted,
|
||||
identical: identical,
|
||||
});
|
||||
|
||||
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
|
||||
setOutput(messages.blankResult || "<div>Files are identical.</div>", messages.blankStats || "");
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveView(view, shouldRender) {
|
||||
currentView = view;
|
||||
viewOptions.forEach((button) => {
|
||||
button.classList.toggle(
|
||||
"active",
|
||||
button.getAttribute("data-view") === view,
|
||||
);
|
||||
});
|
||||
|
||||
if (shouldRender !== false) {
|
||||
renderDiff();
|
||||
}
|
||||
}
|
||||
|
||||
function loadExample() {
|
||||
leftTextarea.value = example.left || "";
|
||||
rightTextarea.value = example.right || "";
|
||||
if (example.language) {
|
||||
languageSelect.value = example.language;
|
||||
}
|
||||
renderDiff();
|
||||
}
|
||||
|
||||
function swapTexts() {
|
||||
const currentLeft = leftTextarea.value;
|
||||
leftTextarea.value = rightTextarea.value;
|
||||
rightTextarea.value = currentLeft;
|
||||
renderDiff();
|
||||
}
|
||||
|
||||
function clearLeft() {
|
||||
leftTextarea.value = "";
|
||||
renderDiff();
|
||||
}
|
||||
|
||||
function clearRight() {
|
||||
rightTextarea.value = "";
|
||||
renderDiff();
|
||||
}
|
||||
|
||||
compareBtn.addEventListener("click", renderDiff);
|
||||
swapBtn.addEventListener("click", swapTexts);
|
||||
exampleBtn.addEventListener("click", loadExample);
|
||||
clearLeftBtn.addEventListener("click", clearLeft);
|
||||
clearRightBtn.addEventListener("click", clearRight);
|
||||
languageSelect.addEventListener("change", renderDiff);
|
||||
|
||||
viewOptions.forEach((button) => {
|
||||
button.addEventListener("click", (event) => {
|
||||
setActiveView(event.currentTarget.getAttribute("data-view"));
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
renderDiff();
|
||||
}
|
||||
});
|
||||
|
||||
initStyleSwitcher(config);
|
||||
setActiveView(currentView, false);
|
||||
renderDiff();
|
||||
}
|
||||
|
||||
global.initDiffPage = initDiffPage;
|
||||
})(window);
|
||||
Reference in New Issue
Block a user