/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2022
* @license Apache-2.0
*/
import {showPanel} from "@codemirror/view";
import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs";
/**
* A Status bar extension for CodeMirror
*/
class StatusBarPanel {
/**
* StatusBarPanel constructor
* @param {Object} opts
*/
constructor(opts) {
this.label = opts.label;
this.timing = opts.timing;
this.tabNumGetter = opts.tabNumGetter;
this.eolHandler = opts.eolHandler;
this.chrEncHandler = opts.chrEncHandler;
this.chrEncGetter = opts.chrEncGetter;
this.htmlOutput = opts.htmlOutput;
this.eolVal = null;
this.chrEncVal = null;
this.dom = this.buildDOM();
}
/**
* Builds the status bar DOM tree
* @returns {DOMNode}
*/
buildDOM() {
const dom = document.createElement("div");
const lhs = document.createElement("div");
const rhs = document.createElement("div");
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.`);
lhs.innerHTML = this.constructLHS();
rhs.innerHTML = this.constructRHS();
dom.appendChild(lhs);
dom.appendChild(rhs);
// Event listeners
dom.querySelectorAll(".cm-status-bar-select-btn").forEach(
el => el.addEventListener("click", this.showDropUp.bind(this), 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;
}
/**
* Handler for dropup clicks
* Shows/Hides the dropup
* @param {Event} e
*/
showDropUp(e) {
const el = e.target
.closest(".cm-status-bar-select")
.querySelector(".cm-status-bar-select-content");
const btn = e.target.closest(".cm-status-bar-select-btn");
if (btn.classList.contains("disabled")) return;
el.classList.add("show");
// Focus the filter input if present
const filter = el.querySelector(".cm-status-bar-filter-input");
if (filter) filter.focus();
// Set up a listener to close the menu if the user clicks outside of it
hideOnClickOutside(el, e);
}
/**
* Handler for EOL Select clicks
* Sets the line separator
* @param {Event} e
*/
eolSelectClick(e) {
// preventDefault is required to stop the URL being modified and popState being triggered
e.preventDefault();
const eolLookup = {
"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")];
if (eolval === undefined) return;
// Call relevant EOL change handler
this.eolHandler(eolval);
hideElement(e.target.closest(".cm-status-bar-select-content"));
}
/**
* Handler for Chr Enc Select clicks
* Sets the character encoding
* @param {Event} e
*/
chrEncSelectClick(e) {
// preventDefault is required to stop the URL being modified and popState being triggered
e.preventDefault();
const chrEncVal = parseInt(e.target.getAttribute("data-val"), 10);
if (isNaN(chrEncVal)) return;
this.chrEncHandler(chrEncVal);
this.updateCharEnc(chrEncVal);
hideElement(e.target.closest(".cm-status-bar-select-content"));
}
/**
* Handler for Chr Enc keyup events
* Filters the list of selectable character encodings
* @param {Event} e
*/
chrEncFilter(e) {
const input = e.target;
const filter = input.value.toLowerCase();
const div = input.closest(".cm-status-bar-select-content");
const a = div.getElementsByTagName("a");
for (let i = 0; i < a.length; i++) {
const txtValue = a[i].textContent || a[i].innerText;
if (txtValue.toLowerCase().includes(filter)) {
a[i].style.display = "block";
} else {
a[i].style.display = "none";
}
}
}
/**
* Counts the stats of a document
* @param {EditorState} state
*/
updateStats(state) {
const length = this.dom.querySelector(".stats-length-value"),
lines = this.dom.querySelector(".stats-lines-value");
let docLength = state.doc.length;
// CodeMirror always counts line breaks as one character.
// We want to show an accurate reading of how many bytes there are.
if (state.lineBreak.length !== 1) {
docLength += (state.lineBreak.length * state.doc.lines) - state.doc.lines - 1;
}
length.textContent = docLength;
lines.textContent = state.doc.lines;
}
/**
* Gets the current selection info
* @param {EditorState} state
* @param {boolean} selectionSet
*/
updateSelection(state, selectionSet) {
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");
if (!selectionSet) {
selInfo.style.display = "none";
curOffsetInfo.style.display = "none";
return;
}
// CodeMirror always counts line breaks as one character.
// We want to show an accurate reading of how many bytes there are.
let from = state.selection.main.from,
to = state.selection.main.to;
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;
}
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");
selInfo.style.display = "inline-block";
curOffsetInfo.style.display = "none";
start.textContent = from;
end.textContent = to;
length.textContent = to - from;
} else { // Position
const offset = this.dom.querySelector(".cur-offset-value");
selInfo.style.display = "none";
curOffsetInfo.style.display = "inline-block";
offset.textContent = from;
}
}
/**
* Sets the current EOL separator in the status bar
* @param {EditorState} state
*/
updateEOL(state) {
if (state.lineBreak === this.eolVal) return;
const eolLookup = {
"\u000a": ["LF", "Line Feed"],
"\u000b": ["VT", "Vertical Tab"],
"\u000c": ["FF", "Form Feed"],
"\u000d": ["CR", "Carriage Return"],
"\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"],
"\u0085": ["NEL", "Next Line"],
"\u2028": ["LS", "Line Separator"],
"\u2029": ["PS", "Paragraph Separator"]
};
const val = this.dom.querySelector(".eol-value");
const button = val.closest(".cm-status-bar-select-btn");
const eolName = eolLookup[state.lineBreak];
val.textContent = eolName[0];
button.setAttribute("title", `End of line sequence:
${eolName[1]}`);
button.setAttribute("data-original-title", `End of line sequence:
${eolName[1]}`);
this.eolVal = state.lineBreak;
}
/**
* Sets the current character encoding of the document
*/
updateCharEnc() {
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 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:
${name}`);
button.setAttribute("data-original-title", `${this.label} character encoding:
${name}`);
this.chrEncVal = chrEncVal;
}
/**
* Sets the latest timing info
*/
updateTiming() {
if (!this.timing) return;
const bakingTime = this.dom.querySelector(".baking-time-value");
const bakingTimeInfo = this.dom.querySelector(".baking-time-info");
if (this.label === "Output" && this.timing) {
bakingTimeInfo.style.display = "inline-block";
bakingTime.textContent = this.timing.duration(this.tabNumGetter());
const info = this.timing.printStages(this.tabNumGetter()).replace(/\n/g, "
");
bakingTimeInfo.setAttribute("data-original-title", info);
} else {
bakingTimeInfo.style.display = "none";
}
}
/**
* Updates the sizing of elements that need to fit correctly
* @param {EditorView} view
*/
updateSizing(view) {
const viewHeight = view.contentDOM.parentNode.clientHeight;
this.dom.querySelectorAll(".cm-status-bar-select-scroll").forEach(
el => {
el.style.maxHeight = (viewHeight - 50) + "px";
}
);
}
/**
* Checks whether there is HTML output requiring some widgets to be disabled
*/
monitorHTMLOutput() {
if (!this.htmlOutput?.changed) return;
if (this.htmlOutput?.html === "") {
// Enable all controls
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 => {
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(".sel-info").classList.add("disabled");
this.dom.querySelector(".cur-offset-info").classList.add("disabled");
}
}
/**
* Builds the Left-hand-side widgets
* @returns {string}
*/
constructLHS() {
return `
abc
sort
highlight_alt
\u279E
( selected)
location_on
`;
}
/**
* Builds the Right-hand-side widgets
* Event listener set up in Manager
*
* @returns {string}
*/
constructRHS() {
const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP).map(name =>
`${name}`
).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.
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.
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.
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.
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 `