statusbar popup keyboard navigation

This commit is contained in:
e218736 2024-02-21 10:13:40 +00:00
parent 85a3510454
commit 6fcf103760
3 changed files with 135 additions and 126 deletions

View file

@ -478,6 +478,11 @@
background-color: #ddd background-color: #ddd
} }
/* Change color of dropup links on focus */
.cm-status-bar-select-content a:focus {
background-color: #ddd;
}
/* Change the background color of the dropup button when the dropup content is shown */ /* Change the background color of the dropup button when the dropup content is shown */
.cm-status-bar-select:hover .cm-status-bar-select-btn { .cm-status-bar-select:hover .cm-status-bar-select-btn {
background-color: #f1f1f1; background-color: #f1f1f1;

View file

@ -5,10 +5,7 @@
*/ */
import {showPanel} from "@codemirror/view"; import {showPanel} from "@codemirror/view";
import { import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs";
CHR_ENC_SIMPLE_LOOKUP,
CHR_ENC_SIMPLE_REVERSE_LOOKUP,
} from "../../core/lib/ChrEnc.mjs";
/** /**
* A Status bar extension for CodeMirror * A Status bar extension for CodeMirror
@ -44,10 +41,7 @@ class StatusBarPanel {
dom.className = "cm-status-bar"; dom.className = "cm-status-bar";
dom.setAttribute("data-help-title", `${this.label} status bar`); dom.setAttribute("data-help-title", `${this.label} status bar`);
dom.setAttribute( dom.setAttribute("data-help", `This status bar provides information about data in the ${this.label}. Help topics are available for each of the components by activating help when hovering over them.`);
"data-help",
`This status bar provides information about data in the ${this.label}. Help topics are available for each of the components by activating help when hovering over them.`
);
lhs.innerHTML = this.constructLHS(); lhs.innerHTML = this.constructLHS();
rhs.innerHTML = this.constructRHS(); rhs.innerHTML = this.constructRHS();
@ -58,25 +52,27 @@ class StatusBarPanel {
const eventHandler = this.showDropUp.bind(this); const eventHandler = this.showDropUp.bind(this);
dom.querySelectorAll(".cm-status-bar-select-btn").forEach((el) => { dom.querySelectorAll(".cm-status-bar-select-btn").forEach((el) => {
el.addEventListener("click", eventHandler, false), el.addEventListener("click", eventHandler, false);
});
dom.querySelectorAll(".cm-status-bar-select-btn").forEach((el) => {
el.addEventListener("keydown", eventHandler, false); el.addEventListener("keydown", eventHandler, false);
}); });
dom.querySelector(".eol-select").addEventListener(
"click", const selectContent = dom.querySelectorAll(
this.eolSelectClick.bind(this), ".cm-status-bar-select-content"
false
);
dom.querySelector(".chr-enc-select").addEventListener(
"click",
this.chrEncSelectClick.bind(this),
false
);
dom.querySelector(".cm-status-bar-filter-input").addEventListener(
"keyup",
this.chrEncFilter.bind(this),
false
); );
selectContent.forEach((el) => {
const aTags = el.getElementsByTagName("a");
for (let i = 0; i < aTags.length; i++) {
aTags[i].addEventListener("keydown", arrowNav, false);
}
});
dom.querySelector(".eol-select").addEventListener("click", this.eolSelectClick.bind(this), false);
dom.querySelector(".chr-enc-select").addEventListener("click", this.chrEncSelectClick.bind(this), false);
dom.querySelector(".cm-status-bar-filter-input").addEventListener("keyup", this.chrEncFilter.bind(this), false);
return dom; return dom;
} }
@ -117,14 +113,14 @@ class StatusBarPanel {
e.preventDefault(); e.preventDefault();
const eolLookup = { const eolLookup = {
LF: "\u000a", "LF": "\u000a",
VT: "\u000b", "VT": "\u000b",
FF: "\u000c", "FF": "\u000c",
CR: "\u000d", "CR": "\u000d",
CRLF: "\u000d\u000a", "CRLF": "\u000d\u000a",
NEL: "\u0085", "NEL": "\u0085",
LS: "\u2028", "LS": "\u2028",
PS: "\u2029", "PS": "\u2029"
}; };
const eolval = eolLookup[e.target.getAttribute("data-val")]; const eolval = eolLookup[e.target.getAttribute("data-val")];
@ -198,9 +194,9 @@ class StatusBarPanel {
* @param {boolean} selectionSet * @param {boolean} selectionSet
*/ */
updateSelection(state, selectionSet) { updateSelection(state, selectionSet) {
const selLen = state?.selection?.main const selLen = state?.selection?.main ?
? state.selection.main.to - state.selection.main.from state.selection.main.to - state.selection.main.from :
: 0; 0;
const selInfo = this.dom.querySelector(".sel-info"), const selInfo = this.dom.querySelector(".sel-info"),
curOffsetInfo = this.dom.querySelector(".cur-offset-info"); curOffsetInfo = this.dom.querySelector(".cur-offset-info");
@ -218,12 +214,11 @@ class StatusBarPanel {
if (state.lineBreak.length !== 1) { if (state.lineBreak.length !== 1) {
const fromLine = state.doc.lineAt(from).number; const fromLine = state.doc.lineAt(from).number;
const toLine = state.doc.lineAt(to).number; const toLine = state.doc.lineAt(to).number;
from += state.lineBreak.length * fromLine - fromLine - 1; from += (state.lineBreak.length * fromLine) - fromLine - 1;
to += state.lineBreak.length * toLine - toLine - 1; to += (state.lineBreak.length * toLine) - toLine - 1;
} }
if (selLen > 0) { if (selLen > 0) { // Range
// Range
const start = this.dom.querySelector(".sel-start-value"), const start = this.dom.querySelector(".sel-start-value"),
end = this.dom.querySelector(".sel-end-value"), end = this.dom.querySelector(".sel-end-value"),
length = this.dom.querySelector(".sel-length-value"); length = this.dom.querySelector(".sel-length-value");
@ -233,8 +228,7 @@ class StatusBarPanel {
start.textContent = from; start.textContent = from;
end.textContent = to; end.textContent = to;
length.textContent = to - from; length.textContent = to - from;
} else { } else { // Position
// Position
const offset = this.dom.querySelector(".cur-offset-value"); const offset = this.dom.querySelector(".cur-offset-value");
selInfo.style.display = "none"; selInfo.style.display = "none";
@ -258,7 +252,7 @@ class StatusBarPanel {
"\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"], "\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"],
"\u0085": ["NEL", "Next Line"], "\u0085": ["NEL", "Next Line"],
"\u2028": ["LS", "Line Separator"], "\u2028": ["LS", "Line Separator"],
"\u2029": ["PS", "Paragraph Separator"], "\u2029": ["PS", "Paragraph Separator"]
}; };
const val = this.dom.querySelector(".eol-value"); const val = this.dom.querySelector(".eol-value");
@ -266,10 +260,7 @@ class StatusBarPanel {
const eolName = eolLookup[state.lineBreak]; const eolName = eolLookup[state.lineBreak];
val.textContent = eolName[0]; val.textContent = eolName[0];
button.setAttribute("title", `End of line sequence:<br>${eolName[1]}`); button.setAttribute("title", `End of line sequence:<br>${eolName[1]}`);
button.setAttribute( button.setAttribute("data-original-title", `End of line sequence:<br>${eolName[1]}`);
"data-original-title",
`End of line sequence:<br>${eolName[1]}`
);
this.eolVal = state.lineBreak; this.eolVal = state.lineBreak;
} }
@ -280,21 +271,13 @@ class StatusBarPanel {
const chrEncVal = this.chrEncGetter(); const chrEncVal = this.chrEncGetter();
if (chrEncVal === this.chrEncVal) return; if (chrEncVal === this.chrEncVal) return;
const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] : "Raw Bytes";
? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal]
: "Raw Bytes";
const val = this.dom.querySelector(".chr-enc-value"); const val = this.dom.querySelector(".chr-enc-value");
const button = val.closest(".cm-status-bar-select-btn"); const button = val.closest(".cm-status-bar-select-btn");
val.textContent = name; val.textContent = name;
button.setAttribute( button.setAttribute("title", `${this.label} character encoding:<br>${name}`);
"title", button.setAttribute("data-original-title", `${this.label} character encoding:<br>${name}`);
`${this.label} character encoding:<br>${name}`
);
button.setAttribute(
"data-original-title",
`${this.label} character encoding:<br>${name}`
);
this.chrEncVal = chrEncVal; this.chrEncVal = chrEncVal;
} }
@ -311,9 +294,7 @@ class StatusBarPanel {
bakingTimeInfo.style.display = "inline-block"; bakingTimeInfo.style.display = "inline-block";
bakingTime.textContent = this.timing.duration(this.tabNumGetter()); bakingTime.textContent = this.timing.duration(this.tabNumGetter());
const info = this.timing const info = this.timing.printStages(this.tabNumGetter()).replace(/\n/g, "<br>");
.printStages(this.tabNumGetter())
.replace(/\n/g, "<br>");
bakingTimeInfo.setAttribute("data-original-title", info); bakingTimeInfo.setAttribute("data-original-title", info);
} else { } else {
bakingTimeInfo.style.display = "none"; bakingTimeInfo.style.display = "none";
@ -326,11 +307,11 @@ class StatusBarPanel {
*/ */
updateSizing(view) { updateSizing(view) {
const viewHeight = view.contentDOM.parentNode.clientHeight; const viewHeight = view.contentDOM.parentNode.clientHeight;
this.dom this.dom.querySelectorAll(".cm-status-bar-select-scroll").forEach(
.querySelectorAll(".cm-status-bar-select-scroll") el => {
.forEach((el) => { el.style.maxHeight = (viewHeight - 50) + "px";
el.style.maxHeight = viewHeight - 50 + "px"; }
}); );
} }
/** /**
@ -341,27 +322,19 @@ class StatusBarPanel {
if (this.htmlOutput?.html === "") { if (this.htmlOutput?.html === "") {
// Enable all controls // Enable all controls
this.dom.querySelectorAll(".disabled").forEach((el) => { this.dom.querySelectorAll(".disabled").forEach(el => {
el.classList.remove("disabled"); el.classList.remove("disabled");
}); });
} else { } else {
// Disable chrenc, length, selection etc. // Disable chrenc, length, selection etc.
this.dom this.dom.querySelectorAll(".cm-status-bar-select-btn").forEach(el => {
.querySelectorAll(".cm-status-bar-select-btn")
.forEach((el) => {
el.classList.add("disabled"); el.classList.add("disabled");
}); });
this.dom this.dom.querySelector(".stats-length-value").parentNode.classList.add("disabled");
.querySelector(".stats-length-value") this.dom.querySelector(".stats-lines-value").parentNode.classList.add("disabled");
.parentNode.classList.add("disabled");
this.dom
.querySelector(".stats-lines-value")
.parentNode.classList.add("disabled");
this.dom.querySelector(".sel-info").classList.add("disabled"); this.dom.querySelector(".sel-info").classList.add("disabled");
this.dom this.dom.querySelector(".cur-offset-info").classList.add("disabled");
.querySelector(".cur-offset-info")
.classList.add("disabled");
} }
} }
@ -398,25 +371,18 @@ class StatusBarPanel {
* @returns {string} * @returns {string}
*/ */
constructRHS() { constructRHS() {
const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP) const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP).map(name =>
.map(
(name) =>
`<a href="#" draggable="false" data-val="${CHR_ENC_SIMPLE_LOOKUP[name]}">${name}</a>` `<a href="#" draggable="false" data-val="${CHR_ENC_SIMPLE_LOOKUP[name]}">${name}</a>`
) ).join("");
.join("");
let chrEncHelpText = "", let chrEncHelpText = "",
eolHelpText = ""; eolHelpText = "";
if (this.label === "Input") { if (this.label === "Input") {
chrEncHelpText = chrEncHelpText = "The input character encoding defines how the input text is encoded into bytes which are then processed by the Recipe.<br><br>The 'Raw bytes' option attempts to treat the input as individual bytes in the range 0-255. If it detects any characters with Unicode values above 255, it will treat the entire input as UTF-8. 'Raw bytes' is usually the best option if you are inputting binary data, such as a file.";
"The input character encoding defines how the input text is encoded into bytes which are then processed by the Recipe.<br><br>The 'Raw bytes' option attempts to treat the input as individual bytes in the range 0-255. If it detects any characters with Unicode values above 255, it will treat the entire input as UTF-8. 'Raw bytes' is usually the best option if you are inputting binary data, such as a file."; eolHelpText = "The End of Line Sequence defines which bytes are considered EOL terminators. Pressing the return key will enter this value into the input and create a new line.<br><br>Changing the EOL sequence will not modify any existing data in the input but may change how previously entered line breaks are displayed. Lines added while a different EOL terminator was set may not now result in a new line, but may be displayed as control characters instead.";
eolHelpText =
"The End of Line Sequence defines which bytes are considered EOL terminators. Pressing the return key will enter this value into the input and create a new line.<br><br>Changing the EOL sequence will not modify any existing data in the input but may change how previously entered line breaks are displayed. Lines added while a different EOL terminator was set may not now result in a new line, but may be displayed as control characters instead.";
} else { } else {
chrEncHelpText = chrEncHelpText = "The output character encoding defines how the output bytes are decoded into text which can be displayed to you.<br><br>The 'Raw bytes' option treats the output data as individual bytes in the range 0-255.";
"The output character encoding defines how the output bytes are decoded into text which can be displayed to you.<br><br>The 'Raw bytes' option treats the output data as individual bytes in the range 0-255."; eolHelpText = "The End of Line Sequence defines which bytes are considered EOL terminators.<br><br>Changing this value will not modify the value of the output, but may change how certain bytes are displayed and whether they result in a new line being created.";
eolHelpText =
"The End of Line Sequence defines which bytes are considered EOL terminators.<br><br>Changing this value will not modify the value of the output, but may change how certain bytes are displayed and whether they result in a new line being created.";
} }
return ` return `
@ -431,7 +397,7 @@ class StatusBarPanel {
</span> </span>
<div class="cm-status-bar-select-content"> <div class="cm-status-bar-select-content">
<div class="cm-status-bar-select-scroll no-select"> <div class="cm-status-bar-select-scroll no-select">
<a href="#" draggable="false" data-val="0">Raw Bytes</a> <a href="#" draggable="false" data-val="0" tabindex="0">Raw Bytes</a>
${chrEncOptions} ${chrEncOptions}
</div> </div>
<div class="input-group cm-status-bar-filter-search"> <div class="input-group cm-status-bar-filter-search">
@ -449,15 +415,15 @@ class StatusBarPanel {
<span class="cm-status-bar-select-btn" data-toggle="tooltip" tabindex="0" data-html="true" data-placement="left" title="End of line sequence"> <span class="cm-status-bar-select-btn" data-toggle="tooltip" tabindex="0" data-html="true" data-placement="left" title="End of line sequence">
<i class="material-icons">keyboard_return</i> <span class="eol-value"></span> <i class="material-icons">keyboard_return</i> <span class="eol-value"></span>
</span> </span>
<div class="cm-status-bar-select-content no-select"> <div class="cm-status-bar-select-content no-select" tabindex="0">
<a href="#" draggable="false" data-val="LF">Line Feed, U+000A</a> <a href="#" draggable="false" data-val="LF" tabindex="0">Line Feed, U+000A</a>
<a href="#" draggable="false" data-val="VT">Vertical Tab, U+000B</a> <a href="#" draggable="false" data-val="VT" tabindex="0">Vertical Tab, U+000B</a>
<a href="#" draggable="false" data-val="FF">Form Feed, U+000C</a> <a href="#" draggable="false" data-val="FF" tabindex="0">Form Feed, U+000C</a>
<a href="#" draggable="false" data-val="CR">Carriage Return, U+000D</a> <a href="#" draggable="false" data-val="CR" tabindex="0">Carriage Return, U+000D</a>
<a href="#" draggable="false" data-val="CRLF">CR+LF, U+000D U+000A</a> <a href="#" draggable="false" data-val="CRLF" tabindex="0">CR+LF, U+000D U+000A</a>
<!-- <a href="#" draggable="false" data-val="NL">Next Line, U+0085</a> This causes problems. --> <!-- <a href="#" draggable="false" data-val="NL" tabindex="0">Next Line, U+0085</a> This causes problems. -->
<a href="#" draggable="false" data-val="LS">Line Separator, U+2028</a> <a href="#" draggable="false" data-val="LS" tabindex="0">Line Separator, U+2028</a>
<a href="#" draggable="false" data-val="PS">Paragraph Separator, U+2029</a> <a href="#" draggable="false" data-val="PS" tabindex="0">Paragraph Separator, U+2029</a>
</div> </div>
</div>`; </div>`;
} }
@ -476,7 +442,7 @@ function hideOnClickOutside(element, instantiatingEvent) {
* Closes element if click is outside it. * Closes element if click is outside it.
* @param {Event} event * @param {Event} event
*/ */
const outsideClickListener = (event) => { const outsideClickListener = event => {
// Don't trigger if we're clicking inside the element, or if the element // Don't trigger if we're clicking inside the element, or if the element
// is not visible, or if this is the same click event that opened it. // is not visible, or if this is the same click event that opened it.
if ( if (
@ -509,14 +475,30 @@ function hideOnMoveFocus(element, instantiatingEvent) {
* Closes element if key press is outside it. * Closes element if key press is outside it.
* @param {Event} event * @param {Event} event
*/ */
const outsideClickListener = (event) => { const outsidePressListener = (event) => {
// Don't trigger if we're pressing keys while inside the element, or if the element // Don't trigger if we're pressing keys while inside the element, or if the element
// is not visible, or if this is the same click event that opened it. // is not visible, or if this is the same click event that opened it.
if ( if (
!element.contains(event.target) && !element.contains(event.target) &&
event.timeStamp !== instantiatingEvent.timeStamp &&
event.key !== "ArrowUp"
) {
hideElement(element);
} else if (
event.key === "Escape" &&
event.timeStamp !== instantiatingEvent.timeStamp event.timeStamp !== instantiatingEvent.timeStamp
) { ) {
hideElement(element); hideElement(element);
} else if (
event.key === "ArrowUp" ||
(event.key === "ArrowDown" &&
event.timeStamp !== instantiatingEvent.timeStamp)
) {
const menuItems = element.getElementsByTagName("a");
menuItems[0].focus();
console.log("ev target:", event.target);
console.log("element", element);
} }
}; };
@ -526,7 +508,7 @@ function hideOnMoveFocus(element, instantiatingEvent) {
element element
) )
) { ) {
elementsWithKeyDownListeners[element] = outsideClickListener; elementsWithKeyDownListeners[element] = outsidePressListener;
document.addEventListener( document.addEventListener(
"keydown", "keydown",
elementsWithKeyDownListeners[element], elementsWithKeyDownListeners[element],
@ -535,6 +517,38 @@ function hideOnMoveFocus(element, instantiatingEvent) {
} }
} }
/**
* Handler for menu item keydown events
* Moves focus to next/previous element based on arrow direction.
* @param {Event} event
*/
const arrowNav = (event) => {
const currentElement = event.target;
if (event.key === "ArrowDown") {
event.preventDefault();
event.stopPropagation();
const nextElement = currentElement.nextElementSibling;
if (nextElement === null) {
currentElement.parentElement.firstElementChild.focus();
} else {
nextElement.focus();
}
} else if (event.key === "ArrowUp") {
event.preventDefault();
event.stopPropagation();
const prevElement = currentElement.previousElementSibling;
if (prevElement === null) {
currentElement.parentElement.lastElementChild.focus();
} else {
prevElement.focus();
}
} else if (event.key === "Tab") {
event.preventDefault();
event.stopPropagation();
currentElement.parentElement.closest(".cm-status-bar-select-content").previousElementSibling.focus();
}
};
/** /**
* Hides the specified element and removes the click or keydown listener for it * Hides the specified element and removes the click or keydown listener for it
* @param {Element} element * @param {Element} element

View file

@ -152,9 +152,7 @@ class ControlsWaiter {
const params = [ const params = [
includeRecipe ? ["recipe", recipeStr] : undefined, includeRecipe ? ["recipe", recipeStr] : undefined,
includeInput && input.length includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined,
? ["input", Utils.escapeHtml(input)]
: undefined,
inputChrEnc !== 0 ? ["ienc", inputChrEnc] : undefined, inputChrEnc !== 0 ? ["ienc", inputChrEnc] : undefined,
outputChrEnc !== 0 ? ["oenc", outputChrEnc] : undefined, outputChrEnc !== 0 ? ["oenc", outputChrEnc] : undefined,
inputEOLSeq !== "\n" ? ["ieol", inputEOLSeq] : undefined, inputEOLSeq !== "\n" ? ["ieol", inputEOLSeq] : undefined,
@ -255,9 +253,7 @@ class ControlsWaiter {
return; return;
} }
const savedRecipes = localStorage.savedRecipes const savedRecipes = localStorage.savedRecipes ? JSON.parse(localStorage.savedRecipes) : [];
? JSON.parse(localStorage.savedRecipes)
: [];
let recipeId = localStorage.recipeId || 0; let recipeId = localStorage.recipeId || 0;
savedRecipes.push({ savedRecipes.push({
@ -287,9 +283,7 @@ class ControlsWaiter {
} }
// Add recipes to select // Add recipes to select
const savedRecipes = localStorage.savedRecipes const savedRecipes = localStorage.savedRecipes ? JSON.parse(localStorage.savedRecipes) : [];
? JSON.parse(localStorage.savedRecipes)
: [];
for (i = 0; i < savedRecipes.length; i++) { for (i = 0; i < savedRecipes.length; i++) {
const opt = document.createElement("option"); const opt = document.createElement("option");
@ -316,9 +310,7 @@ class ControlsWaiter {
if (!this.app.isLocalStorageAvailable()) return false; if (!this.app.isLocalStorageAvailable()) return false;
const id = parseInt(document.getElementById("load-name").value, 10); const id = parseInt(document.getElementById("load-name").value, 10);
const rawSavedRecipes = localStorage.savedRecipes const rawSavedRecipes = localStorage.savedRecipes ? JSON.parse(localStorage.savedRecipes) : [];
? JSON.parse(localStorage.savedRecipes)
: [];
const savedRecipes = rawSavedRecipes.filter((r) => r.id !== id); const savedRecipes = rawSavedRecipes.filter((r) => r.id !== id);
@ -333,9 +325,7 @@ class ControlsWaiter {
if (!this.app.isLocalStorageAvailable()) return false; if (!this.app.isLocalStorageAvailable()) return false;
const el = e.target; const el = e.target;
const savedRecipes = localStorage.savedRecipes const savedRecipes = localStorage.savedRecipes ? JSON.parse(localStorage.savedRecipes) : [];
? JSON.parse(localStorage.savedRecipes)
: [];
const id = parseInt(el.value, 10); const id = parseInt(el.value, 10);
const recipe = savedRecipes.find((r) => r.id === id); const recipe = savedRecipes.find((r) => r.id === id);