initial commit
This commit is contained in:
commit
abbd549428
97 changed files with 97614 additions and 0 deletions
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
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue