initial commit

This commit is contained in:
Julie Blanc 2026-01-19 22:14:03 +01:00
commit abbd549428
97 changed files with 97614 additions and 0 deletions

View file

@ -0,0 +1,333 @@
/**
* @classdesc This provides a common dictionnary for a Paged.js tools.
*
* Web Component will fetch the list of features. For each one a class will be instantiated.
* This class groups all the key data. From this list of instances
* the component will refine the data, import all necessary scripts, hooks and stylesheet.
*
* This very logic can be observed at the end of the component in the setup() function.
*
* @author Benjamin G. <ecrire@bnjm.eu>
* @author Julie Blanc <contact@julie-blanc.fr>
* @tutorial https://gitlab.com/csspageweaver/csspageweaver/-/wikis/home
*/
class CssPageWeaver_Dict {
constructor(){
console.log('CSS Page Weaver Dict initialized');
// Define a Feature class to manage individual features
this.Feature = class {
constructor(featureConfig, id, directory, config) {
this.id = id
this.directory = directory
this.ui = featureConfig.ui
this.parameters = featureConfig.parameters
this.hook = featureConfig.hook
this.script = featureConfig.script
this.stylesheet = featureConfig.stylesheet
this.globalParameters = config
}
}
this.setup()
}
/*-- Generics functions --*/
/**
* Clear and split feature directory path to define an unique ID
* @param {string} path - Feature folder path.
* @returns {string} - Feature ID.
*/
getIdFromPath(path){
return path.trim().split("/").filter(segment => segment.trim() !== '').pop()
}
/*-- Dict --*/
setSharedDictionnary(){
window.cssPageWeaver = {}
// Get the document title and remove spaces for use as a variable
cssPageWeaver.docTitle = document.getElementsByTagName("title")[0].text.replace(/ /g, "");
// Set path & directory
cssPageWeaver.directory = {}
cssPageWeaver.directory.root = `${window.location.origin}/csspageweaver`
cssPageWeaver.directory.plugins = `${cssPageWeaver.directory.root}/plugins`
// Initialise user custom files
cssPageWeaver.user = {}
// Object to hold all features
cssPageWeaver.features = {}
}
/*-- Import or list files --*/
/**
* Import the list of feature names from the manifest file or directory.
*
* This method attempts to import the feature names from a manifest.json file.
* If the manifest is not found, it falls back to listing the feature names
* from the plugin directory.
*
* @returns {Promise<Array>} A promise that resolves to an array of feature names.
*/
async importManifest(){
try{
// Attempt to read the manifest file to get the list of features
return await this.importJson(`${cssPageWeaver.directory.root}/`,`manifest.json`);
} catch(error){
console.log('Manifest not found, trying alternative method.');
// If the manifest is not found, list feature names from the plugin directory
let featuresNamesList = await this.listDir(cssPageWeaver.directory.plugins);
// Return object
return {"plugins": featuresNamesList }
}
}
/**
* Imports an HTML template file from a given directory and file name.
* @param {string} dir - The directory where the template file is located.
* @param {string} file - The name of the template file.
* @returns {Promise<string>} - A promise that resolves to the contents of the template file.
*/
async importTemplate(dir, file) {
const response = await fetch(`${dir}/${file}`);
const template = await response.text();
return template;
}
/**
* Imports a Javascript file from a specified directory.
*
* @param {string} path - The path path where the JS file is located.
* @returns {Promise<Object>} A promise that resolves to the JS content of the file.
* @throws {Error} Throws an error if the fetch request fails or if the file is not found.
*/
async importJs(path) {
try {
return await import(path)
} catch (error) {
console.error(`Error loading JS for ${path}:`, error);
}
}
/**
* Imports a JSON file from a specified directory.
*
* @param {string} dir - The directory path where the JSON file is located.
* @param {string} file - The name of the JSON file to import.
* @returns {Promise<Object>} A promise that resolves to the JSON content of the file.
* @throws {Error} Throws an error if the fetch request fails or if the file is not found.
*/
async importJson(dir, file) {
try {
const response = await fetch(`${dir}/${file}`);
if (!response.ok) {
throw new Error(`🚨 Oups. Can't find ${file} in ${this.getIdFromPath(dir)}`);
}
const json = await response.json();
return json;
} catch (error) {
console.error(`Error fetching json file in ${this.getIdFromPath(dir)}:`, error);
throw error; // Re-throw the error to be caught in importJson
}
}
/**
* Lists the directories within a specified directory by fetching and parsing its HTML content.
*
* This method fetches the automatically generated HTML page listing content of a directory,
* parses it to find links, and extracts directory names based on a date pattern.
*
* Server must allow listing directory.
* Yes. It's dirty.
*
* @param {string} dir - The URL of the directory to list.
* @returns {Promise<Array<string>>} A promise that resolves to an array of directory names.
* @throws {Error} Throws an error if the fetch request fails or if there is an issue parsing the content.
*/
async listDir(dir) {
try {
const response = await fetch(dir);
if (!response.ok) {
throw new Error('Failed to fetch plugin directory'); }
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const links = doc.querySelectorAll('a');
const directories = [];
const datePattern = /\d+\//; // Matches the first number followed by a '/'
links.forEach(link => {
const name = link.textContent;
if (name) {
const match = name.match(datePattern);
if (match) {
// Remove everything from the date part onward
const cleanedName = name.split(datePattern)[0].trim();
directories.push(cleanedName);
}
}
});
return directories;
} catch (error) {
console.error(`Error listing ${dir} items`, error);
throw error
}
}
/*-- CSS --*/
/**
* Compare declared and DOM loaded stylesheet. Warn user if he missed one.
* {array} - An array of stylesheet declared by user in manifest
*/
lookForForgottenStylesheet(userStylesheets) {
// Get all stylesheet (not for screen) already loaded
const links = document.querySelectorAll(`link[rel="stylesheet"]:not([media="screen"]`)
let domStylesheets = []
// Iterate over each link element
links.forEach(link => {
// Exlude CSS Page Weaver stylesheet
if(!link.href.includes('csspageweaver')){
domStylesheets.push(link.href.split('?')[0])
}
});
// We'll not retrieve already loaded CSS just warn user if he forget one stylesheet
//return CSSArray
let missedStylesheets = domStylesheets.filter(sheet => !userStylesheets.includes(sheet));
if(missedStylesheets.length > 0){
console.warn(`😶‍🌫️ Did you missed to include ${missedStylesheets.join(', ') } into csspageweaver/manifest.json? `)
}
}
/*-- Features --*/
/**
* Initializes features by loading their configurations and creating instances.
*
* This method reads feature names from a manifest, imports their configurations,
* and sets up their UI, hooks and scripts if available.
*/
async initializeFeatures(featuresNamesList, featuresParameters) {
// Loop all features
for (const featureName of featuresNamesList) {
const featureDir = `${cssPageWeaver.directory.plugins}/${featureName}/`
const featureEmbedConfig = await this.importJson(featureDir, `config.json`)
const featureManifestConfig = {"parameters" : featuresParameters[featureName]} || {}
const featureConfig = {...featureEmbedConfig, ...featureManifestConfig}
// Create a new instance of the feature
let feature = new this.Feature(featureConfig, featureName, featureDir, cssPageWeaver.parameters);
// Import feature's HTML template if specified
if(feature.ui){
if(feature.ui.template){
feature.ui.html = await this.importTemplate(featureDir, feature.ui.template);
}
}
// Import the feature's script and hook if specified
['script', 'hook'].forEach(property => {
if (feature[property]) {
let fileName = feature[property];
feature[property] = this.importJs(`${featureDir}/${fileName}`);
}
});
// Store the initialized feature
cssPageWeaver.features[featureName] = feature
}
}
async initializeUserHook(handlerList = []) {
cssPageWeaver.user.hook = await Promise.all(
handlerList.map(async path => {
return await this.importJs(path) // Return the promise from importJs
})
)
}
/**
* Retrieves features that have a hook from the `cssPageWeaver` object.
* @returns {Array} An array of features that have a hook. Returns an empty array if `cssPageWeaver` is undefined or does not have features.
*/
getFeaturesHookAsArray(){
// 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.hook)
.map(feature => feature.hook);
}
/**
* 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}`);
}
/*-- Setup --*/
async setup(){
// Set Shared dict
this.setSharedDictionnary()
// Import Manifest
let manifest = await this.importManifest()
// Get features list as an array of class for each
await this.initializeFeatures(manifest.plugins, manifest.pluginsParameters)
// list user stylesheets
cssPageWeaver.user.css = manifest.css || []
// list stylesheets as convenient object
cssPageWeaver.stylesheet = {
features: this.getFeaturesStyleAsArray(),
user: cssPageWeaver.user.css
}
// Warn user if he missed a stylesheet
this.lookForForgottenStylesheet(cssPageWeaver.user.css)
// Import User handlers
await this.initializeUserHook(manifest.hook)
// Dispatch an event to signal that features are loaded
const event = new Event('cssPageWeaver-dictInit');
document.dispatchEvent(event)
}
}
export default CssPageWeaver_Dict

View file

@ -0,0 +1,410 @@
/**
* @classdsec Render paged through a shadow element and return desired pages
* @author Benjamin G. <ecrire@bnjm.eu>
* @tutorial https://gitlab.com/csspageweaver/csspageweaver/-/wikis/home
* Credit: This code is based on an original idea from Julien Taquet
*/
import { Handler, Previewer } from '../lib/paged.esm.js';
import CssPageWeaver_PreRender from './pre_render.js';
// Define a custom web component
class CssPageWeaver_FrameRender extends HTMLElement {
constructor() {
super()
// Attach a shadow root to the element
this.attachShadow({ mode: 'open' });
this.range = {
from: 0,
to: Infinity
}
this.container = {
origin: {
body: document.body,
head: document.head
},
pages: null
}
this.hook = []
this.css = []
console.log('CSS Page Weaver Frame Render initialized');
}
/*-- CSS --*/
hideFrame(){
this.setAttribute("style", "position: absolute; left: 100vw; max-height: 0; width: 100vw; overflow: hidden;")
}
/**
* Copies the dimensions from the document body and applies them to the container inside the shadow DOM.
* This method ensures that the shadow DOM container matches the size and position of the document body.
*/
copyDimensionsFromBody() {
// Get the offsetHeight and client rect of the body.
const offsetHeight = this.container.origin.body.offsetHeight;
const clientRect = this.container.origin.body.getBoundingClientRect();
// Set the dimensions on the container inside the shadow DOM.
this.container.shadow.body.style.width = `${clientRect.width}px`;
this.container.shadow.body.style.height = `${clientRect.height}px`;
this.container.shadow.body.style.top = `${clientRect.top}px`;
this.container.shadow.body.style.left = `${clientRect.left}px`;
// Optionally, log the dimensions for debugging.
console.log('Body Dimensions:', { offsetHeight, clientRect });
console.log('Container Dimensions:', {
height: this.container.shadow.body.style.height,
width: this.container.shadow.body.style.width
})
}
/*-- DOM --*/
/**
* Resets the shadow root by clearing its existing content and creating a new HTML structure.
* This method ensures that the shadow root is empty and ready for new content.
*/
resetShadowRoot() {
// Clear existing content in the shadow root
while (this.shadowRoot.firstChild) {
this.shadowRoot.removeChild(this.shadowRoot.firstChild);
}
// Create new HTML structure
const head = document.createElement('head');
const body = document.createElement('body');
// Append head and body to the shadow root
this.shadowRoot.appendChild(head);
this.shadowRoot.appendChild(body);
this.container.shadow = { head, body };
}
/**
* Passes elements to the light DOM, handling both individual elements and arrays of elements.
*
* @param {Element|Element[]} element - The element or array of elements to pass to the light DOM.
* @returns {void}
*/
passElementsToLightDom(element) {
// Array means a slection of pages had been made
if (Array.isArray(element)) {
element.forEach(page => {
if (page.getAttribute('data-page-number')) {
let page_clone = page.cloneNode(true)
let page_number = parseInt(page.getAttribute('data-page-number'));
let page_ref = this.container.origin.body.querySelector(`[data-page-number="${page_number}"]`);
page_ref?.remove();
let previous = this.container.origin.pages.querySelector(`[data-page-number="${page_number - 1}"]`)
previous.insertAdjacentElement('afterend', page_clone);
// TODO Check if there are pages to remove
}
});
} else {
if (element.classList.contains('pagedjs_pages')) {
this.container.origin.body.querySelector('.pagedjs_pages')?.remove();
this.container.origin.body.appendChild(element);
} else {
let el_UI = parseInt(element.getAttribute('data-unique-identifier'));
let elementOriginal = this.container.origin.body.querySelector(`[data-unique-identifier="${el_UI}"]`);
elementOriginal?.replaceWith(element);
// TODO Need to check if break token is still coherent.
}
}
// Update pages container
this.container.origin.pages = this.container.origin.body.querySelector('.pagedjs_pages')
}
/**
* Generates a unique CSS selector for a given HTML element.
* This method attempts to create a selector that uniquely identifies the element
* based on its attributes and position in the DOM hierarchy.
*
* @param {HTMLElement} element - The HTML element for which to generate a unique selector.
* @returns {string} - A CSS selector string that uniquely identifies the element.
*/
getUniqueSelector(element) {
if(element.tagName == 'BODY'){
return 'body'
}
if (element.getAttribute('data-unique-identifier')) {
// If the element has data-unique-identifier, use it
return `[data-unique-identidier='${element.getAttribute('data-unique-identifier')}]`;
}
if (element.id) {
// If the element has an ID, use it
return `#${element.id}`;
}
if (element.className && element.className.trim() !== '') {
// If the element has a class, use it
// Note: This might not be unique if other elements share the same class
return `.${element.className.split(' ').join('.')}`;
}
// Fallback to using the tag name and its position in the hierarchy
let selector = element.tagName.toLowerCase();
let parent = element.parentElement;
return selector;
}
/* Import & reload */
/**
* Fetches a document from the specified path.
*
* @param {string} path - The URL or path to the document to be fetched.
* @returns {Promise<Document|Object>} - A promise that resolves to the fetched HTML document or JSON object.
* @throws {Error} - Throws an error if the network response is not ok or if there is a problem with the fetch operation.
*/
async fetchDocument(path) {
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error('Network response was not ok');
}
if (path.endsWith('.html') || path.endsWith('/')) {
const text = await response.text();
const parser = new DOMParser();
// Parse the HTML content into a Document object
const html = parser.parseFromString(text, "text/html");
// Get all script elements
//const scripts = html.querySelectorAll('script');
// Remove each script element
//scripts.forEach(script => script.remove());
return html;
} else if (path.endsWith('.json')) {
// Parse the JSON response
return await response.json();
}
} catch (error) {
console.error('There was a problem with the fetch operation:', error);
throw error; // Rethrow the error to handle it outside if needed
}
}
/**
* Reloads a document from the specified path and updates the instance content.
*
* @param {string} path - The URL or path to the document to be reloaded.
* @returns {Promise<void>} - A promise that resolves when the document is reloaded and the content is updated.
*/
async reloadDocument(path) {
let newDoc = await this.fetchDocument(path);
let selector = this.getUniqueSelector(this.container.origin.body);
let newContainer = await newDoc.querySelector(selector);
this._render.setUniqueIdentfier(newContainer);
// DEBUG
const elements = newContainer.querySelectorAll('p');
elements.forEach(el => {
el.textContent = el.textContent.replaceAll('e', '🙈');
});
// Set instance content with new content
this.content = this._render.storeElements(newContainer, true);
// Show me!
this.setView(false);
}
/* Paged JS */
/**
* Determines which elements or pages within a document should be passed to the light DOM.
* Handles different scenarios for selecting elements or pages to pass, including specific
* elements, all pages, or a range of pages.
*/
defineElementsToPass() {
let toPass;
// Detect if we keep a page or an element
if (this.range.element) {
// Get element to pass
toPass = this.container.pages.querySelector(`[data-unique-identifier="${this.range.element}"]`);
// Append element to light DOM
// Finally, delete the element property from the range
delete this.range.element;
console.log(`🔃 Update page ${this.range.to}`)
} else if (this.range.from === 0 && this.range.to === Infinity) {
// Just clone pagedjs_pages container
toPass = this.container.pages.cloneNode(true);
if(!this.firstTime){
console.log(`🔃 Update full document `)
}
} else {
// Array of pages to pass
toPass = [];
// Select all pages with the data-page-number attribute within the clone
const pages = this.container.pages.querySelectorAll('[data-page-number]');
// Iterate through the pages and remove those outside the range
pages.forEach(page => {
const pageNumber = parseInt(page.getAttribute('data-page-number'));
if (pageNumber >= this.range.from && pageNumber <= this.range.to) {
toPass.push(page.cloneNode(true));
}
});
console.log(`🔃 Update ${toPass.length} page${toPass.length > 1 ? 's' : ''}, from page ${this.range.from}`)
}
this.passElementsToLightDom(toPass);
}
/**
* Pagedjs library append style on document head.
* Here we edit this very fucntion to append styles on head's shadow container
* Without this, page break is erratic. Pages are missing.
*/
redefineInsertFunction(){
const polisherInstance = this.previewer.polisher;
// Define the new insert function
polisherInstance.insert = function(text) {
let style = document.createElement("style");
style.setAttribute("data-css-page-weaver-inserted-styles", "true");
style.appendChild(document.createTextNode(text));
cssPageWeaver_frame.container.shadow.head.appendChild(style);
this.inserted.push(style);
};
}
/**
* Appends Paged.js styles to the shadow DOM.
* This method creates a custom handler that clones styles from the document's head
* to the shadow DOM's head before the content is parsed.
* Without this, page break is erratic. Pages are missing.
*/
appendPagedJsStyle(){
class appendPagedStyleToShadow extends Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}
beforeParsed(content){
cssPageWeaver_frame._render.cloneElements('style[data-css-page-weaver-inserted-styles="true"]', cssPageWeaver_frame.container.shadow.head, document.head)
}
}
this.hook.push({default: appendPagedStyleToShadow})
}
/* Setup */
connectedCallback() {
// Hide the entire frame
this.hideFrame();
// Initialize a new instance of CssPageWeaver_PreRender
this._render = new CssPageWeaver_PreRender();
// Append a style link to the document's head using the interface
if (typeof this.interface === 'string' && this.interface.length > 0) {
this._render.appendStyleLink(this.interface, this.container.origin.head);
}
// Store elements from the renderer's container origin into this.content
this.content = this._render.storeElements(this._render.container.origin, false);
// Set a unique identifier for the stored content
this._render.setUniqueIdentfier(this.content);
// Copy styles appended by Paged.js to the document's head into the shadow DOM's head
this.appendPagedJsStyle();
// Set up the view for rendering
this.setView(true);
}
async setView(firstTime) {
// Initialize a new instance of Previewer
this.previewer = new Previewer();
// TODO: This should register hook on fresh instance. It dont and duplicate hook. So I set a trick here.
if(firstTime){
// Register all hooks with the renderer
await this._render.registerAllHook(this.hook, this.previewer);
}
// Modify the Paged.js polisher insert function to append styles to the shadow DOM
this.redefineInsertFunction();
// Clear the shadow root
this.resetShadowRoot();
// Append style links to both the document's head and the shadow DOM's head if interface is a non-empty string
if (typeof this.interface === 'string' && this.interface.length > 0) {
this._render.appendStyleLink(this.interface, this.container.origin.head);
this._render.appendStyleLink(this.interface, this.container.shadow.head);
}
try {
// Preview the content and log the number of rendered pages
const pages = await this.previewer.preview(
this.content,
this.css,
this.container.shadow.body
);
this.container.pages = this.shadowRoot.querySelector('.pagedjs_pages');
console.log('✅ Rendered', pages.total, 'pages');
} catch (error) {
// Handle any errors that occur during the preview operation
console.error('Error during pagination:', error);
}
// Update stylesheet by removing and cloning elements with inserted styles
let styles = 'style[data-css-page-weaver-inserted-styles="true"]';
this._render.removeElements(styles, this.container.origin.head);
this._render.cloneElements(styles, this.container.shadow.head, this.container.origin.head);
// Define elements to pass
this.defineElementsToPass();
}
}
export default CssPageWeaver_FrameRender

View file

@ -0,0 +1,510 @@
/**
* @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

View file

@ -0,0 +1,160 @@
/**
* @classdesc This are methods shared between render.js and frame_render.js
* @author Benjamin G. <ecrire@bnjm.eu>, Julie Blanc <contact@julie-blanc.fr>
* @tutorial https://gitlab.com/csspageweaver/csspageweaver/-/wikis/home
*/
import { Handler } from '../lib/paged.esm.js';
class CssPageWeaver_PreRender{
constructor(){
// Object of container shortcut
this.container = {
origin: document.body
}
}
/*-- CSS --*/
/**
* Append CSS Style as link
* @param {string} path CSS rules to append
* @param {string} destination destination elementto append style element
* @param {string} name name to append as a data-attribute
*/
appendStyleLink(path, destination, name) {
if(!path){
return
}
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = path;
link.media = 'screen';
link.setAttribute(name ? name : 'data-csspageweaver-frame', true)
destination.appendChild(link);
}
/**
* Append CSS Style as style element
* @param {string} style CSS rules to append
* @param {string} destination destination elementto append style element
* @param {string} name name to append as a data-attribute
*/
appendStyleElement(style, destination, name) {
const styleElement = document.createElement('style');
styleElement.textContent = style;
styleElement.setAttribute(name ? name : 'data-csspageweaver-frame', true)
destination.appendChild(styleElement);
}
/*-- DOM --*/
clearElement(container){
container.innerHTML = ""
}
removeElements(query, container){
const headStyles = container.querySelectorAll(query)
headStyles.forEach(style => style.remove());
}
cloneElements(query, container, destination){
const els = container.querySelectorAll(query);
els.forEach(el => {
const _el = el.cloneNode(true);
destination.appendChild(_el);
});
}
/**
* Store a copy of within a container to a document fragment, optionally keeping the original elements.
*
* @param {Element} container - The container whose child elements need to be cloned.
* @param {boolean} keep - Flag indicating whether to keep the original elements.
* @returns {DocumentFragment} A document fragment containing the cloned elements.
*/
storeElements(container, keep) {
// Create a document fragment
let content = document.createDocumentFragment();
// Clone content
container.childNodes.forEach(child => {
if (child.nodeType === 1 && (child.tagName !== 'SCRIPT' && !child.tagName.includes('CSSPAGEWEAVER'))) {
const clonedChild = child.cloneNode(true);
content.appendChild(clonedChild);
if (!keep) {
child.remove();
}
}
});
return content;
}
/**
* Sets a unique identifier for each element within a container.
*
* @param {Element} container - The container whose elements need unique identifiers.
* @returns {void}
*/
setUniqueIdentfier(container) {
const elements = container.querySelectorAll('*');
let parentCount = -1;
// Iterate over each element and assign a unique identifier using the index
elements.forEach((el, i) => {
parentCount = (el.tagName === 'SECTION' || el.tagName === 'ARTICLE') ? parentCount + 1 : parentCount;
let prefix = el.closest('article, section') ? String.fromCharCode(65 + parentCount) : '';
el.setAttribute('data-unique-identifier', `${prefix}-${i}`);
});
}
/* Paged JS */
/**
* Registers all hook features
*
* @param {Object} data - Dataset to loop for registration.
* @param {Object} previewer - PagedJs instance to register
* @returns {Promise<void>} - A promise that resolves when all features of the specified type are registered.
*/
async registerAllHook(hooks, previewer) {
/**
* Register features pagedJs custom hook.
* Hook are function happening at certain time of PagedJs workflow
* @param {Object} hookPromise - The import object for the feature.
* @param {string} id - The feature Id for debugging
*/
async function registerHook(hookPromise, id) {
const hook = await hookPromise;
if (!hook) {
return;
}
const handlerClass = hook.default || Object.values(hook)[0];
// Ensure the handlerClass is a valid Handler class
if (handlerClass.prototype instanceof Handler) {
try {
previewer.registerHandlers(handlerClass);
} catch (error) {
console.error(`An error occurred while registering the handler ${id ? `for ${id}` : ``}:`, error);
}
}
}
for (const hookPromise of hooks) {
await registerHook(hookPromise);
}
}
}
export default CssPageWeaver_PreRender

View file

@ -0,0 +1,58 @@
/**
* @classdesc This take content, paginates it and return pages to DOM. Simple.
* @author Benjamin G. <ecrire@bnjm.eu>, Julie Blanc <contact@julie-blanc.fr>
* @tutorial https://gitlab.com/csspageweaver/csspageweaver/-/wikis/home
*/
import { Previewer } from '../lib/paged.esm.js';
import CssPageWeaver_PreRender from './pre_render.js';
class CssPageWeaver_SimpleRender extends CssPageWeaver_PreRender{
constructor(){
super()
// Futur Parsed HTML to store in order to paginate
this.content = null
// Hook and CSS to pass to pagedJs. Can be edit.
this.hook = []
this.css = []
this.interface = null
console.log('CSS Page Weaver simple view initialized');
}
async setup(){
// Clone the content elements
this.content = this.storeElements(this.container.origin, false);
// Unique Id
this.setUniqueIdentfier(this.content);
this.setView()
}
async setView(){
// Set up the previewer
this.previewer = new Previewer();
await this.registerAllHook(this.hook, this.previewer);
if(typeof this.interface == 'string' && this.interface.length > 0){
this.appendStyleLink(this.interface, document.head)
}
try {
// Await the preview operation
const pages = await this.previewer.preview(this.content, this.css, this.destination);
// Log the result
console.log('Rendered', pages.total, 'pages.');
} catch (error) {
// Handle any errors that occur during the preview operation
console.error('Error during pagination:', error);
}
}
}
export default CssPageWeaver_SimpleRender