walter-boente_book-collection/csspageweaver/modules/gui.js

510 lines
14 KiB
JavaScript
Raw Normal View History

2026-01-19 22:14:03 +01:00
/**
* @classdesc A web component that provides a GUI for a Paged.js previewer.
* Web Component will compose (or import) a HTML template and attach event listener
* to allow front-end interactions.
* @extends HTMLElement
* @author Benjamin G. <ecrire@bnjm.eu>, Julie Blanc <contact@julie-blanc.fr>
* @tutorial https://gitlab.com/csspageweaver/csspageweaver/-/wikis/home
* Credit: This code is based on an original idea from Julie Blanc
*/
class CssPageWeaver_GUI extends HTMLElement {
constructor (){
super()
console.log('CSS Page Weaver GUI Component initialized');
}
/*-- Dict --*/
extendSharedDict(){
cssPageWeaver.directory.interface = `${cssPageWeaver.directory.root}/interface`
// Initialize UI elements
cssPageWeaver.ui = {}
cssPageWeaver.ui.body = document.querySelector('body')
cssPageWeaver.ui.shortcut_index = [] // For user convenience, store all keyboard shortcut
// Initialize parameters and events
cssPageWeaver.ui.event = {}
// Set API
cssPageWeaver.helpers = {
addKeydownListener: this.addKeydownListener.bind(this)
};
}
/*-- CSS --*/
/**
* Loads a CSS file and appends it to the document head.
* @param {string} path - The path to the CSS file.
*/
appendCSStoDOM(path) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = path;
link.setAttribute('data-css-page-weaver-gui', true)
document.head.appendChild(link);
}
/**
* Aggregates the CSS paths from all features that have a stylesheet.
* @returns {Array} An array of CSS file paths.
*/
getFeaturesStyleAsArray(){
// Convert the features object to an array
const featuresArray = Object.values(cssPageWeaver.features);
// Filter to only features with a hook
return featuresArray
.filter(feature => feature.stylesheet)
.map(feature => `${feature.directory}${feature.stylesheet}`);
}
/*-- DOM --*/
/**
* Creates a panel for a feature with the specified UI configuration.
*
* This method constructs a form element for a feature, including a title,
* description, toggle switch, and additional HTML content if specified.
* It also handles shortcuts for the feature.
*
* @param {string} id - The ID of the feature.
* @param {Object} ui - The UI configuration object for the feature.
* @returns {HTMLElement} The created form element representing the feature's panel.
*/
createPanel(id, ui){
function createTitle(){
if(ui.title){
// Create a title for this feature
const title = document.createElement('h1');
title.textContent = ui.title;
if(ui.description){
title.title = ui.description
}
// Compose title container
titleContainer.appendChild(title);
}
}
function createDescription(){
if(ui.description){
// Create a details element for the description
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = '?'
const p = document.createElement('p')
p.textContent = ui.description
// compose details container
details.appendChild(summary)
details.appendChild(p)
// Append the details container to the title container
titleContainer.appendChild(details)
}
}
function createToggle(){
//
// If feature require a simple ON/OFF toggle,
if(ui.toggle){
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `${id}-toggle`;
checkbox.name = `${id}-toggle`;
const label = document.createElement('label');
label.htmlFor = `${id}-toggle`;
label.id = `label-${id}-toggle`;
const seeSpan = document.createElement('span');
seeSpan.className = 'button-see button-not-selected';
seeSpan.textContent = 'see';
const hideSpan = document.createElement('span');
hideSpan.className = 'button-hide';
hideSpan.textContent = 'hide';
label.appendChild(seeSpan);
label.insertAdjacentHTML('beforeEnd', ' '); // This little hack to preserve harmony with handmade template
label.appendChild(hideSpan);
// Append input & label to group-title
titleContainer.appendChild(checkbox);
titleContainer.appendChild(label);
form.classList.add('button-toggle')
// Add toggle to API
cssPageWeaver.ui[id] = {
toggleInput: checkbox,
toggleLabel: label
}
cssPageWeaver.ui.event[id] = {
toggleState: checkbox.checked
}
}
}
function displayShortcut(){
if(ui.shortcut){
// Function to convert key array to a human-readable string
function keysToString(keyArray) {
const keyMapping = {
"shiftKey": "Shift",
"ctrlKey": "Ctrl",
"altKey": "Alt",
"metaKey": "Meta"
};
// Transform the array to a string like 'Ctrl + Z'
const humanReadableKeys = keyArray.map(key => keyMapping[key] || key);
return humanReadableKeys.join(' + ');
}
const shortcutList = document.createElement('ul')
shortcutList.className = "shortcut-list"
ui.shortcut.forEach(item => {
// If item disable shortcut display on panel
if(item.tutorial == false){
return
}
// Create a list item for the shortcut
const li = document.createElement('li')
li.textContent = Array.isArray(item.keys) ? keysToString(item.keys) : item.keys
li.title = item.description;
// Append the item to the shortcut list
shortcutList.appendChild(li)
})
// Append Shortcut list to its feature panel
form.appendChild(shortcutList)
}
}
// Create a form for this feature
const form = document.createElement('form');
form.className = `panel-group`;
form.id = `${id}-form`;
// Create Title Container for this feature
const titleContainer = document.createElement('div');
titleContainer.className = 'panel-group-title';
createTitle()
createDescription()
createToggle()
// Append title group to form
form.appendChild(titleContainer);
// Append additional template to form
if(ui.html){
form.insertAdjacentHTML("beforeEnd", ui.html)
}
displayShortcut()
// Return complete feature panel
return form
}
/**
* Creates the panel container and subpanels for each feature.
*
* This method constructs a panel container and populates it with subpanels
* for features that have a UI. It also lists features without a UI in a
* separate section.
*/
createMainPanel() {
// Create Panel container
const formContainer = document.createElement('div');
formContainer.id = 'cssPageWeaver_panel';
// Create base input to toggle Menu
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'cssPageWeaver_toggle-panel';
checkbox.name = 'toggle-panel';
checkbox.checked = this.isPanelOpen()
const label = document.createElement('label');
label.htmlFor = 'cssPageWeaver_toggle-panel';
const openSpan = document.createElement('span');
openSpan.id = 'panel-open';
openSpan.textContent = '';
const closedSpan = document.createElement('span');
closedSpan.id = 'panel-closed';
closedSpan.textContent = '≡';
label.appendChild(openSpan);
label.appendChild(closedSpan);
this.appendChild(checkbox);
this.appendChild(label);
// Convert the features object to an array
const featuresArray = Object.values(cssPageWeaver.features);
// Lets filter to only features with control panel
const featuresWithPanels = featuresArray.filter(feature => feature.ui && feature.ui.panel !== null && feature.ui.panel !== undefined);
// Append the panels to the panel container
featuresWithPanels.forEach(feature => {
formContainer.insertAdjacentElement('beforeEnd', feature.ui.panel);
});
// Create Hidden Feature Panel
this.createHiddenFeaturePanel(featuresArray, formContainer)
// Append panel container to main element
this.appendChild(formContainer);
}
createHiddenFeaturePanel(featuresArray, formContainer){
// Create a list of active Hook or script without UI
const featuresWithoutPanels = featuresArray.filter(feature => !feature.ui);
// List these elements on the interface if there are any
if(featuresWithoutPanels.length > 0){
const details = document.createElement('details');
details.id = "hidden-features"
// Create Summary with title
const summary = document.createElement('summary');
const title = document.createElement('h1');
title.textContent = "Also active";
const span = document.createElement('span');
span.textContent = `${featuresWithoutPanels.length} plugin${featuresWithoutPanels.length > 1 ? 's' : ''}`
// Append Title to summary
summary.appendChild(title)
summary.appendChild(span)
// Create list
const ul = document.createElement('ul');
// Create a list of features without panels
featuresWithoutPanels.forEach(feature => {
const li = document.createElement('li');
li.textContent = feature.id;
ul.appendChild(li);
});
// Append elements to details container
details.appendChild(summary)
details.appendChild(ul)
// Append unlisted features container to main container
formContainer.insertAdjacentElement('beforeEnd', details);
}
}
/*-- Features --*/
/**
*/
loopFeatures_attachPanel() {
Object.values(cssPageWeaver.features).forEach(feature => {
if(feature.ui){
// Create a UI panel for the feature
feature.ui.panel = this.createPanel(feature.id, feature.ui);
}
})
}
/*-- Script --*/
/**
* Registers all script features
*
* @param {Object} data - Dataset to loop for registration.
* @returns {Promise<void>} - A promise that resolves when all features of the specified type are registered.
*/
async runAllScript(data){
async function runScript(scriptPromise, parameters){
// Await the script promise to get the script
const script = await scriptPromise;
// If the script or its default export is not available, exit the function
if(!script || !script.default){
return
}
new script.default(parameters)
}
if (Array.isArray(data)) {
// If data is an array, iterate over each item directly
for (const item of data) {
await runScript(item);
}
} else {
// If data is an object,
for (const key in data) {
if (data.hasOwnProperty(key) && data[key].script) {
await runScript(data[key].script, data[key].id);
}
}
}
}
/*-- Events --*/
/**
* Connects the custom element to the DOM.
*/
async connectedCallback () {
this.addEventListener('click', this);
this.addEventListener('input', this);
document.addEventListener('cssPageWeaver-dictInit', this.setup.bind(this));
}
/**
* Handles events for this Web Component.
* @param {Event} event - The event object.
*/
handleEvent (event) {
let featureId = this.findParentID(event)
this[`on${event.type}`](event, featureId);
}
/**
* Find subpanel ID targeted by event
* @param {event} - the event object
*/
findParentID(event){
let parent = event.target.parentNode;
while (parent) {
if (parent.id && parent.id.includes('-form')) {
const id = parent.id.split('-form')[0];
return id
break;
}
parent = parent.parentNode;
}
}
/**
* Handles the click event for a feature.
*
* @param {Event} event - The click event object.
* @param {string} featureId - The ID of the feature being clicked.
*/
onclick (event, featureId) {
this.togglePanel(event)
}
/**
* Handles the input event for a feature.
*
* @param {Event} event - The input event object.
* @param {string} featureId - The ID of the feature being interacted with.
*/
oninput (event, featureId) {
// If the input event is for a toggle element
if(cssPageWeaver.ui.event && event.target.id == `${featureId}-toggle` ){
cssPageWeaver.ui.event[featureId].toggleState = event.target.checked
}
}
/**
* Toggles the state of a panel based on the event target.
* @param {Event} event - The event object that triggered the toggle action.
*/
togglePanel(event){
// Handle Toogle
if(event.target.id == "cssPageWeaver_toggle-panel"){
let isOpen = this.isPanelOpen()
localStorage.setItem('gui_toggle' + cssPageWeaver.docTitle, !isOpen)
}
}
/**
* Checks whether the panel is open or not.
* @returns {boolean} - Whether the panel is open or not.
*/
isPanelOpen(){
return localStorage.getItem('gui_toggle' + cssPageWeaver.docTitle) == 'true' || false
}
/*-- Helpers --*/
/**
* This function associate a keydown event to a function
* Important: scope is document wide, when above events are component specific
* @param {array} keyArray - Keyboard keys combinaison to listen to
* @param {function} callback - Function to call when right selection is pressed
*/
addKeydownListener(keyArray, callback) {
document.addEventListener('keydown', (event) => {
const isKeyPressed = keyArray.every(key => {
if (key === 'shiftKey') return event.shiftKey;
if (key === 'ctrlKey') return event.ctrlKey;
if (key === 'altKey') return event.altKey;
if (key === 'metaKey') return event.metaKey;
return event.key === key;
});
if (isKeyPressed) {
callback();
}
});
// Store shortcut information for convenience
cssPageWeaver.ui.shortcut_index.push(`${keyArray.join(" + ")} is set for ${callback.name}`)
}
/*-- Setup component --*/
setup (){
this.extendSharedDict()
// Load basic CSS assets
this.appendCSStoDOM(`${cssPageWeaver.directory.interface}/css/panel.css`);
// Load features CSS
cssPageWeaver.stylesheet.features.forEach(path => this.appendCSStoDOM(path))
// Create and populate main Panel
this.loopFeatures_attachPanel()
this.createMainPanel()
// Register plugins scripts in a PagedJs after render event
this.runAllScript(cssPageWeaver.features)
}
}
export default CssPageWeaver_GUI