version switch: turn into a custom element #104778

Open
Tobias Heinke wants to merge 3 commits from Tobias/blender-manual:switcher-two into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
3 changed files with 199 additions and 181 deletions

View File

@ -1,32 +1,36 @@
<div class="rst-versions" data-toggle="rst-versions" role="note" aria-label="Document versions"> <div class="rst-versions" data-toggle="rst-versions" role="note" aria-label="Document versions">
<ul id="versionwrap" role="presentation"> <ul id="version-menus" role="presentation">
<li role="presentation"> <li role="presentation">
<button id="version-popover" class="version-btn" tabindex="0" type="button" aria-label="Versions selector" <version-switch type="version" class="closed">
aria-haspopup="true" aria-owns="version-vsnlist" aria-disabled="true"> <button tabindex="0" type="button" aria-label="Versions selector"
aria-haspopup="true" aria-owns="version-dialog-version">
{{ release }} {{ release }}
</button> </button>
<div class="version-dialog" aria-hidden="true"> <div id="version-dialog-version" class="version-dialog" aria-hidden="true">
<div class="version-title" aria-role="heading">Versions</div> <div id="version-title-version" class="version-title" aria-role="heading">Versions</div>
<ul id="version-vsnlist" class="version-list" role="menu" aria-labelledby="version-popover" aria-hidden="true"> <ul role="menu" aria-labelledby="version-title-version" aria-hidden="true">
<li role="presentation">Loading...</li> <li role="presentation">Loading...</li>
</ul> </ul>
</div> </div>
</version-switch>
</li> </li>
<li role="presentation"> <li role="presentation">
<button id="lang-popover" class="version-btn" tabindex="0" type="button" aria-label="Language selector" <version-switch type="lang" class="closed">
aria-haspopup="true" aria-owns="version-langlist"> <button tabindex="0" type="button" aria-label="Language selector"
aria-haspopup="true" aria-owns="version-dialog-lang">
{% if language is not none %} {{ language }} {% else %} en {% endif %} {% if language is not none %} {{ language }} {% else %} en {% endif %}
</button> </button>
<div class="version-dialog" aria-hidden="true"> <div id="version-dialog-lang" class="version-dialog" aria-hidden="true">
<div class="version-title" aria-role="heading">Languages</div> <div id="version-title-lang" class="version-title" aria-role="heading">Languages</div>
<ul id="version-langlist" class="version-list" role="menu" aria-labelledby="lang-popover" aria-hidden="true"> <ul role="menu" aria-labelledby="version-title-lang" aria-hidden="true">
<li role="presentation">Loading...</li> <li role="presentation">Loading...</li>
</ul> </ul>
</div> </div>
</version-switch>
</li> </li>
<template id="version-entry"> <template id="version-entry">
<li tabindex="-1" role="presentation"><a tabindex="-1" role="menuitem"></a></li> <li tabindex="-1" role="presentation"><a tabindex="-1" role="menuitem"></a></li>
<li class="selected" tabindex="-1" role="presentation"><span tabindex="-1" aria-current="page"></span></li> <li class="current" tabindex="-1" role="presentation"><span tabindex="-1" aria-current="page"></span></li>
</template> </template>
</ul> </ul>
<template id="version-warning"> <template id="version-warning">

View File

