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
}
/* 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 */
.cm-status-bar-select:hover .cm-status-bar-select-btn {
background-color: #f1f1f1;

View file

@ -5,10 +5,7 @@
*/
import {showPanel} from "@codemirror/view";
import {
CHR_ENC_SIMPLE_LOOKUP,
CHR_ENC_SIMPLE_REVERSE_LOOKUP,
} from "../../core/lib/ChrEnc.mjs";
import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs";
/**
* A Status bar extension for CodeMirror
@ -44,10 +41,7 @@ class StatusBarPanel {
dom.className = "cm-status-bar";
dom.setAttribute("data-help-title", `${this.label} status bar`);
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.`
);
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.`);
lhs.innerHTML = this.constructLHS();
rhs.innerHTML = this.constructRHS();
@ -58,25 +52,27 @@ class StatusBarPanel {
const eventHandler = this.showDropUp.bind(this);
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);
});
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
const selectContent = dom.querySelectorAll(
".cm-status-bar-select-content"
);
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;
}
@ -117,14 +113,14 @@ class StatusBarPanel {
e.preventDefault();
const eolLookup = {
LF: "\u000a",
VT: "\u000b",
FF: "\u000c",
CR: "\u000d",
CRLF: "\u000d\u000a",
NEL: "\u0085",
LS: "\u2028",
PS: "\u2029",
"LF": "\u000a",
"VT": "\u000b",
"FF": "\u000c",
"CR": "\u000d",
"CRLF": "\u000d\u000a",
"NEL": "\u0085",
"LS": "\u2028",
"PS": "\u2029"
};
const eolval = eolLookup[e.target.getAttribute("data-val")];
@ -198,9 +194,9 @@ class StatusBarPanel {
* @param {boolean} selectionSet
*/
updateSelection(state, selectionSet) {
const selLen = state?.selection?.main
? state.selection.main.to - state.selection.main.from
: 0;
const selLen = state?.selection?.main ?
state.selection.main.to - state.selection.main.from :
0;
const selInfo = this.dom.querySelector(".sel-info"),
curOffsetInfo = this.dom.querySelector(".cur-offset-info");
@ -218,12 +214,11 @@ class StatusBarPanel {
if (state.lineBreak.length !== 1) {
const fromLine = state.doc.lineAt(from).number;
const toLine = state.doc.lineAt(to).number;
from += state.lineBreak.length * fromLine - fromLine - 1;
to += state.lineBreak.length * toLine - toLine - 1;
from += (state.lineBreak.length * fromLine) - fromLine - 1;
to += (state.lineBreak.length * toLine) - toLine - 1;
}
if (selLen > 0) {
// Range
if (selLen > 0) { // Range
const start = this.dom.querySelector(".sel-start-value"),
end = this.dom.querySelector(".sel-end-value"),
length = this.dom.querySelector(".sel-length-value");
@ -233,8 +228,7 @@ class StatusBarPanel {
start.textContent = from;
end.textContent = to;
length.textContent = to - from;
} else {
// Position
} else { // Position
const offset = this.dom.querySelector(".cur-offset-value");
selInfo.style.display = "none";
@ -258,7 +252,7 @@ class StatusBarPanel {
"\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"],
"\u0085": ["NEL", "Next Line"],
"\u2028": ["LS", "Line Separator"],
"\u2029": ["PS", "Paragraph Separator"],
"\u2029": ["PS", "Paragraph Separator"]
};
const val = this.dom.querySelector(".eol-value");
@ -266,10 +260,7 @@ class StatusBarPanel {
const eolName = eolLookup[state.lineBreak];
val.textContent = eolName[0];
button.setAttribute("title", `End of line sequence:<br>${eolName[1]}`);
button.setAttribute(
"data-original-title",
`End of line sequence:<br>${eolName[1]}`
);
button.setAttribute("data-original-title", `End of line sequence:<br>${eolName[1]}`);
this.eolVal = state.lineBreak;
}
@ -280,21 +271,13 @@ class StatusBarPanel {
const chrEncVal = this.chrEncGetter();
if (chrEncVal === this.chrEncVal) return;
const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal]
? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal]
: "Raw Bytes";
const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] : "Raw Bytes";
const val = this.dom.querySelector(".chr-enc-value");
const button = val.closest(".cm-status-bar-select-btn");
val.textContent = name;
button.setAttribute(
"title",
`${this.label} character encoding:<br>${name}`
);
button.setAttribute(
"data-original-title",
`${this.label} character encoding:<br>${name}`
);
button.setAttribute("title", `${this.label} character encoding:<br>${name}`);
button.setAttribute("data-original-title", `${this.label} character encoding:<br>${name}`);
this.chrEncVal = chrEncVal;
}
@ -311,9 +294,7 @@ class StatusBarPanel {
bakingTimeInfo.style.display = "inline-block";
bakingTime.textContent = this.timing.duration(this.tabNumGetter());
const info = this.timing
.printStages(this.tabNumGetter())
.replace(/\n/g, "<br>");
const info = this.timing.printStages(this.tabNumGetter()).replace(/\n/g, "<br>");
bakingTimeInfo.setAttribute("data-original-title", info);
} else {
bakingTimeInfo.style.display = "none";
@ -326,11 +307,11 @@ class StatusBarPanel {
*/
updateSizing(view) {
const viewHeight = view.contentDOM.parentNode.clientHeight;
this.dom
.querySelectorAll(".cm-status-bar-select-scroll")
.forEach((el) => {
el.style.maxHeight = viewHeight - 50 + "px";
});
this.dom.querySelectorAll(".cm-status-bar-select-scroll").forEach(
el => {
el.style.maxHeight = (viewHeight - 50) + "px";
}
);
}
/**
@ -341,27 +322,19 @@ class StatusBarPanel {
if (this.htmlOutput?.html === "") {
// Enable all controls
this.dom.querySelectorAll(".disabled").forEach((el) => {
this.dom.querySelectorAll(".disabled").forEach(el => {
el.classList.remove("disabled");
});
} else {
// Disable chrenc, length, selection etc.
this.dom
.querySelectorAll(".cm-status-bar-select-btn")
.forEach((el) => {
this.dom.querySelectorAll(".cm-status-bar-select-btn").forEach(el => {
el.classList.add("disabled");
});
this.dom
.querySelector(".stats-length-value")
.parentNode.classList.add("disabled");
this.dom
.querySelector(".stats-lines-value")
.parentNode.classList.add("disabled");
this.dom.querySelector(".stats-length-value").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(".cur-offset-info")
.classList.add("disabled");
this.dom.querySelector(".cur-offset-info").classList.add("disabled");
}
}
@ -398,25 +371,18 @@ class StatusBarPanel {
* @returns {string}
*/
constructRHS() {
const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP)
.map(
(name) =>
const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP).map(name =>
`<a href="#" draggable="false" data-val="${CHR_ENC_SIMPLE_LOOKUP[name]}">${name}</a>`
)
.join("");
).join("");
let chrEncHelpText = "",
eolHelpText = "";
if (this.label === "Input") {
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.";
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.";
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.";
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 {
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.";
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.";
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.";
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 `
@ -431,7 +397,7 @@ class StatusBarPanel {
</span>
<div class="cm-status-bar-select-content">
<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}
</div>
<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">
<i class="material-icons">keyboard_return</i> <span class="eol-value"></span>
</span>
<div class="cm-status-bar-select-content no-select">
<a href="#" draggable="false" data-val="LF">Line Feed, U+000A</a>
<a href="#" draggable="false" data-val="VT">Vertical Tab, U+000B</a>
<a href="#" draggable="false" data-val="FF">Form Feed, U+000C</a>
<a href="#" draggable="false" data-val="CR">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="NL">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="PS">Paragraph Separator, U+2029</a>
<div class="cm-status-bar-select-content no-select" tabindex="0">
<a href="#" draggable="false" data-val="LF" tabindex="0">Line Feed, U+000A</a>
<a href="#" draggable="false" data-val="VT" tabindex="0">Vertical Tab, U+000B</a>
<a href="#" draggable="false" data-val="FF" tabindex="0">Form Feed, U+000C</a>
<a href="#" draggable="false" data-val="CR" tabindex="0">Carriage Return, U+000D</a>
<a href="#" draggable="false" data-val="CRLF" tabindex="0">CR+LF, U+000D U+000A</a>
<!-- <a href="#" draggable="false" data-val="NL" tabindex="0">Next Line, U+0085</a> This causes problems. -->
<a href="#" draggable="false" data-val="LS" tabindex="0">Line Separator, U+2028</a>
<a href="#" draggable="false" data-val="PS" tabindex="0">Paragraph Separator, U+2029</a>
</div>
</div>`;
}
@ -476,7 +442,7 @@ function hideOnClickOutside(element, instantiatingEvent) {
* Closes element if click is outside it.
* @param {Event} event
*/
const outsideClickListener = (event) => {
const outsideClickListener = event => {
// 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.
if (
@ -509,14 +475,30 @@ function hideOnMoveFocus(element, instantiatingEvent) {
* Closes element if key press is outside it.
* @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
// is not visible, or if this is the same click event that opened it.
if (
!element.contains(event.target) &&
event.timeStamp !== instantiatingEvent.timeStamp &&
event.key !== "ArrowUp"
) {
hideElement(element);
} else if (
event.key === "Escape" &&
event.timeStamp !== instantiatingEvent.timeStamp
) {
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
)
) {
elementsWithKeyDownListeners[element] = outsideClickListener;
elementsWithKeyDownListeners[element] = outsidePressListener;
document.addEventListener(
"keydown",
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
* @param {Element} element

View file

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