pcjs/my_modules/ecpjs-client/lib/stepper.js

207 lines
8 KiB
JavaScript

/*
* stepper.js
* by Jeff Parsons, June 24, 2012
*
* Stepper methods extracted from the original Component class.
*/
/*
* The Stepper class defines a set of stepping functions used to help drive multi-step
* operations that a component may want to "single-step." The setStep() function determines
* the delay, if any, between the steps. If there is no delay, then all the supplied
* "step" functions are called directly. Otherwise, any delay (even a minimal delay of 0ms)
* results in the creation and queuing of step objects. Every step object contains:
*
* fn: step function pointer
* n: step number
*
* and is stored in a simple array (aStep) used to enqueue/dequeue the step objects.
*
* firstStep() gets the ball rolling by queuing the first "step" function. That initial
* "step" function, as well as any or all subsequent "step" functions, call addStep()
* to add more steps to the queue as needed. As each "step" function successfully finishes
* (by returning true), the next "step" function is dequeued and called. As soon as a
* "step" function returns false, the dequeuing process stops, and any remaining steps
* in the queue are ignored. If the queue empties before that happens, then we essentially
* repeat firstStep(), and the process continues.
*
* If a negative millisecond delay (-1) has been set via setStep(), then both firstStep()
* and addStep() call their respective "step" functions directly, instead of queuing them.
* That option should be used only for very brief steps, otherwise the browser will appear
* to hang if the stepping functions never yield.
*
* Also, when "step" functions are called directly, the step number (n) is omitted from the
* calls, so that the "step" function can quickly determine whether to bypass its internal print
* operations. printStep() will automatically bypass, since it has access to the step number,
* but if you want to maximize speed, then either always check for an undefined step number, or
* use separate "step" functions that omit both the step number and the print operations altogether.
*
* NOTE: the stepping functions use printStatus() indirectly, via printStep(). However,
* there may be times when a component wants to reserve its "status" control for more pertinent
* messages. In those cases, the component can pass a "quiet" setting to setStep().
*/
function Stepper() {
}
Stepper.prototype = {
/*
* initStep(parms) accepts any or all of the following parameter (parms) properties:
*
* step: millisecond step setting (0 for minimum delay when single-stepping, -1 for direct calls instead of steps)
*/
initStep: function(parms) {
this.msStep = parms.step;
this.quiet = (this.msStep == 0);
},
/*
* setStep(ms, quiet)
*
* ms can be any of:
*
* 1) non-negative number of milliseconds
* 2) -1 to call all step functions directly without delay
* 3) an HTML element (eg, a button) that will control stepping via its "onclick" handler
*
* Note that even 0 is a supported millisecond delay, albeit a minimal one, insuring that scripts
* don't run too long without yielding.
*
* If quiet, then printStep() messages will not be passed to printStatus(); however, if logging is
* enabled (refer to this.fLog), printStep() messages will still be logged.
*/
setStep: function(ms, quiet) {
this.msStep = ms;
this.quiet = quiet;
},
stopSteps: function() {
if (this.timerSteps !== undefined)
clearTimeout(this.timerSteps);
if (this.timerNotify !== undefined)
clearTimeout(this.timerNotify);
this.aSteps = [];
this.cSteps = 0;
this.timerSteps = undefined;
this.fnNotify = undefined;
this.timerNotify = undefined;
if (!this.quiet) this.printStatus(); // "clear" the status field, if any, as well
},
firstStep: function(fn, fnNotify) {
this.fnNotify = fnNotify;
this.timerNotify = undefined;
if (this.msStep == -1) {
while (fn.call(this))
;
if (this.fnNotify) this.timerNotify = setTimeout(this.fnNotify, 0);
return;
}
this.kickStep(fn);
this.nextStep(0);
},
kickStep: function(fn) {
this.addStep(function(n) {
// this.log(this.toString() + ": Step " + n + ": automatic kickStep");
if (!fn.call(this, n))
return false;
return this.kickStep(fn);
});
return true;
},
addStep: function(fn) {
if (this.msStep == -1) {
return fn.call(this);
}
var step = {fn:fn, n:++this.cSteps};
this.aSteps.push(step);
return true;
},
removeStep: function($this) {
$this.timerSteps = undefined;
if (typeof $this.msStep == "object")
$this.msStep.onclick = null;
var step = $this.aSteps.shift();
if (step === undefined)
return false;
return $this.doStep(step);
},
doStep: function(step) {
if (!step.fn.call(this, step.n)) {
if (this.fnNotify) this.timerNotify = setTimeout(this.fnNotify, 0);
return false;
}
return this.nextStep(this.msStep);
},
nextStep: function(ms) {
if (this.aSteps.length == 0) {
console.warn("Component.nextStep(): unexpected end of steps");
return false;
}
if (typeof ms == "number") {
/*
* IE doesn't support:
*
* setTimeout(this.removeStep, ms, this);
*
* so we're forced to do this instead:
*
* var thisParm = this;
* setTimeout(function() {thisParm.removeStep(thisParm);}, ms);
*/
var thisParm = this;
this.timerSteps = setTimeout(function() {thisParm.removeStep(thisParm);}, ms);
}
else
if (typeof ms == "object") {
ms.onclick = function(regParm) {
/*
* If we defined the onclick handler below as "function(e)" instead of simply "function()", then we could
* also receive an event object (e); however, IE reportedly requires that we examine a global (window.event)
* instead. If that's true, and if we ever care to get more details about the click event, then we might
* have to worry about that (eg, define a local var: "var event = window.event || e").
*/
return function() {
return regParm.removeStep(regParm);
};
}(this);
}
else
alert("unexpected step parameter (" + ms + ")");
return true;
},
printStep: function(n, s) {
if (n !== undefined) {
if (!this.quiet)
this.printStatus("Step " + n + ": " + s);
else if (this.fLog)
this.log(this.toString() + ": Step " + n + ": " + s);
}
},
/*
* printStatus(s)
*
* Passes any string (s) to the associated HTML element of class "status", if any;
* pass a blank string, or nothing at all, to clear the contents of the associated
* "status" element.
*
* If there's no "status" element associated with this component, then status messages
* are simply thrown away. However, if logging is enabled (ie, parms.log was true),
* all status messages are still logged.
*/
printStatus: function(s) {
if (this.updateStatus !== undefined) {
this.updateStatus(s);
}
if (this.fLog && s) {
this.log(this.toString() + ": " + s);
}
},
/*
* setStatusUpdate(s)
*
* Sets the "updateStatus" handler that printStatus() uses to display any status messages.
*/
setStatusUpdate: function(updateStatus) {
this.updateStatus = updateStatus;
}
};
Component.extend(Component.prototype, Stepper.prototype);