Blog-Archiv

Dienstag, 24. März 2015

JS Slide Show Aftermath

Because the ~ 500 lines of code for slideshow.js took much more time than I expected, I won't discuss the source in details (like I announced in the slide show of my last Blog). Instead I will try to show up the nuts and bolts I've come across when writing that script.

For a demo of that project you can always have a look at the slide show on my homepage. Within that show you also can find all sources.

Render HTML Elements in a Slide

This is not really difficult. I did it by copying the element into the slide:

1
2
3
4
5
6
      var displayInSlide = function(htmlElement) {
        slideframe.innerHTML = htmlElement.outerHTML; // copy the element into slide
        
        var slide = slideframe.children[0];
        scaleContents(slide);
      };

Setting the element's outerHTML to the innerHTML of the slide frame does the work. Care must be taken when having an HTMLCollection of slides retrieved by document.getElementsByClassName(). This could be a "live" collection that detects the creation of a new (copied) element with the according class, and then you would have another slide in your show :-) Best is to copy the list of slides into an array before starting the show.

For normal text slides you will want them to be centered, and with a bigger font. Both is done by following CSS:

.slideframe  {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 98%; 
  height: 98%; /* enable vertical centering */
  margin: auto; /* makes it centered, at least horizontally */
  overflow: auto;

  /* default font, overridable in slideshow.js */
  font-size: 130%;
  font-family: sans-serif;
}

It is the display: flex that does the centering work here. Supported only by new browsers, not IE-10.

Mind that only slides smaller than the display area will be centered. When you want a centering effect also on bigger slides, or even long-text slides, you need to do something like the following:

      var scaleText = function(slide) {
        var downsizeTextVerticalPercent = 80;
        var downsizeTextHorizontalPercent = 70;

        // to keep vertical centering, set width/height only when content bigger than frame
        // but this MUST be done, else long text would be cut on top
        if (slide.scrollHeight >= controls.slideframe.clientHeight * downsizeTextVerticalPercent / 100)
          slide.style.height = downsizeTextVerticalPercent+"%";

        if (slide.scrollWidth >= controls.slideframe.clientWidth * downsizeTextHorizontalPercent / 100)
          slide.style.width = downsizeTextHorizontalPercent+"%";
      };

Display Hyperlinks

In HTML there is a tag called OBJECT. This is a wildcard element that takes every kind of URL and tries its best to display that URL. When the URL points to a web page, it embeds a browser view into the current page, having its own scrollbar and event queue. You can not receive key presses or mouse clicks into that page by a JS event listener in parent page. It is like an IFRAME element.

This works quite well for all kinds of resources, even for videos, although InternetExplorer (once again) will do things different, especially when rendering JS resources in an OBJECT tag.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
      var displayHyperLink = function(hyperLink) {
        slideframe.innerHTML =
          "<object type='text/html' height='100%' width='98%' data='"+hyperLink+"'></object>";
      };

      var display = function() {
        var htmlElement = slidesQueue.getCurrent();

        var hyperLink = (htmlElement.href || htmlElement.src);
        var isImage = (htmlElement.tagName === "IMG");

        if (hyperLink && ! isImage)
          displayHyperLink(hyperLink);

        ....
      };

Scale Images

You don't want your beautiful photos to appear in a complete wrong aspect ratio. The web advices us to use ....

img {
  height: 100%;
  width: auto;
}

.... for images that should have a dynamic size but want to keep their proportion. But what if the 100% height would make parts of the image disappear because it's a landscape? Then you would like to define width to 100% and height to auto. I solved this using JS and CSS as follows:

.imageHeightTooBig  {
  height: 100%;
  width: auto;
}
.imageWidthTooBig  {
  width: 100%;
  height: auto;
}

      var scaleImage = function(slide) {
        // precondition is that image is loaded and displayed
        var width = slide.naturalWidth || slide.scrollWidth;
        var height = slide.naturalHeight || slide.scrollHeight;
        var widthTooBig = (width >= controls.slideframe.clientWidth);
        var heightTooBig = (height >= controls.slideframe.clientHeight);
        
        if (widthTooBig || heightTooBig) {
          var className = heightTooBig ? "imageHeightTooBig" : "imageWidthTooBig";
          if (slide.className.indexOf(className) < 0)
            slide.className += " "+className;
        }
      };

This code does nothing when neither height nor width of the image (slide) are too big. In any other case it finds out which of them is too big and applies the according CSS class to the element. When both are too big this would result in 100% height and auto width.

That way the image never should have a scrollbar, and its aspect ratio should be correct. When making the browser bigger and reloading the image you will have a bigger picture.

It is always better to have an image in original aspect-ratio and without scrollbars. To look deeper into it there should be some kind of zoom installed.

Lazy Loading Images

When having an slide show with a lot of high-resolution images you will wait a long time when loading that page. The browser would try to resolve all images as soon as the HTML has been rendered.

To avoid this you can remove the src attribute from the IMG tag and put the URL into the alt attribute (= alternative text) instead. There must be some JS logic then that sets the src attribute when the slide is displayed. This would look like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<body>

  <h1>My Best Photos</h1>

  <img alt="hikes/IMG_4021.JPG"/>
  <img alt="hikes/IMG_4104.JPG"/>
  <img alt="hikes/IMG_4171.JPG"/>
  ....
  ....
  <!-- lots of photos ... -->

  
  <script type="text/javascript" src="slideshow.js">
  </script>
  
</body>

There is no src attributes on these IMG elements, thus the browser would not load anything. As soon as one of the elements is displayed, JS can set the src attribute and thus start the image loading. When it is fully loaded, the image can be scaled and centered.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
          if ( ! slide.src && slide.alt ) // lazy loading images have src in alt attribute
            slide.src = slide.alt;

          if (slide.complete) { // is loaded
            scaleImage(slide);
          }
          else { // wait until loaded
            var scaleImageLater = function() {
              slide.removeEventListener('load', scaleImageLater); // important!
              scaleImage(slide);
            };
            slide.addEventListener('load', scaleImageLater);
          }

Let Choose the Slide Number

I started to implement this with hash-tags (the part behind the '#' in browser address line). An URL of my slide show looks like this (for slide 25):

http://fritzthecat-blog.blogspot.co.at/2015/03/#25

Editing that number would take you to the desired slide, that was my intent.

I then detected that I needed to click the browser "Reload" button to actually load that slide, pressing the ENTER key was not enough (at least for Firefox). I introduced an "hashchange" event listener and started to experiment, but quickly cancelled all efforts then, because this caused strange effects, slides were skipped and similar.

Unfortunately it is not possible to get back from the slide show to the previous location by using the browser "Back" button. Which is really ugly. I will try to realize this with HTML-5 history API.

Working with AMD

AMD demands that you use no global variables. Every module X needed to do the work of a module Y should be defined as dependency of Y. It is then passed as parameter to the factory-function of Y by the AMD loader.
Other parameters that are not modules must be passed to the module-function that the factory-function returns. To have an imagination how that looks, here comes an example.

 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
"use strict";

define( // AMD module definition

  [ // dependency parameters, path relative to AMD base-path
    "queue",
    "slideshow/slidecontrols"
  ],

  function( // factory function, parameters are parallel to dependencies
    queue,
    slidecontrols
  )
  {
    return function(maximum, imageListener) // module function
    // page-specific configurations go to the module function
    {
      var install = function() {
        ....
        queue.initialize(maximum);
        ....
        slidecontrols.initialize(imageListener);
        ....
      };

      install();

    }; // module function end

  } // module factory function end

); // define module end

This is the archetype you have to live with when applying AMD. The whole module definition is enclosed in a define() call. The first parameter to it is an array of module identifiers, the second a factory function that produces the module when called by the AMD loader. The AMD loader resolves the dependencies and passes their results as parameters to the factory function. The result of that factory call is taken to be the module, be it an object or a function.

Working with my personal AMD loader I had to write an AMD caller for each HTML page that uses JS. This caller is for providing parameters to the module function. Here is an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
"use strict";

define( // AMD module definition

  [ // dependency parameters, path relative to AMD base-path
    "slideshow/slideshow.js"
  ],
  
  function(slideshow) // factory function
  {
    var useDownwardButton = false;
    var documentInitiallyVisible = false;

    slideshow(
        useDownwardButton,
        documentInitiallyVisible
    );
  } // factory function end

); // define module end

Here the AMD factory function is used to call the dependency module (slideshow) with some page-specific configuration parameters.

A particular "pain in the brain" are circular references. Assume that module A needs B, but B needs also some services by A. Such should be resolved by cutting out from module A the part that module B needs, putting it to a new module C. Module B can than be built by passing C, and A can be built by passing B and C.

Implementing with AMD it not so easy. The same with refactoring. Being already difficult in JS, this is even more difficult in an AMD environment.

The deeper you go into JS, the more you find out that it is a writeonly language :-(



Keine Kommentare: