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,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