code-export-syntax/chromium/assets/js/pages/edit.js
2025-10-18 19:24:46 +00:00

345 lines
No EOL
9.4 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const KEY_STORAGE_TNX = "tnx";
const PATH_THEME_FOLDER = "/themes/";
const THEME_NAME_DEFAULT = "default_dark_modern";
const MODAL_OFFSET_TOP_PX = 25;
const CLASSNAME = Object.freeze({
ACTIVE : "active",
EMPTY : "empty",
HIGHLIGHT_SPANS : "highlight-spans"
});
const SELECTOR = Object.freeze({
SPAN_MKT : ".span-mkt",
SPAN_JOIN : "button.join",
SPAN_SPLIT : "button.split",
MODAL_ELEMENT : "edit-span-modal",
EDITOR_ELEMENT : "main",
EXPORT_ELEMENT : "[data-export]",
TOGGLE_HIGHLIGHT : "input[name='highlight-spans']"
});
class Editor {
static #editingAbort;
static #editingSpanElement;
/**
* Stop editing
*/
static stopEditing() {
this.#editingAbort?.abort();
this.#editingSpanElement?.removeAttribute("contenteditable");
this.#editingSpanElement = null;
Editor.#modalElement.classList.remove(CLASSNAME.ACTIVE);
}
/**
* Returns all elements that loads a theme stylesheet
*
* @returns {NodeList}
*/
static get #themeElements() {
return document.head.querySelectorAll(`[href^='${PATH_THEME_FOLDER}']`);
}
/**
* Returns the span editor modal element
*
* @returns {HTMLElement}
*/
static get #modalElement() {
return document.body.querySelector(SELECTOR.MODAL_ELEMENT);
}
constructor() {
this.#setContent();
this.theme = THEME_NAME_DEFAULT;
this.#bindExport();
this.#bindSplitSpan();
this.#bindModifySpan();
this.#bindSetSpanMkt();
this.#bindJoinSpanLeft();
this.#bindHighlightSpans();
this.#bindAdjustFontSizePreview();
}
/**
* Return the editor wrapper element
* @returns {HTMLElement}
*/
get editor() {
return document.body.querySelector(SELECTOR.EDITOR_ELEMENT);
}
/**
* Get the name of the current theme
*
* @returns {string}
*/
get theme() {
return Editor.#themeElements() ? Editor.#themeElements()[0].dataset.themeName : null;
}
/**
* Update current theme
*
* @param {string} themeName
*/
set theme(themeName = THEME_NAME_DEFAULT) {
if (typeof themeName !== "string") {
throw Error("Theme name expects a string");
}
// Remove existing themes
Editor.#themeElements.forEach(element => element.remove());
// Create link element to load stylesheet from themes directory
const element = document.createElement("link");
element.rel = "stylesheet";
element.href = PATH_THEME_FOLDER + THEME_NAME_DEFAULT + ".css";
element.dataset.themeName = themeName;
document.head.appendChild(element);
}
/**
* Set the current font size
* @param {number} size
*/
set fontSize(size) {
Editor.stopEditing();
this.editor.style.setProperty("--font-size", `${size}px`);
// TODO: Adjust span top and height
}
/**
* Save the current editor HTML to session storage
*/
save() {
chrome.storage.session.set({ [KEY_STORAGE_TNX]: this.editor.innerHTML });
}
/**
* Start editing a span element
*
* @param {HTMLSpanElement} spanElement
*/
editSpan(spanElement) {
if (!(spanElement instanceof HTMLSpanElement)) {
throw Error("Target element must be of type HTMLSpanElement");
}
// Stop edting any existing spans
Editor.stopEditing();
Editor.#editingAbort = new AbortController();
Editor.#editingSpanElement = spanElement;
// Create event listener to filter characters when editing this span content
document.addEventListener("keyup", (event) => {
// Stop editing on enter key is pressed
if (["Enter", "NumpadEnter"].includes(event.code)) {
event.preventDefault();
return Editor.stopEditing();
}
this.#setSpanText(spanElement.innerText);
}, { signal: Editor.#editingAbort.signal });
// Stop editing if main is clicked around this span
this.editor.addEventListener("click", (event) => {
if (event.target !== spanElement) {
Editor.stopEditing();
}
}, { signal: Editor.#editingAbort.signal });
spanElement.setAttribute("contenteditable", true);
spanElement.focus();
this.#editSpanModal(spanElement);
}
/**
* Open editor modal on a span element
*
* @param {HTMLSpanElement} spanElement
*/
#editSpanModal(spanElement) {
const x = spanElement.offsetLeft;
const y = spanElement.offsetTop + MODAL_OFFSET_TOP_PX;
// Set text preview
this.#setSpanText(spanElement.innerText);
// Mark the current mkt on the span as selected in the modal editor
Editor.#modalElement.querySelector(`.${spanElement.classList.toString()}`)?.classList.add(CLASSNAME.ACTIVE);
// Scroll lists to the top
Editor.#modalElement.querySelectorAll("ul, ol").forEach(element => element.scrollTop = 0);
Editor.#modalElement.style.top = `${y}px`;
Editor.#modalElement.style.left = `${x}px`;
//Editor.#modalElement.style.setProperty("transform", `translate(${x}px, ${y}px)`);
Editor.#modalElement.classList.add(CLASSNAME.ACTIVE);
}
/**
* Set the preview texts in the span editor modal
*
* @param {string} text
*/
#setSpanText(text) {
Editor.#editingSpanElement.classList.remove(CLASSNAME.EMPTY);
// Span has no text
if (text.length < 1) {
text = "";
Editor.#editingSpanElement.classList.add(CLASSNAME.EMPTY);
}
// Update the preview text in the span editor modal
Editor.#modalElement.querySelectorAll(SELECTOR.SPAN_MKT).forEach(element => element.querySelector("p").innerText = text);
}
/**
* Sets the editor content with HTML from session storage
*/
async #setContent() {
const content = await chrome.storage.session.get(KEY_STORAGE_TNX);
this.editor.innerHTML = content[KEY_STORAGE_TNX];
}
/**
* Bind adjust font size input elements
*/
#bindAdjustFontSizePreview() {
const inputElements = document.body.querySelector("#text-size").querySelectorAll("input");
inputElements.forEach(inputElement => {
// Update all input element values when any input element changes
inputElement.addEventListener("input", () => {
inputElements.forEach(element => element.value = inputElement.value);
this.fontSize = inputElement.value;
})
});
}
/**
* Bind listener for editing spans
*/
#bindModifySpan() {
// We're going to bind all clicks on main since we might have a lot of span elements
this.editor.addEventListener("click", (event) => {
// Bail out if the target element is not a span
if (!(event.target instanceof HTMLSpanElement)) {
return;
}
this.editSpan(event.target);
});
}
/**
* Bind set span mtk by clicking a text preview in the span modal list
*/
#bindSetSpanMkt() {
Editor.#modalElement.querySelectorAll(SELECTOR.SPAN_MKT).forEach(element => {
element.addEventListener("click", (event) => {
// Bail out if we're not editing anything
if (!Editor.#editingSpanElement) {
return;
}
// Make the span in the editor the same mkt as the one in the modal
Editor.#editingSpanElement.classList = event.target.querySelector("p").classList.toString();
// Mark all mkts in the modal as inactive
Editor.#modalElement.querySelectorAll(SELECTOR.SPAN_MKT).forEach(element => element.classList.remove(CLASSNAME.ACTIVE));
// Make the current mkt active
event.target.classList.add(CLASSNAME.ACTIVE);
});
});
}
/**
* Bind button for joining the span being edited with the one to the left
*/
#bindJoinSpanLeft() {
document.body.querySelector(SELECTOR.SPAN_JOIN).addEventListener("click", () => {
// Bail out if we're not editing anything or element doesn't have a previous element
if (!Editor.#editingSpanElement || !Editor.#editingSpanElement.previousElementSibling) {
return;
}
// Concatinate editing span element text with previous element
Editor.#editingSpanElement.previousElementSibling.innerText += Editor.#editingSpanElement.innerText;
// Shift editing to the previous element
this.editSpan(Editor.#editingSpanElement.previousElementSibling);
// Delete the original span
Editor.#editingSpanElement.nextElementSibling.remove();
});
}
/**
* Bind button for splitting the span being edited into two spans at the cursor location
*/
#bindSplitSpan() {
document.body.querySelector(SELECTOR.SPAN_SPLIT).addEventListener("click", () => {
const selection = window.getSelection();
// Bail out if we're at the start or end of the span text
if (selection.baseOffset < 1 || selection.baseOffset === Editor.#editingSpanElement.innerText.length) {
return;
}
const newSpan = Editor.#editingSpanElement.cloneNode();
// Set text of cloned span to everything ahead of the cursor
newSpan.innerText = Editor.#editingSpanElement.innerText.slice(selection.baseOffset);
// Truncate the text of the original span up to the cursor
Editor.#editingSpanElement.innerText = Editor.#editingSpanElement.innerText.slice(0, selection.baseOffset);
// Append new span and shift editing towards it
Editor.#editingSpanElement.insertAdjacentElement("afterend", newSpan);
this.editSpan(newSpan);
});
}
/**
* Bind button for toggling highlighting of individual span elements
*/
#bindHighlightSpans() {
document.body.querySelector(SELECTOR.TOGGLE_HIGHLIGHT).addEventListener("click", (event) => {
event.target.checked
? this.editor.classList.add(CLASSNAME.HIGHLIGHT_SPANS)
: this.editor.classList.remove(CLASSNAME.HIGHLIGHT_SPANS);
});
}
/**
* Bind export HTML buttons
*/
#bindExport() {
document.querySelectorAll(SELECTOR.EXPORT_ELEMENT).forEach(element => {
element.addEventListener("click", () => {
const url = new URL(window.location);
url.pathname = element.dataset.export;
this.save();
window.open(url, "_blank");
});
})
}
}
globalThis._Editor = new Editor();