@ -1,4 +1,4 @@
#versionwrap { #version-menus {
margin: 0; margin: 0;
display: flex; display: flex;
padding-top: 2px; padding-top: 2px;
@ -6,18 +6,20 @@
font-size: var(--sidebar-item-font-size); font-size: var(--sidebar-item-font-size);
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
}
#versionwrap>ul {
list-style: none; list-style: none;
} }
#versionwrap>li { #version-menus>li {
display: flex; display: flex;
width: 50%; width: 50%;
} }
.version-btn { version-switch {
display: flex;
flex-grow: 1;
}
version-switch button {
display: inline-block; display: inline-block;
background-color: var(--color-sidebar-background); background-color: var(--color-sidebar-background);
width: 100%; width: 100%;
@ -32,37 +34,39 @@
z-index: 400; z-index: 400;
} }
.version-btn-open::after { version-switch.open button::after {
color: gray; color: gray;
} }
.version-btn:hover, version-switch button:hover,
.version-btn:focus { version-switch button:focus {
border-color: #525252; border-color: #525252;
} }
.version-btn-open { version-switch.open button {
color: gray; color: gray;
border: solid 1px var(--color-sidebar-background-border); border: solid 1px var(--color-sidebar-background-border);
} }
.version-btn.wait { version-switch.wait button {
cursor: wait; cursor: wait;
} }
.version-btn.disabled { version-switch.disabled button {
cursor: not-allowed; cursor: not-allowed;
color: dimgray; color: dimgray;
} }
.version-dialog { version-switch .version-dialog {
display: none; visibility: hidden;
position: absolute; position: absolute;
bottom: 24px; bottom: 24px;
width: 50%; width: 50%;
margin: 0 5px; margin: 0 5px;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 0 6px #000C; box-shadow: 0 0 6px #000C;
opacity: 0;
transition: visibility 0s linear 300ms, opacity 300ms;
z-index: 999; z-index: 999;
max-height: calc(100vh - 30px); max-height: calc(100vh - 30px);
overflow-x: clip; overflow-x: clip;
@ -70,7 +74,19 @@
cursor: default; cursor: default;
} }
.version-title { version-switch.open .version-dialog {
visibility: visible;
opacity: 1;
transition: visibility 0s linear 0s, opacity 200ms;
}
@media(prefers-reduced-motion: reduce) {
version-switch .version-dialog {
transition: 0s !important;
}
}
version-switch .version-title {
padding: 5px; padding: 5px;
color: var(--color-content-foreground); color: var(--color-content-foreground);
text-align: center; text-align: center;
@ -80,7 +96,7 @@
border-bottom: solid 1.5px var(--color-sidebar-background-border); border-bottom: solid 1.5px var(--color-sidebar-background-border);
} }
.version-list { version-switch ul {
padding-left: 0; padding-left: 0;
margin-top: 0; margin-top: 0;
margin-bottom: 4px; margin-bottom: 4px;
@ -89,9 +105,9 @@
border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px;
} }
.version-list a, version-switch li a,
.version-list span, version-switch li span,
.version-list li { version-switch li {
position: relative; position: relative;
display: block; display: block;
font-size: 98%; font-size: 98%;
@ -102,18 +118,18 @@
color: var(--color-sidebar-link-text); color: var(--color-sidebar-link-text);
} }
.version-list li { version-switch li {
background-color: var(--color-sidebar-background); background-color: var(--color-sidebar-background);
color: var(--color-sidebar-link-text); color: var(--color-sidebar-link-text);
padding: 1px; padding: 1px;
} }
.version-list li:hover, version-switch li:hover,
.version-list li a:focus { version-switch li a:focus {
background-color: var(--color-background-hover); background-color: var(--color-background-hover);
} }
.version-list li.selected { version-switch li.current {
background: var(--color-sidebar-item-background--current); background: var(--color-sidebar-item-background--current);
font-weight: 700; font-weight: 700;
} }

View File

@ -1,4 +1,4 @@
(function() {//switch: v1.5 (function() {//switch: v2.0
"use strict"; "use strict";
var versionsFileUrl = "https://docs.blender.org/versions.json" var versionsFileUrl = "https://docs.blender.org/versions.json"
@ -28,43 +28,43 @@ var all_langs = {
"zh-hant": "&#x4E2D;&#x6587;(&#x7E41;&#x9AD4;)", "zh-hant": "&#x4E2D;&#x6587;(&#x7E41;&#x9AD4;)",
}; };
class Popover { class VersionSwitch extends HTMLElement {
constructor(id) { constructor() {
this.isOpen=false; super();
this.type = (id === "version-popover"); }
this.btn = document.querySelector('#' + id);
this.dialog = this.btn.nextElementSibling; connectedCallback() {
this.list = this.dialog.querySelector("ul");
this.sel = null;
const that = this; const that = this;
this.btnClickHandler = function(e) {that.init();e.preventDefault();e.stopPropagation();}; this.addEventListener("focusout", (e) => {that.focusoutHandler(); e.stopImmediatePropagation();});
this.btnKeyHandler = function(e) {if(that.btnKeyFilter(e)){that.init();e.preventDefault();e.stopPropagation();} }; this.addEventListener("mouseleave", (e) => {that.mouseleaveHandler(e); e.stopImmediatePropagation();});
this.btn.addEventListener("click", this.btnClickHandler); this.firstElementChild.addEventListener("mousedown", (e) => {that.buttonClickHandler(); e.preventDefault()});
this.btn.addEventListener("keydown", this.btnKeyHandler); this.firstElementChild.addEventListener("keydown", (e) => { if(that.buttonKeyFilter(e)){that.buttonClickHandler(); e.preventDefault()}});
this.lastElementChild.addEventListener("keydown", (e) => {that.keyHandler(e);});
} }
init() { init() {
this.btn.removeEventListener("click", this.btnClickHandler); return new Promise((resolve) => {
this.btn.removeEventListener("keydown", this.btnKeyHandler);
new Promise((resolve, reject) => {
if(all_versions === undefined) { if(all_versions === undefined) {
this.btn.classList.add("wait"); this.className = "wait";
fetch(versionsFileUrl) fetch(versionsFileUrl)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
all_versions = data; all_versions = data;
resolve(); resolve();
}) })
.catch(() => { .catch((err) => {
console.error("Version Switch Error: versions.json could not be loaded."); this.className = "disabled";
this.btn.classList.remove("disabled"); console.error(err);
}); });
} else { } else {
resolve(); resolve();
} }
}) })
.then(() => { .then(() => {
if (this.lastElementChild.hasAttribute("data-inited")) {
return;
}
this.lastElementChild.setAttribute("data-inited", true);
let release = DOCUMENTATION_OPTIONS.VERSION; let release = DOCUMENTATION_OPTIONS.VERSION;
const m = release.match(/\d\.\d+/g); const m = release.match(/\d\.\d+/g);
if (m) {release = m[0];} if (m) {release = m[0];}
@ -75,15 +75,7 @@ init() {
const version = this.getNamed(release); const version = this.getNamed(release);
this.buildList(version, lang); this.buildList(version, lang);
this.className = "closed";
this.list.firstElementChild.remove();
const that = this;
this.list.addEventListener("keydown", function(e) {that.keyMove(e);});
this.btn.classList.remove("wait");
this.btnOpenHandler();
this.btn.addEventListener("mousedown", function(e){that.btnOpenHandler(); e.preventDefault()});
this.btn.addEventListener("keydown", function(e){ if(that.btnKeyFilter(e)){that.btnOpenHandler();} });
}); });
} }
warnOld(release, all_versions) { warnOld(release, all_versions) {
@ -111,182 +103,188 @@ warnOld(release, all_versions) {
body.prepend(warning); body.prepend(warning);
} }
} }
buildList(v, l) { buildList(version, lang) {
const type = this.getAttribute("type") === "version";
const list = this.querySelector("ul");
list.firstElementChild.remove();
const url = new URL(window.location.href); const url = new URL(window.location.href);
let pathSplit = ["", "manual", l, v]; let pathSplit = ["", "manual", lang, version];
if (url.pathname.startsWith("/manual/")) { if (url.pathname.startsWith("/manual/")) {
pathSplit.push(url.pathname.split('/').slice(4).join('/')); pathSplit.push(url.pathname.split('/').slice(4).join('/'));
} else { } else {
pathSplit.push(url.pathname.substring(1)); pathSplit.push(url.pathname.substring(1));
} }
let dyn, cur; let dyn, cur;
if(this.type) { if(type) {
dyn = all_versions; dyn = all_versions;
cur = v; cur = version;
} else { } else {
dyn = all_langs; dyn = all_langs;
cur = l; cur = lang;
} }
const that = this;
const template = document.querySelector("template#version-entry").content; const template = document.querySelector("template#version-entry").content;
for (let [ix, title] of Object.entries(dyn)) { for (let [key, value] of Object.entries(dyn)) {
let clone; let clone;
if (ix === cur) { if (key === cur) {
clone = template.querySelector("li.selected").cloneNode(true); clone = template.querySelector("li.current").cloneNode(true);
clone.querySelector("span").innerHTML = title; clone.querySelector("span").innerHTML = value;
} else { } else {
pathSplit[2 + that.type] = ix; pathSplit[2 + type] = key;
let href = new URL(url); let href = new URL(url);
href.pathname = pathSplit.join('/'); href.pathname = pathSplit.join('/');
clone = template.firstElementChild.cloneNode(true); clone = template.firstElementChild.cloneNode(true);
const link = clone.querySelector("a"); const link = clone.querySelector("a");
link.href = href; link.href = href;
if (that.type) { if (type) {
link.innerHTML = title; link.innerHTML = value;
} else { } else {
const hint = document.createElement("bdi"); const hint = document.createElement("bdi");
hint.innerHTML = title; hint.innerHTML = value;
link.appendChild(hint); link.appendChild(hint);
} }
link.setAttribute("lang", !that.type ? ix : "en"); link.setAttribute("lang", !type ? key : "en");
} }
that.list.append(clone); list.append(clone);
}; };
return this.list;
} }
getNamed(v) { getNamed(version) {
for (let [ix, title] of Object.entries(all_versions)) { for (let [key, value] of Object.entries(all_versions)) {
if (ix === "dev" || ix === "latest") { if (key === "dev" || key === "latest") {
const m = title.match(/\d\.\d[\w\d\.]*/)[0]; const m = value.match(/\d\.\d[\w\d\.]*/)[0];
if (parseFloat(m) == v) { if (parseFloat(m) == version) {
v = ix; version = key;
return false; break;
} }
} }
}; }
return v; return version;
} }
dialogToggle(speed) { open() {
speed = window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : speed; if(this.className === "closed") {
const wasClose = !this.isOpen; this.init()
const that = this;
if(!this.isOpen) {
this.btn.classList.add("version-btn-open");
this.btn.setAttribute("aria-pressed", true);
this.dialog.setAttribute("aria-hidden", false);
this.dialog.style.display = "block";
this.dialog.animate({opacity: [0, 1], easing: ['ease-in', 'ease-out']}, speed).finished
.then(() => { .then(() => {
this.focusoutHandlerPrime = function(e) {that.focusoutHandler(); e.stopImmediatePropagation();}; this.className = "open";
this.mouseoutHandlerPrime = function(e){that.mouseoutHandler(); e.stopImmediatePropagation();}; this.firstElementChild.setAttribute("aria-pressed", true);
this.btn.parentNode.addEventListener("focusout", this.focusoutHandlerPrime); this.lastElementChild.setAttribute("aria-hidden", false);
this.btn.parentNode.addEventListener("mouseleave", this.mouseoutHandlerPrime); const s = this.querySelector(".selected");
if (s) {
s.setAttribute("tabindex", -1);
s.classList.remove(".selected");
}
if(document.activeElement !== null && document.activeElement !== document && document.activeElement !== document.body) {
const n = this.enter(this.querySelector("ul"));
n.classList.add("selected");
n.setAttribute("tabindex", 0);
n.focus();
}
}); });
this.isOpen = true; }
}
close() {
if(this.className === "open") {
this.className = "closed";
const button = this.firstElementChild;
button.setAttribute("aria-pressed", false);
this.lastElementChild.setAttribute("aria-hidden", true);
const s = this.querySelector(".selected");
if (s) {
s.classList.remove(".selected");
s.setAttribute("tabindex", -1);
}
button.setAttribute("tabindex", 0);
if(document.activeElement !== null && document.activeElement !== document && document.activeElement !== document.body) {
button.focus();
}
}
}
toggle() {
if(this.className === "closed") {
this.open();
} else { } else {
this.btn.classList.remove("version-btn-open"); this.close();
this.btn.setAttribute("aria-pressed", false);
this.dialog.setAttribute("aria-hidden", true);
this.btn.parentNode.removeEventListener("focusout", this.focusoutHandlerPrime);
this.btn.parentNode.removeEventListener("mouseleave", this.mouseoutHandlerPrime);
this.dialog.animate({opacity: [1, 0], easing: ['ease-in', 'ease-out']}, speed).finished
.then(() => {
this.dialog.style.display = "none";
if (this.sel) {this.sel.setAttribute("tabindex", -1);}
this.btn.setAttribute("tabindex", 0);
if(document.activeElement !== null && document.activeElement !== document && document.activeElement !== document.body) {
this.btn.focus();
}
});
this.isOpen = false;
}
if(wasClose) {
if (this.sel) {this.sel.setAttribute("tabindex", -1);}
if(document.activeElement !== null && document.activeElement !== document && document.activeElement !== document.body) {
const nw = this.listEnter();
nw.setAttribute("tabindex", 0);
nw.focus();
this.sel = nw;
}
} }
} }
btnOpenHandler() { buttonClickHandler() {
this.dialogToggle(300); this.toggle();
} }
focusoutHandler() { focusoutHandler() {
const list = this.list;
const that = this; const that = this;
setTimeout(function() { setTimeout(() => {
if (!list.querySelector(":focus")) { if (!that.querySelector("ul :focus")) {
that.dialogToggle(200); that.close();
} }
}, 200); }, 200);
} }
mouseoutHandler() { mouseleaveHandler(e) {
this.dialogToggle(200); if (this.contains(e.relatedTarget)) { return; }
this.close();
} }
btnKeyFilter(e) { buttonKeyFilter(e) {
if (e.ctrlKey || e.shiftKey) {return false;} if (e.ctrlKey || e.shiftKey) {return false;}
if(e.key === " " || e.key === "Enter" || (e.key === "ArrowDown" && e.altKey) || e.key === "ArrowDown" || e.key === "ArrowUp") { if(e.key === " " || e.key === "Enter" || (e.key === "ArrowDown" && e.altKey) || e.key === "ArrowDown" || e.key === "ArrowUp") {
return true; return true;
} }
return false; return false;
} }
keyMove(e) { keyHandler(e) {
if (e.ctrlKey || e.shiftKey) {return true;} if (e.ctrlKey || e.shiftKey) {return true;}
let nw = e.target; let n = e.target;
const list = this.querySelector("ul");
switch(e.key) { switch(e.key) {
case "ArrowUp": nw = this.listPrev(nw); break; case "ArrowUp": n = this.prev(list, n); break;
case "ArrowDown": nw = this.listNext(nw); break; case "ArrowDown": n = this.next(list, n); break;
case "Home": nw = this.listFirst(); break; case "Home": n = this.first(list); break;
case "End": nw = this.listLast(); break; case "End": n = this.last(list); break;
case "Escape": nw = this.listExit(); break; case "Escape": n = this.exit(); break;
case "ArrowLeft": nw = this.listExit(); break; case "ArrowLeft": n = this.exit(); break;
case "ArrowRight": nw = this.listExit(); break; case "ArrowRight": n = this.exit(); break;
default: return false; default: return false;
} }
nw.setAttribute("tabindex", 0); const s = this.querySelector(".selected");
nw.focus(); if (s) {
if (this.sel) {this.sel.setAttribute("tabindex", -1);} s.setAttribute("tabindex", -1);
this.sel = nw; s.classList.remove(".selected");
}
n.classList.add("selected");
n.setAttribute("tabindex", 0);
n.focus();
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
listPrev(nw) { prev(list, n) {
if (nw.parentNode.previousElementSibling.length !== 0) { if (n.parentNode.previousElementSibling) {
return nw.parentNode.previousElementSibling.firstElementChild; return n.parentNode.previousElementSibling.firstElementChild;
} else { } else {
return this.listLast(); return this.last(list);
} }
} }
listNext(nw) { next(list, n) {
if (nw.parentNode.nextElementSibling.length !== 0) { if (n.parentNode.nextElementSibling) {
return nw.parentNode.nextElementSibling.firstElementChild; return n.parentNode.nextElementSibling.firstElementChild;
} else { } else {
return this.listFirst(); return this.first(list);
} }
} }
listFirst() { first(list) {
return this.list.firstElementChild.firstElementChild; return list.firstElementChild.firstElementChild;
} }
listLast() { last(list) {
return this.list.lastElementChild.firstElementChild; return list.lastElementChild.firstElementChild;
} }
listExit() { exit() {
this.mouseoutHandler(); this.close();
return this.btn; return this.firstElementChild;
} }
listEnter() { enter(list) {
return this.list.firstElementChild.firstElementChild; return list.firstElementChild.firstElementChild;
} }
} }
customElements.define("version-switch", VersionSwitch);
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
let lang = DOCUMENTATION_OPTIONS.LANGUAGE; let lang = DOCUMENTATION_OPTIONS.LANGUAGE;
if(!lang || lang === "None") {lang = "en";} if(!lang || lang === "None") {lang = "en";}
if(all_langs.hasOwnProperty(lang)) {document.querySelector("#lang-popover").innerHTML = all_langs[lang];} if(all_langs.hasOwnProperty(lang)) {document.querySelector("version-switch[type=lang] button").innerHTML = all_langs[lang];}
new Popover("version-popover");
new Popover("lang-popover");
}); });
})(); })();