Skip to main content

Adds an expand button to the code examples found in <pre><code> blocks, allowing more content to display horizontally.
Deprecated in favour of CSS resize:horizontal property

Expanding example <pre><code> blocks

Deprecated in favour of CSS resize:horizontal property. Resource kept for reference only.

Presented here is a rough example on extending the <pre><code> block, containing examples of code, horizontally breaking out of existing column constraints.

At a simple visual level the code is working, though it has limitations (PC chunky scrollbars), and should be considered as a work-in-progress.

Still assessing accessibility impact. Will most probably hide the button from screen-readers as its action would be completely pointless. Though what of the keyboard chain?

How it basically works

Looks for occurrences of the data-expand data attribute and adds an activation button.

Clicking the button clones the entire container, then places it outside the scope of the block which restrains its width. It is then overlaid at the same visual position, and the width is animated via CSS to expand into the increased area available.

Expected HTML structure

Consider the .column block has a width, defined for the layout, which doesn't allow the full content of data-expand to be visible.

Language JavaScript
<div class="column">
  …
  <div data-expand>
    <pre>
      <code>
        <!-- Requires more horizontal space to be fully visible -->
        <!-- Yes this content will scroll horizontally, but sometimes you need to see more -->
      </code>
    </pre>
  </div>
  …
</div>

Note that the body element should not have a width stated to allow expansion.

Test examples

All measurements for the examples were using a browser width of 48rem (768px) with a base font size of 16px. The code samples presented are meaningless.

  1. All code content is displayed naturally, without scroll bars. Does not need an expand button.
    Language JavaScript
    .navigation {
      position: fixed;
    }
  2. Code content wider than displayed, but narrower than the available expanded area. Scroll bars do not appear when expanded, but do while minimised.
    Language JavaScript
    "use strict";
    var fixedObj = document.getElementById("navigation");
  3. Expanded code content is wider than the display area. Scroll bars are always present.
    Language JavaScript
    
    (function(){function e(){var b=pageYOffset,a=c.getBoundingClientRect().height;Math.abs(d-b)<=a/2||(b>d&&b>a?c.classList.add("hide"):c.classList.remove("hide"),d=b)}var c=document.getElementById("navigation"),a=!1,d=0;c&&(window.addEventListener("scroll",function(){a=!0},!1),setInterval(function(){a&&(requestAnimationFrame(e),a=!1)},250))})();

The JavaScript nitty gritty

Presented a little roughly for now. I'll tidy up as soon as I get the time.

Overview and closure.

Language JavaScript
(function(){

  var dataAttribute;

  function findXYPos(obj) {…}
  function willHaveScrollBars(element)  {…}
  function hasScrollBars(element) {…}
  function isExpandable(element) {…}
  function removeClone(prop) {…}
  function addClone(prop) {…}
  function btnClicked() {…}
  function btnFocussed() {…}
  function btnBlurred() {…}
  function addButtonListeners(btn) {…}
  function newButton(blockObj, config) {…}
  function removeClones() {…}
  function removeExpander() {…}
  function initialise(config) {…}
  function addExpander() {…}

  addExpander();
  window.addEventListener('resize', debounce(addExpander, 300, false));
  window.addEventListener('resize', debounce(removeExpander, 300, true));

})();

Functions

Language JavaScript
  // http://www.quirksmode.org/js/findpos.html
  function findXYPos(obj) {
    var X = Y = 0;
    if (obj.offsetParent) {
      do {
        X += obj.offsetLeft;
        Y += obj.offsetTop;
      } while (obj = obj.offsetParent);
    }
    return {x : X, y : Y};
  }

Language JavaScript
function willHaveScrollBars(element) {
  // if preW > maxW - (2* left px) then should have scroll bars when expanded, but may be auto-hidden
  var maxW = d.body.scrollWidth - (2 * findXYPos(element).x);
  return element.getElementsByTagName("pre")[0].scrollWidth > maxW;
}
Language JavaScript
function hasScrollBars(element) {
  // true if div height > pre height
  return element.scrollHeight > element.getElementsByTagName("pre")[0].scrollHeight;
}
Language JavaScript
function isExpandable(element) {
  // true if pre width > div width
  var preObj = element.getElementsByTagName("pre"),
      value = false;
  if (preObj) {
    value = preObj[0].scrollWidth > element.scrollWidth;
  }
  return value;
}
Language JavaScript
function removeClone(prop) {
  var cloneObj = d.getElementById(prop.expandId);
  if (cloneObj) {
    var style = cloneObj.getAttribute("style");
    style = style.replace("max-width:" + prop.maxWidth, "max-width:" + prop.minWidth);
    requestAnimationFrame(function(){
      openingSound.play();
      cloneObj.setAttribute("style", style);
      cloneObj.getElementsByTagName("button")[0].classList.remove("ON");
      // remove clone button ON

      // allow animation time to complete before complete removal
      setTimeout(function(){
        requestAnimationFrame(function(){
          var blockObj = d.getElementById(prop.blockId);
          d.body.removeChild(cloneObj);

          // Restore original shadows
          blockObj.classList.remove("OFF");
        });
      }, 700);
    });
  }
}
Language JavaScript
function addClone(prop) {
  var blockObj = d.getElementById(prop.blockId);

  // clone the whole block
  var cloneObj = blockObj.cloneNode(true);
  cloneObj.classList.add("cloned");
  cloneObj.id = prop.expandId;
  cloneObj.setAttribute("style",
    "top:" + prop.top + "px; "
    + "left:" + prop.left + "px; "
    + "min-width:" + prop.minWidth + "px; "
    + "max-width:" + prop.minWidth + "px; "
  );

  // add button click event to close
  clonedBtn = cloneObj.getElementsByTagName("button")[0];
  if (clonedBtn) {
    clonedBtn.prop = prop;
    clonedBtn.addEventListener("click", btnClicked, false);
  }
  var style = cloneObj.getAttribute("style");
  style = style.replace("max-width:" + prop.minWidth, "max-width:" + prop.maxWidth);
  requestAnimationFrame(function(){

    // Remove original shadows
    blockObj.classList.add("OFF");
    d.body.appendChild(cloneObj);

    // Animate in
    requestAnimationFrame(function(){
      cloneObj.setAttribute("style", style);
      cloneObj.getElementsByTagName("button")[0].classList.add("ON");
      openingSound.play();
    });
  });
}
Language JavaScript
function btnClicked() {
  var btn = this;
  var isExpanded = d.getElementById(btn.prop.expandId);
  if (isExpanded) {
    removeClone(btn.prop);
  } else {
    addClone(btn.prop);
  }
}
Language JavaScript
function btnFocussed() {
  d.getElementById(this.prop.blockId).classList.add("lift");
}

function btnBlurred() {
  d.getElementById(this.prop.blockId).classList.remove("lift");
}

function addButtonListeners(btn) {
  //console.log("Adding Events to btn = " + btn.prop.blockId);
  btn.addEventListener("click", btnClicked, false);

  // hovering / focussing the button should lift the entire block visually.
  btn.addEventListener("mouseover", btnFocussed, false);
  btn.addEventListener("focus", btnFocussed, false);
  btn.addEventListener("mouseout", btnBlurred, false);
  btn.addEventListener("blur", btnBlurred, false);
}
Language JavaScript
function newButton(blockObj, config) {
  var btn = d.createElement('button');
  var XY = findXYPos(blockObj);
  var svgClasses = config.svgClasses ? config.svgClasses : "svg-plus";

  // btn.setAttribute("aria-pressed", false);
  btn.className = config.btnClasses ? config.btnClasses : "expand-btn";
  btn.id = blockObj.id + "_btn";
  btn.prop = {
    blockId : blockObj.id,
    expandId : blockObj.id + "_clone",
    top : XY.y,
    left: XY.x,
    minWidth : blockObj.scrollWidth,
    maxWidth : d.body.scrollWidth - 2 * XY.x
  };
  btn.innerHTML = '<span><svg class="' + svgClasses +'" width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg"><title>More</title><path d="M10.5 19l17 0"/><path class="h" d="M19 10.5l0 17"/></svg></span>';
  // addButtonListeners(btn);
  return btn;
}
Language JavaScript
function removeClones() {
  var clones = d.getElementsByClassName("cloned"),
      i = clones.length;
  while (i--) {
    clones[i].parentNode.removeChild(clones[i]);
  }
}
Language JavaScript
function removeExpander() {
  var clones = d.getElementsByClassName("cloned"),
      i = clones.length,
      timer,
      expands,
      buttons;

  while (i--) {
    clones[i].setAttribute("style", clones[i].getAttribute("style") + ";max-width:280px");
  }

  timer = setTimeout(removeClones, 500);

  var expands = d.querySelectorAll("[" + dataAttribute + "]");
  i = expands.length;
  while (i--) {
    buttons = expands[i].getElementsByTagName("button");
    if (buttons.length) {
      buttons[0].parentNode.removeChild(buttons[0]);
    }
    expands[i].classList.remove("OFF");
    expands[i].removeAttribute("id");
  }
}
Language JavaScript
function initialise(config) {
  dataAttribute = config.dataAttribute ? config.dataAttribute : "data-expand";
  var expands = d.querySelectorAll("[" + dataAttribute + "]");
  var i = expands.length;
  var blockObj, btnObj;
  if (i) {
    while (i--) {
      blockObj = expands[i];
      if (!isExpandable(blockObj)) {
        continue;
      }
      if (!blockObj.id) {
        blockObj.id = "cb" + i;
      }
      btnObj = newButton(blockObj, config);
      blockObj.appendChild(btnObj);
      addButtonListeners(btnObj);
    }
  }
}
Language JavaScript
function addExpander() {
  initialise({
    dataAttribute : "data-expand",
    btnClasses : "expand-btn"
  });
}

To do:

Needs top position calculating on click to allow for late loaded or dynamic content.

Socialise: