Blog-Archiv

Sonntag, 13. November 2016

"Realized" Markers by JS and CSS

Imagine you need to study or memorize some text, hard to read, hard to realize, hard to remember. If you think you got it, you want to mark it. Coming back some minutes later you can test yourself if you really got it.

This Blog shows how dynamic "Realized"-markers on an HTML page can by implemented via JS. It consists of some CSS and related JavaScript. Restriction is that these markers will disappear when you reload the page (browser-refresh).

As a proof of concept, every chapter and title on this Blog page can be marked as realized by a double-click, or clicking it and typing 'r'. Then a green "Got it!" label should appear on the right side of the text, and additionally a green hook on the left side of a text spanning multiple lines.

Any JS code here is implemented in pure JS, no jQuery is used. Mind that it won't work on IE-10 and older.

Example


Double-click this, or click and type 'r'!


User Stories

As a web-browser user of possibly long pages I want to mark those chapters and paragraphs that I "realized", so that I have control about what I've already read on that page.

When using "realized" markers, in case I find out I did not get it and need to read it once more, I want to remove the marker with exactly the same gesture as I have set it.

Specification

As normally a user clicks quite frequently into a page, a single mouse-click would be a too weak gesture for adding and removing markers. The gesture to set a marker should be a double click. Additionally also the "r" on keyboard should work the same way.

The JS part will install event listeners and dispatch those events. The dispatcher will have to ignore inline elements and seek up to the related block parent element. Clicks to elements that have several "markable" elements within them must be ignored.

The CSS part will decide over the shape and location of the markers. The marker could be an image, a text, or a Unicode symbol. It could be placed left or right of the text, or on both sides (useful for text that spans several lines). But it also could be a border, or a titled border.

Implementation

JavaScript

Following source code is introduced top-down. Put it on bottom of your HTML page before the </body> end tag, and replace the "...." by all the other functions listed below.

Install Event Listeners

  <script type="text/javascript">
    
    .... // further functions will be here

    var currentElement;
     
    window.addEventListener("dblclick", function(event) {
      var isLeftMouseButton = (event.which === 1);
      if (isLeftMouseButton)
        markingListener(event.target);
    });
    
    window.addEventListener("click", function(event) {
      currentElement = event.target;
    });
    
    document.addEventListener("keydown", function(event) {
      var isR = (event.key === 'r');
      if (isR && currentElement)
        markingListener(currentElement);
    });

  </script>

The double-click listener ("dblclick") decides whether it is the left mouse-button, and when so, dispatches the event to function markingListener().

The click listener ("click") stores the currently focused element to variable currentElement in case a keyboard-event follows. Unfortunately the keyboard-event does not point to the currently focused element.

The keyboard listener ("keydown") checks whether "r" has been pressed, and uses variable currentElement to dispatch the event to markingListener() when so.

Dispatch Events

    var markingListener = function(clickedElement) {
      while (isInline(clickedElement)) /* ignore inline-elements */
        clickedElement = clickedElement.parentElement;
      
      if (isMarkable(clickedElement)) {
        clickedElement.classList.toggle("realized");
        
        if (spansSeveralRows(clickedElement))
          clickedElement.classList.toggle("multiline");
      }
    };

    var isInline = function(element) {
      var style = window.getComputedStyle(element);
      return style.display === 'inline';
    };

The clickedElement parameter of markingListener() designates the HTML element where the event occurred. As long as it is an inline element, it gets adjusted to its parent, because it does not make sense to mark small text spans. When the element finally is one that is "markable", the CSS class "realized", representing the marker, is set to it. Additionally, when it spans several lines, a "multiline" CSS class is set onto it.

The .classList.toggle() function should be supported by all new HTML-5 browsers. It removes the CSS class when present, and adds it when not.

Presentation Logic

    var isMarkable = function(element, lastLevel) {
      if (isInline(element))
        return false;
      
      if ( ! lastLevel )
        for (var i = 0; i < element.children.length; i++)
          if (isMarkable(element.children[i], true))
            return false;
        
      return true;
    };

An element counts as markable when it does not contain other markable elements. Mind that this implementation is recursive, and it uses a parameter to avoid a deeper recursion than just one child level. Only when parameter lastLevel was not given (undefined), or it was false, the condition if ( ! lastLevel ) would become true.

Now here is the function that determines whether an element spans multiple lines. It is somehow experimental, mixed together from various sources on the Internet.

    var spansSeveralRows = function(element) {
      var oldHTML = element.innerHTML;
      var oldHeight = element.offsetHeight;

      element.innerHTML = "X";
      var oneLineHeight = element.offsetHeight;

      element.innerHTML = oldHTML;

      return oneLineHeight < oldHeight;
    };

This replaces the text of the given element by an 'X' and then compares the new height with the former height. As 'X' for sure will be a single line, the height should be smaller than the original height when it was multi-line. Of course the old text is then restored. This test causes a short flickering, but it works quite reliable.

CSS

Put following CSS into the <head> tag of your HTML page.

  <style type="text/css">

    .realized::after  {
      content: 'Got it!';
      white-space: nowrap;

      background-color: lightGreen;
      border: 1px solid gray;
      border-radius: 0.1em;
      
      padding: 0.04em;
      margin-left: 0.2em;
    }

    .realized.multiline::before  {
      content: '\002611'; /* hook */
      
      background-color: lightGreen;
    }

  </style>

This CSS uses pseudo-elements as markers. A pseudo-element is specified after a "::" separator. Pseudo-elements can be styled like normal elements. Additionally they support the content property, which can be a text (Unicode symbol), an image, or other things like an attribute-value of the referenced element.

The selector .realized::after { .... } reads as

  • "Put contained styles on a dynamically created element, located after any element with CSS class realized".

The selector .realized.multiline::before would locate the created element before its reference element.

The notation '\002611' is the hexadecimal number of a Unicode character representing a checked checkbox. To avoid ambiguities, that number must be filled up with leading zeros, so that exactly six digits are present.

Roundup

As you can see, it is quite easy to get these markers onto the page, but the technical details to achieve good usability can become demanding. The DOM API is not as comfortable and clear as other user-interface APIs with less history burden may be.




Keine Kommentare: