initial commit
This commit is contained in:
commit
abbd549428
97 changed files with 97614 additions and 0 deletions
333
csspageweaver/modules/dict.js
Normal file
333
csspageweaver/modules/dict.js
Normal 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
|
||||
410
csspageweaver/modules/frame_render.js
Normal file
410
csspageweaver/modules/frame_render.js
Normal 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
|
||||
|
||||
510
csspageweaver/modules/gui.js
Normal file
510
csspageweaver/modules/gui.js
Normal 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
|
||||
160
csspageweaver/modules/pre_render.js
Normal file
160
csspageweaver/modules/pre_render.js
Normal 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
|
||||
58
csspageweaver/modules/render.js
Normal file
58
csspageweaver/modules/render.js
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue