Stretch to fit content (how to best handle it?)

I’ve created a plugin to handle blog posts, with an HTML WYSIWYG Editor and a Blog Post Display Element.

I’ve used the instante.setHeight() method to implement a “stretch to fit content” functionality. The code below (the “update” function of the element) uses the height of the instance to + an adjustment to define the final height.

var div = instance.canvas; // represents the element
if (properties.stretch_to_fit_content) {
        
        var adjust = properties.bubble.border_width()*2+properties.bubble.padding_vertical()*2
        
        // Check if content is smaller than the editorHeight
        if (div.find("#blog-post-content").height() < instance.data.editorHeight) {
			
            // If the content is smaller, set the height to its original value, from the editor
            div.height(instance.data.editorHeight)
            instance.setHeight(instance.data.editorHeight + adjust)
            
        } else {
            
            // If the content is bigger, adjust the height
        	div.height("auto")
        	instance.setHeight(div.height() + adjust)           
        }
        
    }

The problem: bubble is inconsistent with the order in with the elements are loaded and sometimes the blog post overlaps with the footer of the page, probably because the content was not yet loaded.

Is there a way to guarantee this action occurs only after the content is loaded?

I’m really on the fence about revealing the trick to a Bubble safe way of resizing height to fit the content. Here goes anyways. This was one of my best kept secrets. There are three problems that have to be solved to make height resizing work:

  1. We have to determine where the scrollbars should end up. This depends on the positioning of the ancestors to the element.
  2. We have to update the height of every ancestor of the element with the correct height of the ancestors content.
  3. We have to watch for resizing of the element that actually contains the dynamic content. It will not be instance.canvas, but rather some child of instance.canvas.

To solve this problem I’m going to assume the plugin is using context.jQuery() and that you have stored a handle to the DOM element that contains the dynamic content in instance.data.dynamicelement. Note that this is the bare DOM element, not the jQuery object that wraps DOM elements.

To tackle the first problem we begin by noting that instance.canvas is a jQuery object when the plugin is using jQuery. We can then traverse the ancestors using the jQuery closest() function to find the first match to a CSS attribute selector, looking for position: fixed styles. The reason for this is that the first position: fixed ancestor is going to stay static with respect to the browser window, regardless of scrolling. Thus we need the scroll bars to appear in that element, so that it acts like a mini-browser window. For the other cases we are safe updating size all the way to the document body, and then using the browser scroll bars. Putting together we have the following one-liner:

// Find fixed pop-ups
const fixed = instance.canvas.closest('[style *= "position: fixed"]');

To tackle the second problem of resizing the ancestors we implement a ResizeObserver callback that traverses all the ancestors using the jQuery parentsUntil function and the jQuery each callback to set the ancestors height style to the internal contentscrollHeight property’s value. The nuance is that we only want to traverse the ancestors until the first fixed position ancestor, if any, that we found in the first step. Putting this together yields the more complex conditional code. Note the browser hack document.scrollingElement || document.documentElement to get the correct scrolling element for the browser window:

// Scroll bar first fixed pop-up
if (fixed.length > 0) {
  fixed.css("overflow-y", "auto");

  // Traverse to first fixed pop-up
  instance.data.sizeobserver = new ResizeObserver(
    (a, o) => context.jQuery(a[0].target)
    .parentsUntil('[style *= "position: fixed"]')

     // Assign height to content height
    .each(
      (i, e) => e.style.height = e.scrollHeight.toString() + "px"
    )
  );
}

// Scroll bar browser
else {
  context.jQuery(document.scrollingElement || document.documentElement).css("overflow-y", "auto");

  // Traverse until the document body
  instance.data.sizeobserver = new ResizeObserver(
    (a, o) => context.jQuery(a[0].target)
    .parentsUntil("body")

    // Assign height to content height
    .each(
      (i, e) => e.style.height = e.scrollHeight.toString() + "px"
    )
  );
}

Finally we have the one-liner to watch the DOM element with the dynamic content, using the stored handle instance.data.dynamicelement:

// Connect
instance.data.sizeobserver.observe(instance.data.dynamicelement);

Combining all three parts in the element’s initialize function we have:

function initialize(instance, context) {

  // Find fixed pop-ups
  const fixed = instance.canvas.closest('[style *= "position: fixed"]');

  // Scroll bar first fixed pop-up
  if (fixed.length > 0) {
    fixed.css("overflow-y", "auto");

    // Traverse to first fixed pop-up
    instance.data.sizeobserver = new ResizeObserver(
      (a, o) => context.jQuery(a[0].target)
      .parentsUntil('[style *= "position: fixed"]')

      // Assign height to content height
      .each(
        (i, e) => e.style.height = e.scrollHeight.toString() + "px"
      )
    );
  }

  // Scroll bar browser
  else {
    context.jQuery(document.scrollingElement || document.documentElement).css("overflow-y", "auto");

    // Traverse until the document body
    instance.data.sizeobserver = new ResizeObserver(
      (a, o) => context.jQuery(a[0].target)
      .parentsUntil("body")

      // Assign height to content height
      .each(
        (i, e) => e.style.height = e.scrollHeight.toString() + "px"
      )
    );
  }

  // Connect
  instance.data.sizeobserver.observe(instance.data.dynamicelement);
}
7 Likes