Blog-Archiv

Montag, 18. Januar 2016

JS Titled Border

Titled Border

Not that I want to repeat all those desktop user interface patterns in the browser environment, but a titled border is simply a beautiful and elegant way to style a chapter in a web page.


This is a HTML fieldset with legend

HTML provides this by the FIELDSET element. When you nest a LEGEND element into it, you will also get a title and a border. I wrote this article before I detected that element type.

WARNING: this article uses pure JavaScript (JS), no jQuery:-(, and ignores web-browsers that do not follow standards.

Here comes a CSS-only variant of "titled border", without JS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1"/>
    
    <style>
      .titled-border {
        border: 1px solid gray;
        border-radius: 0.5em;
        position: relative; /* be parent for absolutely positioned title */
        padding-top: 0.5em; /* leave half of the title height space on top */
      }
      .border-title {
        position: absolute; /* go out of the layout, let followers fill up */
        top: -0.5em; /* slip upwards */
        left: 0.4em; /* distance to border corner */
        background-color: white; /* cover underlying border */
        padding: 0 0.3em 0 0.3em; /* leave space between border and text, left and right */
      }
    </style>
 
  </head>
  
  <body>
  
    <div class="titled-border">
      <span class="border-title"><b>Titled Border</b></span>
      
      <div>
        Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim.
      </div>
    </div>
      
  </body>
</html>

I created two CSS rules-sets, the selector .titled-border is for the border and .border-title is for the text.

The .titled-border carries the border specification, and a relative positioning, which is for being the coordinate system to the absolutely positioned title element. Further it specifies a padding-top to make place for the title that will be lifted there.

The .border-title is positioned absolutely, it goes upwards half a letter height, and a little to the left to keep distance from the border corner. Then it needs a defined background-color to not let strike through the border. The final paddings are the gaps between the corder and the title.

The HTML is simple, any border is an enclosing div, and the title is its first child element. Best are inline-elements like span, as they do not expand across the whole width, like block-elements do.

Is it that easy?

I thought I was done, and tried this out in combination with different HTML. Here is a screenshot of the result.

As you see, there is a significant problem with positioning the title text, and letting the border be visible where it should be. Easy only for left-aligned titles, and the real problems start when using a <H1> as title, because this has margins and is full-width.

Write CSS for each such box?

So I faced the necessity to either write new CSS for every bordered title I come across, or develop this further using JavaScript (JS). Of course I wanted to reuse a concept that makes it easy to style the title and the border independently of their CSS declarations.

Here comes my titled-border solution

Here comes my titled-border solution!

CSS

    <style type="text/css">
      .titled-border {
        border: 2px solid gray;
        border-radius: 0.5em;
      }
    </style>

Not much CSS is left. Positioning must be done dynamically, same for the background-color of the text. You could even leave out this CSS and style each border individually via inline-styles. But mind that the (now following) JS will search for the classes titled-border and border-title, so you need at least to set titled-border upon the border-element. By default the first child could count as title, so this class can be ommitted, except on nested titled borders, where the JS would fail when border-title was not set upon the title element.

JS

Here is the scetch of the script that will evolve now.
You can find the complete source on bottom of this page, or on my homepage.

    <script type="text/javascript">
      "use strict";

      var titledBorderFactory = function(TITLED_BLOCK_CLASS, BLOCK_TITLE_CLASS)
      {
        var that = {};

        // ....

        that.init = function(topElement) {
          topElement = topElement || document.body;
          // ....
        };

        return that;
      };

      titledBorderFactory("titled-border", "border-title").init();
    
    </script>

This is the closure (or module) that encapsulates functions and variables working on titled borders. As top-level factory-function parameters I want to pass the CSS class-names of the titled-border and the border-title, so that this is not restricted to titled-border and border-title. Then I would like to pass an optional top-level element where to search below for titled borders, in case I want to have different areas on my page. The default for this would, of course, be the document.body element to process the whole page. All the now following functions go into that closure.

Find Titled Border Elements

Here is the init() implementation, which could be understood as constructor.

        /**
         * Initializes all titled blocks below given element or in whole document.
         * @param topElement optional, the container to search for titled borders, default is document.body.
         */
        that.init = function(topElement) {
          topElement = topElement || document.body;
          var titledBlocks = topElement.querySelectorAll("."+TITLED_BLOCK_CLASS);

          for (var i = titledBlocks.length - 1; i >= 0; i--) {
            var titledBlock = titledBlocks[i];
            var title = titledBlock.querySelector("."+BLOCK_TITLE_CLASS); /* find first child with given CSS class */
            if ( ! title ) /* by default take first child */
              title = titledBlock.children[0];

            if (title)
              positionTitle(titledBlock, title);
          }
        };

This sets the default in case no parameter has been given. Then it searches for CSS class titled-border below the top element. For each of them, it retrieves the related title element and passes both for further processing to positionTitle().

Position the Title on the Border

Here comes the hard layout work to be done. It is commented extensively, so try to read the comments and understand the source by them. That is what comments are made for. Functions used here may be implemented in the utils on bottom.

        /** Moves the title into a div that is created as first child of the block. */
        var positionTitle = function(titledBlock, title) {
          var titleHeight = title.offsetHeight; /* height including padding and border */
          var titleStyle = window.getComputedStyle(title); /* need to calculate margins */
          var titleMargins = getTopAndBottomMargins(titleStyle);
          var fullTitleHeight = titleHeight + titleMargins;

          var titleContainer = createTitleContainer(titleHeight, fullTitleHeight, getAlignment(titleStyle));
          styleTitle(titleStyle, title, titledBlock);
          styleTitledBlock(titleHeight, titledBlock);

          titledBlock.insertBefore(titleContainer, titledBlock.children[0]); /* insert titleContainer at start */
          title.parentElement.removeChild(title); /* move title from parent to titleContainer */
          titleContainer.appendChild(title);
        };

This finds together some variables that are used to style the elements now. Most important is the full height of the title. The negative half of this will be the top offset. Mind that the computed style is used here, because with element styles you might see only the inline-element margins.

Then a new element is created, to be used as wrapper for the title. This is needed to align the title text left, center or right, given by its own text-align CSS property. The new element is styled in its own function, as are the border and the title.

Finally the new element is inserted as first child into the border-container, and the title element is moved into the new element.

        /** Creates title container that allows alignment. */
        var createTitleContainer = function(titleHeight, fullTitleHeight, alignment) {
          var titleContainer = document.createElement("div"); /* need a new element */
          
          titleContainer.style.position = "absolute"; /* go off the layout, let fill up following elements */
          titleContainer.style["background-color"] = "transparent"; /* let shine through border */
          titleContainer.style.top = (-fullTitleHeight / 2)+"px"; /* slip upwards above the border */
          titleContainer.style.left = "0"; /* to enable alignment, stick to the left */
          titleContainer.style.width = "100%"; /* to enable alignment, take whole width */
          titleContainer.style["box-sizing"] = "border-box"; /* do not cause overflows */
          titleContainer.style["text-align"] = alignment; /* adopt alignment from title */
          if (alignment !== "center") /* keep distance from border-corner */
            titleContainer.style["padding-"+alignment] = (titleHeight / 3)+"px";
          
          return titleContainer;
        };

        /** Prepares title. */
        var styleTitle = function(titleStyle, title, titledBlock) {
          var backgroundColor = titleStyle["background-color"];
          if (isTransparent(backgroundColor )) /* must not be transparent, border would strike through */
            title.style["background-color"] = findParentBackgroundColor(titledBlock);
          
          title.style["padding-left"] = "0.2em"; /* distance between text and border */
          title.style["padding-right"] = "0.3em";
          title.style["display"] = "inline-block"; /* makes block-elements lose 100% width, this enables alignment */
          title.style["white-space"] = "nowrap"; /* do not break title into new line */
          title.style["overflow"] = "hidden"; /* do not write into other elements */
        };

        /** Prepares bordering block. */
        var styleTitledBlock = function(titleHeight, titledBlock) {
          titledBlock.style.position = "relative"; /* be parent for absolute titleContainer */
          titledBlock.style["margin-top"] = (titleHeight / 2)+"px"; /* avoid title to slip out of page on top */
          titledBlock.style["padding-top"] = (titleHeight / 2)+"px"; /* avoid followers to overlap with title above */
        };

The title-container needs to be absolutely positioned towards the relative border-container. It goes upwards (negative top) half of its height. It must be transparent to let the border strike through, and it needs to be full-width to enable alignment. Then it sets the alignment, avoiding layout problems by using box-sizing: border-box.

The title styles must guarantee that the border does not strike through, using a background color. This is retrieved from the parents when not declared on the element itself, default is white. Important here is the display: inline-block style. It makes any title that was defined as block element shrink its width to what is really needed. Only that way I can align in its container block.

The border element finally needs to be positioned relatively, because it will be the coordinate system for the absolute title-container. It must provide a margin-top for the title to not slip out of the page on top, and a padding-top for the following child elements to not slip into the lower part of the title.

Utils

Here are the remaining functions that have been used in the code above.

        var findParentBackgroundColor = function(element) {
          while (element && element !== document.documentElement) {
            var style = window.getComputedStyle(element);
            var backgroundColor = style["background-color"];
            if (backgroundColor && ! isTransparent(backgroundColor))
              return backgroundColor;

            element = element.parentElement;
          }
          return "white";
        };

        var isTransparent = function(color) {
          return color === "transparent" || color === "rgba(0, 0, 0, 0)";
        };

        var getAlignment = function(style) {
          var alignment = style["text-align"];
          return ! alignment ? "left" : (alignment === "start") ? "left" : alignment;
        };

        var getTopAndBottomMargins = function(style) {
          return window.parseInt(style["margin-top"]) + window.parseInt(style["margin-bottom"]);
        };

Mind that always the computed style is passed to these functions, not the element-style. Remarkable may be the isTransparent() function, amazingly there is no standard for this. Also remarkable the text-align property value "start" that I encountered on Webkit browsers.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
      "use strict";

      /**
       * Creates an object with an init() function that can layout titled borders.
       * @param TITLED_BLOCK_CLASS optional, name of CSS class designating the bordered container, without leading dot.
       * @param BLOCK_TITLE_CLASS optional, name of CSS class designating the title, without leading dot.
       * @returns an object to call init().
       */
      var titledBorderFactory = function(TITLED_BLOCK_CLASS, BLOCK_TITLE_CLASS)
      {
        var that = {};

        var findParentBackgroundColor = function(element) {
          while (element && element !== document.documentElement) {
            var style = window.getComputedStyle(element);
            var backgroundColor = style["background-color"];
            if (backgroundColor && ! isTransparent(backgroundColor))
              return backgroundColor;

            element = element.parentElement;
          }
          return "white";
        };

        var isTransparent = function(color) {
          return color === "transparent" || color === "rgba(0, 0, 0, 0)";
        };

        var getAlignment = function(style) {
          var alignment = style["text-align"];
          return ! alignment ? "left" : (alignment === "start") ? "left" : alignment;
        };

        var getTopAndBottomMargins = function(style) {
          return window.parseInt(style["margin-top"]) + window.parseInt(style["margin-bottom"]);
        };

        /** Creates title container that allows alignment. */
        var createTitleContainer = function(titleHeight, fullTitleHeight, alignment) {
          var titleContainer = document.createElement("div"); /* need a new element */
          
          titleContainer.style.position = "absolute"; /* go off the layout, let fill up following elements */
          titleContainer.style["background-color"] = "transparent"; /* let shine through border */
          titleContainer.style.top = (-fullTitleHeight / 2)+"px"; /* slip upwards above the border */
          titleContainer.style.left = "0"; /* to enable alignment, stick to the left */
          titleContainer.style.width = "100%"; /* to enable alignment, take whole width */
          titleContainer.style["box-sizing"] = "border-box"; /* do not cause overflows */
          titleContainer.style["text-align"] = alignment; /* adopt alignment from title */
          if (alignment !== "center") /* keep distance from border-corner */
            titleContainer.style["padding-"+alignment] = (titleHeight / 3)+"px";
          
          return titleContainer;
        };

        /** Prepares title. */
        var styleTitle = function(titleStyle, title, titledBlock) {
          var backgroundColor = titleStyle["background-color"];
          if (isTransparent(backgroundColor )) /* must not be transparent, border would strike through */
            title.style["background-color"] = findParentBackgroundColor(titledBlock);
          
          title.style["padding-left"] = "0.2em"; /* distance between text and border */
          title.style["padding-right"] = "0.3em";
          title.style["display"] = "inline-block"; /* makes block-elements lose 100% width, this enables alignment */
          title.style["white-space"] = "nowrap"; /* do not break title into new line */
          title.style["overflow"] = "hidden"; /* do not write into other elements */
        };

        /** Prepares bordering block. */
        var styleTitledBlock = function(titleHeight, titledBlock) {
          titledBlock.style.position = "relative"; /* be parent for absolute titleContainer */
          titledBlock.style["margin-top"] = (titleHeight / 2)+"px"; /* avoid title to slip out of page on top */
          titledBlock.style["padding-top"] = (titleHeight / 2)+"px"; /* avoid followers to overlap with title above */
        };

        /** Moves the title into a div that is created as first child of the block. */
        var positionTitle = function(titledBlock, title) {
          var titleHeight = title.offsetHeight; /* height including padding and border */
          var titleStyle = window.getComputedStyle(title); /* need to calculate margins */
          var titleMargins = getTopAndBottomMargins(titleStyle);
          var fullTitleHeight = titleHeight + titleMargins;

          var titleContainer = createTitleContainer(titleHeight, fullTitleHeight, getAlignment(titleStyle));
          styleTitle(titleStyle, title, titledBlock);
          styleTitledBlock(titleHeight, titledBlock);

          titledBlock.insertBefore(titleContainer, titledBlock.children[0]); /* insert titleContainer at start */
          title.parentElement.removeChild(title); /* move title from parent to titleContainer */
          titleContainer.appendChild(title);
        };

        /**
         * Initializes all titled blocks below given element or in whole document.
         * @param topElement optional, the container to search for titled borders, default is document.body.
         */
        that.init = function(topElement) {
          topElement = topElement || document.body;
          var titledBlocks = topElement.querySelectorAll("."+TITLED_BLOCK_CLASS);

          for (var i = titledBlocks.length - 1; i >= 0; i--) {
            var titledBlock = titledBlocks[i];
            var title = titledBlock.querySelector("."+BLOCK_TITLE_CLASS); /* find first child with given CSS class */
            if ( ! title ) /* by default take first child */
              title = titledBlock.children[0];

            if (title)
              positionTitle(titledBlock, title);
          }
        };

        return that;
      };

      /* initialize all titled borders */
      titledBorderFactory("titled-border", "border-title").init();
    



Keine Kommentare: