Blog-Archiv

Samstag, 20. Juni 2015

JS Asynchronous Waiting

JavaScript is a single-threaded language. JS can not create a parallel running activity that does one thing while another activity thread does another thing. Which implicates that JS needs no synchronization, and concurrency problems do not occur in a JS environment. We should be happy, one less piece of complexity!

But how do we do when we want to set a CSS-class onto an HTML element, but the element is not present yet?
We can't spawn a thread, but we can use the global predefined JS function setTimeout() to do that later when the element might already exist. That function accepts a callback function and a millisecond timeout as parameters, and it will call the callback function when the timeout has elapsed.

var timeout = 4000; // 4 seconds

setTimeout(
  function() {
    alert("The timeout of "+timeout+" ms elapsed!");
  },
  timeout
);

When you put this JS code into a web page, you will see a dialog after 4 seconds. This will not repeat.

The technique of invoking things later occurs in many UI environments. In Java/Swing it is the SwingUtilities.invokeLater() method, where the event-queue is used to post a Runnable instance for later execution.

This Blog develops a JS wait() function that permits asynchronous function execution at a time when some condition becomes true. Similar to Promises.

Specification

Here are the User Stories for the "wait module".

  • Condition and Success Action
    I want to execute a success function as soon as a condition function returns true. Because I do not know at what time the condition function will return true (it might e.g. check the existence of a SCRIPT element), it must be called periodically by the browser until it returns true. Then, and only then, the success function must be called.

  • Time Parameters
    I want to define a maximum time for evaluating the condition, and I want to specify how frequently the evaluation is tried, both as millisecond durations. The software must check that the evaluation interval is not bigger than the maximum wait time. When I pass zero as maximum, or a negative time amount, I want to wait unlimited for the condition (page lifetime).

  • More Conditions
    Maybe there will be several condition functions. For success, either all of them must be true at the same time, or each of them must have been true at least once before the evaluation expires, this must be configurable.

  • Given-Up Action
    Additionally to the condition and success functions, optionally I want to pass a givenUp function to be called when the condition never got true until the evaluation expires.

  • Progress Action
    Further I optionally want to pass a progress function that should be called on each evaluation attempt. This could be useful for displaying the elapsed time, or to animate some activity indicator. Additionally this function should have the opportunity to cancel the waiting, by returning false instead of true.

Design

Here is the well-documented interface of what will be usable from outside, as JS code.

    /**
     * Creates a wait object for given millisecond parameters.
     * @param maximumWaitMillis optional, default 4000 milliseconds, the time
     *     after which wait should return unsuccessfully when condition never
     *     became true.
     * @param delayMillis optional, default 100 milliseconds, the time after
     *     which the condition should be tested again.
     * @param allMustBeTrueAtSameTime optional, default false, when true,
     *     all conditions must be true at same time, it is not enough that
     *     a condition gets true shortly for a time while others are false.
     */
    var waitFactory = function(maximumWaitMillis, delayMillis, allMustBeTrueAtSameTime)
    {
      var that = {};

      ....

      /**
       * Waits for 1-n conditions to become true.
       * @param conditions required, an array of functions that all must return
       *     true for success() to be called.
       * @param success required, a function to be executed when all conditions
       *     returned true.
       * @param givenUp optional, a function to be executed when time ran out.
       * @param progress optional, a function to be executed any time the
       *     condition is checked.
       */
      that.waitForAll = function(conditions, success, givenUp, progress) {
        ....
      };

      /**
       * Convenience function to wait for just one condition.
       */
      that.waitFor = function(condition, success, givenUp, progress) {
        that.waitForAll([ condition ], success, progress, givenUp);
      };
      
      return that;
    };

And here is the (really important!) example usage.

  var idToFind = "idToFind";

  waitFactory(4000).waitFor( // wait 4 seconds
    function() { return document.getElementById(idToFind) !== undefined; },
    function() { alert("The element "+idToFind+" exists!"); }
    function() { alert("Sorry, the element "+idToFind+" does not exists!"); }
    function() { return document.getElementById("cancelBox").checked; }
  );

This will wait 4 seconds for the existence of element with id "idToFind". In case the element is already present, or appears within the given amount of time, an alert-dialog will pop up and tell about it. Else another alert will appear after 4 seconds and tell that waiting time elapsed unsuccessfully. When the user clicks at the checkbox with id "cancelBox", the wait loop will end at the next evaluation attempt.

Implementation

There are some things I want to have done properly:

  • default value definitions must be once-and-only-once
  • assert that
    • required parameters are really given, and their values are in the allowed range, throw an exception when not
    • a started waitInstance is not started again before its time expired
  • avoid code duplication: the processing of a single condition must forward to the processing of several conditions

Generally I try to avoid instance variables, I prefer parameters wherever possible. The more instance vars you have, the more vulnerable your object is to state conflicts. With parameters nothing can go wrong, you have the things that you need at the place where it is needed. This is also nice for refactoring.

Default Value Definitions

Defaults must be assigned before assertions check parameter values.

    var waitFactory = function(maximumWaitMillis, delayMillis, allMustBeTrueAtSameTime)
    {
      if (delayMillis === undefined || delayMillis <= 0)
        delayMillis = 100;

      if (maximumWaitMillis === undefined)
        maximumWaitMillis = 4000;
        
      ....

Here time defaults are defined when parameters were not given. The default for allMustBeTrueAtSameTime is false, but this needs not to be defined here as long as you do not compare the parameter directly to true or false: if ( ! allMustBeTrueAtSameTime ). This will be true when allMustBeTrueAtSameTime is either false or undefined.

I let pass through maximumWaitMillis === 0 or <= 0, this will permit to wait "forever".

Mind that I do not use the maximumWaitMillis = maximumWaitMillis || 4000; pattern, because this would also strike when maximumWaitMillis is zero!

Parameter Assertions

Assertions should reject erroneous calls as early as possible, with an adequate message. If you implement functions that do not check their own preconditions, you implement incalculable risks.

    var waitFactory = function(maximumWaitMillis, delayMillis, allMustBeTrueAtSameTime)
    {
      .... // default definitions

      if (delayMillis >= maximumWaitMillis)
        throw "Maximum wait time ("+maximumWaitMillis+") must be greater than evaluation delay ("+delayMillis+")!";
        
      ....

A delayMillis evaluation interval that is greater than the maximumWaitMillis time would prevent success in any case, this makes no sense.

      that.waitForAll = function(conditions, success, givenUp, progress) {
        if ( ! conditions || ! conditions.length || ! success )
          throw "Expected conditions and success function!";
          
        for (var i = 0; i < conditions.length; i++)
          if ( ! conditions[i] )
            throw "Expected condition at index "+i;
        
        ....

Undefined functions in the conditions array do not make sense, so it is checked here. Mind that this does not call the success function, it just tests whether the function pointer is undefined.
The givenUp and progress functions are optional, thus they are not checked.

Conditions Aggregation

Now I want to wrap the (possibly several) condition functions into one function that tests them all in two different ways:

  1. either all of them must have been true at least once before wait time expires
  2. or all of them must be true at the same evaluation attempt.
      var conditionChecks;

      that.waitForAll = function(conditions, success, givenUp, progress) {
        .... // parameter assertions
        
        var condition = function() {
          var result = true;
          for (var i = 0; i < conditions.length; i++) {
            var condition = conditions[i];
            if (conditionChecks) { // when caller defined an array, use it to buffer results
              if ( ! conditionChecks[i] && ! (conditionChecks[i] = condition()) )
                result = false;
            }
            else if ( ! condition() ) {
              return false;
            }
          }
          return result;
        };

        startToWait(condition, success, givenUp, progress);
      };

The conditions parameter is an array containing all condition functions. The conditionChecks variable will be initialized to an array holding returned booleans when allMustBeTrueAtSameTime is true, or undefined when not.

Now look at the local condition() function:

  1. The first variant is achieved when the conditionChecks array has been defined (is not undefined).
  2. The second variant will take place when it is undefined, because then the loop is broken by the return statement whenever one of the conditions is false.

Mind that the local condition() function is not executed here, it is passed to startToWait() for later usage.

Why is the condition() function a local inner function? Because that way I don't need to store the conditions parameter into an instance-variable that.conditions. As I said, I prefer parameters.

This aggregation works for a single condition as well as for several of them.

Start to Wait

      var working;

      var startToWait = function(condition, success, givenUp, progress) {
        if (working)
          throw "Can not wait for other conditions while working!";
          
        setWorking(true);
        wait(condition, success, givenUp, progress, undefined);
      };
    
      var setWorking = function(work) {
        working = work;
        
        if ( ! allMustBeTrueAtSameTime )
          conditionChecks = [];
      };

Here comes the private part of this JS module. The startToWait() function will not be visible outside, nevertheless it is inseparably connected to all variables and functions within the waitFactory function, and they all together will survive the call of their factory as a closure, being accessible via the that object. This is the JS encapsulation mechanism.

The startToWait() function asserts that the object is idle and not in a timeout loop. Then it sets the new state and delegates to wait() which does the real work.

The setWorking() function encapsulates the state. Any function that wants to set the working variable should call this function. It initializes the conditionChecks array to a value according to the top-level parameter allMustBeTrueAtSameTime. When this is true, it will always be undefined, else it will be set to a new array.

Wait

This is the final part that concentrates on setTimeout() calls.

      var wait = function(condition, success, givenUp, progress, alreadyDelayedMillis) {
        if (alreadyDelayedMillis === undefined)
          alreadyDelayedMillis = 0;
        
        if (progress)
          if ( ! progress(alreadyDelayedMillis, maximumWaitMillis) )
            return;
              
        if (maximumWaitMillis > 0 && alreadyDelayedMillis >= maximumWaitMillis) {
          if (givenUp)
            givenUp(maximumWaitMillis);
          setWorking(false);
        }
        else if (condition()) {
          success(alreadyDelayedMillis);
          setWorking(false);
        }
        else {
          setTimeout(
            function() {
              wait(condition, success, givenUp, progress, (maximumWaitMillis > 0) ? alreadyDelayedMillis + delayMillis : 0);
            },
            delayMillis
          );
        }
      };

Look at the alreadyDelayedMillis parameter. This will contain the already elapsed milliseconds. When you go back to the startToWait() implementation, you see that I call the wait() function with undefined as last parameter, which is exactly alreadyDelayedMillis. All further recursive calls of wait() will pass the parameter correctly. The parameter is checked at the start of the function, and initialized to zero when it is undefined. Thus it will count from zero to given maximum time. This again is to avoid an instance variable.

Everything else is "as expected".
First the progress() function is called when it was given. Then the maximum time is checked, and givenUp() is called when exceeded, and the working state is reset to idle. Else the condition() aggregate is called, and success() is executed when it returned true, inclusive resetting working state to idle. In any other case the predefined JS function setTimeout() is used to schedule another call to wait(), this time with increased alreadyDelayedMillis.

Test

You can find the full source at bottom of this page. Next is a manual test page that lets try out all waitFactory() parameters. Especially the allMustBeTrueAtSameTime parameter should be tested.

As soon as Start was pressed, a script regularly checks whether the elements "Booh" and "Waah" exist in the document. When yes, the text log below will be green, when not and time runs out, it will be red. You can create and delete the elements by pressing the according buttons.


Maximum wait: Check interval: ms

All conditions must be true at same time


Elapsed: .... of .... ms


Full Source

You can always go to my homepage to visit the current state of this project.

Click here to see source code.




Donnerstag, 11. Juni 2015

JS Swipe Gesture

Mobile devices like Smartphones bring new life into the world of events. There are user gestures that can not be performed with a computer mouse, for instance zooming in and out with two fingers. This is called "pinch zoom", and it is also available for the touchpad of laptops. (Generally the touchpad is closest to what is a mobile input device.)

On a desktop computer, the mousewheel could be used to zoom. But when you look at the inner life of event processing, you see that the dispatch of mouse wheel events is completely different from that of two-finger touch events. Finally both gestures magnify the screen contents by giving a flow of events, but generating that flow is subject to programming software that interprets the according device events.

In case of web applications running on mobiles we have (besides lots of Blogs like this :-) jQuery mobile that provides us with utilities:

  • tap (like mouse click)
  • taphold (long press, e.g. for selecting text)
  • scroll (moving the tapped finger to uncover hidden view areas)
  • swipe (moving the tapped finger fast to browse to next page)
  • ....

jQuery also provides an abstraction layer that allows to program against events that are dynamically adjusted to the target device. This is called

But here is the raw material for event processing:

the w3c specifies a standard for browser vendors how the JavaScript API for touch-devices should look like and act.

Let's make something out of it. This Blog is about a JS swipe detector that can be used for both mouse- and touch-devices, and event adapters for actually using it with both of them.

What's Swipe?

Swipe is a "Turn the Page" gesture, we want to see "next" or "previous" by quickly smearing over the mobile screen with one finger from left to right or right to left. In case "next" is below "current", it also could be an up- or down-movement.

On a desktop computer this would be a mouse-click into the scrollbar's empty area, to make it scroll a whole page up or down. Having a keyboard you would press Page-Up or Page-Down.

A swipe-event is quite similar to a scroll-event. Actually they differ just by velocity and some minimum distance. That means a swipe

  1. is a fast gesture, and it
  2. needs to span a minimum of pixels, else it will be a scroll

Enable a Gesture

Here is JS source code that installs a swipe gesture on the slide show I introduced in a past Blog.

      var installListeners = function() {

        ....

        var swipeCallback = function(swipeType) {
          if (swipeType === "left")
            forwardAction();
          else if (swipeType === "right")
            backwardAction();
        };
        swipeMouse(swipeCallback, controls.slideframe);
        swipeTouch(swipeCallback, controls.slideframe);
      }

At the same time this specifies what to achieve:

  • I want to have a function swipeMouse() for installing a mouse-swipe,
  • and a function swipeTouch() for installing a touch-swipe,
  • onto a given HTML DOM element, in this case controls.slideframe,
  • calling back to a given function swipeCallback when the gesture occurs,
  • and that function receives a String parameter swipeType telling the swipe direction.

Actually the swipe-callback then executes forwardAction() or backwardAction() to skip to next or previous slide.

It should be no problem to install a touch listener on a desktop browser, or a mouse listener on a mobile. You simply would not receive events that the device does not support.

Common Swipe Logic

The common swipe-logic that is used by both touch- and mouse-events is in a "stateful" JS object that holds following "state" (stateful means it has private variables that have different values at different times):

    /**
     * Install a swipe gesture detector upon given element,
     * calling back to given swipeCallback function.
     * @param swipeCallback function to call when gesture occurs.
     * @param element the element to watch for gesture.
     */
    var swipeDetector = function(swipeCallback, element) {     
      var startTime, startX, startY;
      var highSpeed = false;

      var that = {};

      /**
       * To be called by derivations with a concrete start-event.
       * @param startMoveX the x-coordinate of the start-event.
       * @param startMoveY the y-coordinate of the start-event.
       */
      that.setStartState = function(startMoveX, startMoveY) {
        ....
      };
      
      /**
       * To be called by derivations with a concrete move-event.
       * @param moveX the x-coordinate of the move-event.
       * @param moveY the y-coordinate of the move-event.
       * @param event to call preventDefault() upon when swipe speed is reached.
       */
      that.setMoveState = function(moveX, moveY, event) {
        ....
      };

      /**
       * To be called by derivations with a concrete end-event.
       * @param endMoveX the x-coordinate of the end-event.
       * @param endMoveY the y-coordinate of the end-event.
       */
      that.detectSwipe = function(endMoveX, endMoveY) {
        ....
      };

      ....

    }

This object remembers the start-time and -location in private variables when setStartState() is called. It assumes that the pending gesture will not be high-speed, but when it is, it will remember that and prevent the event default in that case, so that the view would not scroll on a mobile device.

Mind that the private vars (startTime, startX, ...) are available to the that object at any time, which means every time you call the swipeDetector() function, new instances of these variables will start to live, and will survive the termination of the function by being tied to the closure of the that object.

When having the start coordinates of a gesture, it is easy to calculate the distance traveled when the gesture ends. This is needed to establish a minimum distance for swipes.

When having the start coordinates and the start time of a gesture, we can calculate the speed of the gesture by

speed = distance / duration

As soon as the speed exceeds a certain threshold, thee highSpeed flag will be set and event default processing (scrolling) will be prevented. That way the view won't move on a mobile when you swipe.

If you do not want to implement this :-), here is the source of my swipe detector.

  (Click left arrow to see source code.)

Mind that this does nothing without some event adapters that call its public functions. All functions starting with "that." are such, visible to the outside world.

Event Adapters

Following facilitates the API events documented by the w3c to drive the swipe detector for mouse events on a desktop browser.

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

    var swipeMouse = function(swipeCallback, element) {
      var detector = swipeDetector(swipeCallback, element);
      element = detector.getSensor();
      
      element.addEventListener("mousedown", function(event) {
        detector.setStartState(event.pageX, event.pageY);
      });
      
      element.addEventListener("mousemove", function(event) {
        if (detector.isStarted())
          detector.setMoveState(event.pageX, event.pageY, event);
      });
      
      element.addEventListener("mouseup", function(event) {
        detector.detectSwipe(event.pageX, event.pageY);
      });
      element.addEventListener("mouseout", function(event) {
        detector.detectSwipe(event.pageX, event.pageY);
      });
    };

Reusage of the swipe-detector source code happens by delegation. The event adapter allocates an instance of swipeDetector and connects event listeners to it. The events that are fired by the browser generate swipe events then.

Following does the same for touch events on a mobile browser.

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

    var swipeTouch = function(swipeCallback, element) {
      var detector = swipeDetector(swipeCallback, element);
      element = detector.getSensor();
      
      var touchCallback = function(event, isStart, isEnd) {
        if (event.changedTouches.length === 1) {
          var touch = event.changedTouches[0];
          if (isStart)
            detector.setStartState(touch.pageX, touch.pageY);
          else if (isEnd)
            detector.detectSwipe(touch.pageX, touch.pageY);
          else
            detector.setMoveState(touch.pageX, touch.pageY, event);
        }
      };

      element.addEventListener("touchstart", function(event) {
        touchCallback(event, true, false);
      });
      
      element.addEventListener("touchmove", function(event) {
        touchCallback(event, false, false);
      });
      
      var touchEndCallback = function(event) {
        touchCallback(event, false, true);
      };
      element.addEventListener("touchend", touchEndCallback);
      element.addEventListener("touchcancel", touchEndCallback);
      
      return detector;
    };

Events are browser-specific, so don't blame me if this does not work on IE :-!


You can always go to my homepage to see the full source of this, and to try it out with different configuration parameters (that I dropped here for simplicity), with both your mobile phone and a desktop browser.