Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: vivliostyle doesn't call POST_LAYOUT_BLOCK for div elements #1429

Open
MoamenAbdelsattar opened this issue Dec 6, 2024 · 4 comments
Open
Labels

Comments

@MoamenAbdelsattar
Copy link
Contributor

MoamenAbdelsattar commented Dec 6, 2024

I'm trying to implement and automatic deferring plugin, which defers the layout of some images or boxes until it fits in the column to avoid breaking it and also avoid wasted white space. I did this:

let deferred = [];
let defer_types = [
  "until-fit",
]
Plugin.registerHook(
    "POST_LAYOUT_BLOCK",
    (nodeContext, checkpoints, column) => {
        
        let remaining_space = (column.afterEdge - column.beforeEdge) - column.element.firstChild.scrollHeight;  
        if(!nodeContext.viewNode.hasAttribute("data-viv-box-break")){
            for(let i = 0; i < deferred.length; i++){
                if(!nodeContext.viewNode.parentElement.matches(`[data-adapt-eloff="${deferred[i].parentEloff}"]`)) continue; // If it's not the same parent: don't try to insert
                if((deferred[i].defer_type = "until-fit" &&
                    remaining_space > deferred[i].savedHeight) ||
                    !nodeContext.sourceNode.nextSibling // If it doesn't fit but this element is the last in its parent: don't defer anymore
                  ){
                        // insert the deferred element
                        nodeContext.viewNode.before(deferred[i]);
                        remaining_space -= deferred[i].savedHeight;
                        deferred.splice(i, 1);
                        i--;
                }
            }
        }
        let defer_type = 0;
        if(!nodeContext.viewNode.hasAttribute("data-defer")) return;
        else{
            defer_type = nodeContext.viewNode.getAttribute("data-defer");
        }
        if(defer_type == "until-fit"){
            if((column.element.firstChild.scrollHeight < (column.afterEdge - column.beforeEdge)) || !nodeContext.sourceNode.nextSibling) return; // Don't defer if the element fits or if it doesn't have any other siblings
            // else
            let clone = nodeContext.viewNode.cloneNode(true);
            clone.savedHeight = nodeContext.viewNode.scrollHeight;
            clone.deferType = defer_type;
            clone.parentEloff = nodeContext.viewNode.parentElement.getAttribute("data-adapt-eloff");
            clone.removeAttribute("data-defer");
            deferred.push(clone);
            nodeContext.viewNode.style.display = "none";
        }
      },
);

Now images are deferred sucessfully.
Before:
non-deferred-image
After:
deferred-image

For a note box: the POST_LAYOUT_BLOCK hook is not called, and the note box is not deferred:
non-deferred-note

@MoamenAbdelsattar
Copy link
Contributor Author

MoamenAbdelsattar commented Dec 6, 2024

Workaround: put your note box inside in svg element:

<svg width="100%" height="100%" data-defer="until-fit">
    <foreignobject width="100%" height="100%" data-fit-height="">
       <!-- your note html here -->
    </foreignobject>
</svg>

and a little fix in the deferring code:

let deferred = [];
let defer_types = [
  "until-fit",
]
Plugin.registerHook(
    "POST_LAYOUT_BLOCK",
    (nodeContext, checkpoints, column) => {
        let inner;
        if(nodeContext.viewNode.tagName == "svg" && (inner = nodeContext.viewNode.querySelector("foreignobject[data-fit-height]"))){
            nodeContext.viewNode.setAttribute("height", inner.firstElementChild.scrollHeight);
        }
        let remaining_space = (column.afterEdge - column.beforeEdge) - column.element.firstChild.scrollHeight;  
        if(!nodeContext.viewNode.hasAttribute("data-viv-box-break")){
            for(let i = 0; i < deferred.length; i++){
                if(!nodeContext.viewNode.parentElement.matches(`[data-adapt-eloff="${deferred[i].parentEloff}"]`)) continue; // If it's not the same parent: don't try to insert
                if((deferred[i].defer_type = "until-fit" &&
                    remaining_space > deferred[i].savedHeight) ||
                    !nodeContext.sourceNode.nextSibling // If it doesn't fit but this element is the last in its parent: don't defer anymore
                  ){
                        // insert the deferred element
                        nodeContext.viewNode.before(deferred[i]);
                        remaining_space -= deferred[i].savedHeight;
                        deferred.splice(i, 1);
                        i--;
                }
            }
        }
        let defer_type = 0;
        if(!nodeContext.viewNode.hasAttribute("data-defer")) return;
        else{
            defer_type = nodeContext.viewNode.getAttribute("data-defer");
        }
        if(defer_type == "until-fit"){
            if((column.element.firstChild.scrollHeight < (column.afterEdge - column.beforeEdge)) || !nodeContext.sourceNode.nextSibling) return; // Don't defer if the element fits or if it doesn't have any other siblings
            // else
            let clone = nodeContext.viewNode.cloneNode(true);
            clone.savedHeight = nodeContext.viewNode.scrollHeight;
            clone.deferType = defer_type;
            clone.parentEloff = nodeContext.viewNode.parentElement.getAttribute("data-adapt-eloff");
            clone.removeAttribute("data-defer");
            deferred.push(clone);
            nodeContext.viewNode.style.display = "none";
        }
      },
);

Now, the note is deferred:
deferred-note

@MoamenAbdelsattar
Copy link
Contributor Author

Another issue: the hook is not called in order, and I need to make sure the deferred box is after the element in the source HTML

@MoamenAbdelsattar
Copy link
Contributor Author

MoamenAbdelsattar commented Dec 7, 2024

Since the hook isn't called for divs and uls, deferred elements can't be inserted before a list or a div element. As a workaround: I make javascript place empty elements at places where it's valid to insert a deferred element.

@MoamenAbdelsattar
Copy link
Contributor Author

MoamenAbdelsattar commented Dec 8, 2024

If someone else needs to defer nonbreaakable blocks like me, here is the last version that works without bugs untill now:

function adaptSVG(el, inner){
    el.setAttribute("height", inner.firstElementChild.scrollHeight);
}
Plugin.registerHook(
    "POST_LAYOUT_BLOCK",
    (nodeContext, checkpoints, column) => {
        let inner;
        if(nodeContext.viewNode.tagName == "svg" && (inner = nodeContext.viewNode.querySelector("foreignobject[data-fit-height]"))){
            adaptSVG(nodeContext.viewNode, inner);
        }
        let remaining_space = (column.pageFloatLayoutContext.parent.container.height) - column.element.firstChild.scrollHeight;
        let position = nodeContext.sourceNode;
        if(nodeContext.viewNode.matches("div[data-defer-position]")){
            for(let i = 0; i < deferred.length; i++){
                if(!nodeContext.viewNode.parentElement.matches(`[data-adapt-eloff="${deferred[i].parentEloff}"]`) || 
                   !(parseInt(nodeContext.viewNode.getAttribute("data-adapt-eloff")) > parseInt(deferred[i].eloff)))
                       continue; // If it's not the same parent or an element before the deferred: don't try to insert
                if((deferred[i].defer_type = "until-fit" &&
                    remaining_space > deferred[i].savedHeight) ||
                    !nodeContext.sourceNode.matches(":has(~[data-defer-position])") // If it doesn't fit but this position is the last in its parent: don't defer anymore
                  ){
                        // insert the deferred element
                        position.after(deferred[i]);
                        position = deferred[i];
                        remaining_space -= deferred[i].savedHeight;
                        deferred.splice(i, 1);
                        i--;
                        
                }
            }
        }
        let defer_type;
        if(!nodeContext.viewNode.hasAttribute("data-defer")) return;
        else{
            defer_type = nodeContext.viewNode.getAttribute("data-defer");
        }
        if(defer_type == "until-fit"){
            if((column.element.firstChild.scrollHeight < (column.afterEdge - column.beforeEdge)) || !nodeContext.sourceNode.matches(":has(~[data-defer-position])")) return; // Don't defer if the element fits or if it doesn't have any other defer-position siblings
            // else
            let clone = nodeContext.viewNode.cloneNode(true);
            clone.savedHeight = nodeContext.viewNode.scrollHeight;
            clone.deferType = defer_type;
            clone.eloff = nodeContext.viewNode.getAttribute("data-adapt-eloff");
            clone.parentEloff = nodeContext.viewNode.parentElement.getAttribute("data-adapt-eloff");
            clone.removeAttribute("data-defer");
            deferred.push(clone);
            nodeContext.viewNode.style.display = "none";
        }
      },
);

You will then use the following code to wrap the deferred blocks and insert deferring positions:

// create html form string
function CS(html, selector = ":first-child"){ 
    const temp = document.createElement('template');
    temp.innerHTML = html;
    return temp.content.querySelector(selector);
}
function Q(el, selector)  {return el.querySelector(selector)}
function QA(el, selector)  {return el.querySelectorAll(selector)}
function FillWithDeferPositions(el){
    for(let i = 0; i < el.childNodes.length; i++){
        el.childNodes[i].after(CS("<div data-defer-position>"));
        i++;
    }
}
function HandleDeferredBoxes(parent){
    QA(parent, ".defer").forEach((el)=>{
        let frame = CS(`<svg width="100%" height="100%" data-defer="until-fit"><foreignObject width="100%" height="100%" data-fit-height=""></foreignObject></svg>`);
        el.after(frame);
        Q(frame, "foreignObject").appendChild(el);
        if(!Q(frame.parentElement, "[data-defer-position]")){
            FillWithDeferPositions(frame.parentElement);
        }
    })
}

You can customize this more by, for example, not inserting deferring positions after headers.

const NoDeferPositions = "h1,h2,h3,h4,h5,h6";
function FillWithDeferPositions(el){
    for(let i = 0; i < el.childNodes.length; i++){
        if(!el.childNodes[i].matches(NoDeferPositions)){
            el.childNodes[i].after(CS("<div data-defer-position>"));
            i++;
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant