Source RapidContext_Widget_ProgressBar.js

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2023 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15// Namespace initialization
16if (typeof(RapidContext) == "undefined") {
17    RapidContext = {};
18}
19RapidContext.Widget = RapidContext.Widget || { Classes: {} };
20
21/**
22 * Creates a new progress bar widget.
23 *
24 * @constructor
25 * @param {Object} attrs the widget and node attributes
26 * @param {number} [attrs.min] the minimum range value, defaults to 0
27 * @param {number} [attrs.max] the maximum range value, defaults to 100
28 * @param {number} [attrs.value] the progress value, a number between `min`
29 *            and `max`, defaults to 0
30 * @param {number} [attrs.ratio] the progress ratio, a floating-point number
31 *            between 0.0 and 1.0, defaults to 0.0
32 * @param {number} [attrs.text] the additional information text, defaults to
33 *            blank
34 * @param {boolean} [attrs.noratio] the hide ratio (in percent) display flag,
35 *            defaults to `false`
36 * @param {boolean} [attrs.novalue] the hide value display flag, defaults to
37 *            `false`
38 * @param {boolean} [attrs.notime] the hide remaining time display flag,
39 *            defaults to `false`
40 * @param {boolean} [attrs.hidden] the hidden widget flag, defaults to false
41 *
42 * @return {Widget} the widget DOM node
43 *
44 * @class The progress bar widget class. Used to provide a dynamic progress
45 *     meter, using a `<div>` HTML elements. The progress bar also provides a
46 *     completion time estimation that is displayed in the bar. Whenever the
47 *     range is modified, the time estimation is reset.
48 * @extends RapidContext.Widget
49 *
50 * @example <caption>JavaScript</caption>
51 * let attrs = { text: "Working", noratio: true, notime: true };
52 * let w = RapidContext.Widget.ProgressBar(attrs);
53 *
54 * @example <caption>User Interface XML</caption>
55 * <ProgressBar text="Working" noratio="true" notime="true" />
56 */
57RapidContext.Widget.ProgressBar = function (attrs) {
58    let text = MochiKit.DOM.DIV({ "class": "widgetProgressBarText" });
59    let meter = document.createElement("progress");
60    let o = MochiKit.DOM.DIV({}, text, meter);
61    RapidContext.Widget._widgetMixin(o, RapidContext.Widget.ProgressBar);
62    o.addClass("widgetProgressBar");
63    o.setAttrs(Object.assign({ min: 0, max: 100, value: 0 }, attrs));
64    return o;
65};
66
67// Register widget class
68RapidContext.Widget.Classes.ProgressBar = RapidContext.Widget.ProgressBar;
69
70/**
71 * Returns the widget container DOM node.
72 *
73 * @return {Node} returns null, since child nodes are not supported
74 */
75RapidContext.Widget.ProgressBar.prototype._containerNode = function () {
76    return null;
77};
78
79/**
80 * Updates the widget or HTML DOM node attributes. Note that updating the
81 * value will automatically also update the ratio. All calls to this method
82 * may trigger a new remaining time estimation.
83 *
84 * @param {Object} attrs the widget and node attributes to set
85 * @param {number} [attrs.min] the minimum range value, if modified the current
86 *            value and ratio are reset
87 * @param {number} [attrs.max] the maximum range value, if modified the current
88 *            value and ratio are reset
89 * @param {number} [attrs.value] the progress value, a number between `min`
90 *            and `max`
91 * @param {number} [attrs.ratio] the progress ratio, a floating-point number
92 *            between 0.0 and 1.0
93 * @param {number} [attrs.text] the additional information text
94 * @param {boolean} [attrs.noratio] the hide ratio (in percent) display flag
95 * @param {boolean} [attrs.novalue] the hide value display flag
96 * @param {boolean} [attrs.notime] the hide remaining time display flag
97 * @param {boolean} [attrs.hidden] the hidden widget flag
98 */
99RapidContext.Widget.ProgressBar.prototype.setAttrs = function (attrs) {
100    /* eslint complexity: "off" */
101    let now = new Date().getTime();
102    attrs = Object.assign({}, attrs);
103    if ("min" in attrs || "max" in attrs) {
104        attrs.min = Math.max(parseInt(attrs.min, 10) || this.min || 0, 0);
105        attrs.max = Math.max(parseInt(attrs.max, 10) || this.max || 100, attrs.min);
106        attrs.value = attrs.value || null;
107        attrs.ratio = attrs.ratio || 0.0;
108        this.startTime = now;
109        this.updateTime = now;
110        this.timeRemaining = null;
111    }
112    if ("value" in attrs) {
113        let min = attrs.min || this.min;
114        let max = attrs.max || this.max;
115        let val = Math.min(Math.max(parseFloat(attrs.value), min), max);
116        attrs.value = isNaN(val) ? null : val;
117        attrs.ratio = isNaN(val) ? null : (val - min) / (max - min);
118    }
119    if ("ratio" in attrs) {
120        let val = Math.min(Math.max(parseFloat(attrs.ratio), 0.0), 1.0);
121        attrs.ratio = isNaN(val) ? null : val;
122    }
123    if ("noratio" in attrs) {
124        attrs.noratio = RapidContext.Data.bool(attrs.noratio) || null;
125    }
126    if ("novalue" in attrs) {
127        attrs.novalue = RapidContext.Data.bool(attrs.novalue) || null;
128    }
129    if ("notime" in attrs) {
130        attrs.notime = RapidContext.Data.bool(attrs.notime) || null;
131        this.timeRemaining = null;
132    }
133    this.__setAttrs(attrs);
134    if (!this.notime && now - this.updateTime > 1000) {
135        let estimate = this._remainingTime();
136        this.updateTime = now;
137        this.timeRemaining = (estimate && estimate.text) || null;
138    }
139    this._render();
140};
141
142/**
143 * Returns the remaining time.
144 *
145 * @return {Object} the remaining time object, or
146 *         `null` if not possible to estimate
147 */
148RapidContext.Widget.ProgressBar.prototype._remainingTime = function () {
149    let duration = new Date().getTime() - this.startTime;
150    duration = Math.max(Math.round(duration / this.ratio - duration), 0);
151    if (isFinite(duration) && !isNaN(duration)) {
152        let res = {
153            total: duration,
154            days: Math.floor(duration / 86400000),
155            hours: Math.floor(duration / 3600000) % 24,
156            minutes: Math.floor(duration / 60000) % 60,
157            seconds: Math.floor(duration / 1000) % 60,
158            millis: duration % 1000
159        };
160        let pad = MochiKit.Text.padLeft;
161        if (res.days >= 10) {
162            res.text = res.days + " days";
163        } else if (res.days >= 1) {
164            res.text = res.days + " days " + res.hours + " hours";
165        } else if (res.hours >= 1) {
166            res.text = res.hours + ":" + pad("" + res.minutes, 2, "0") + " hours";
167        } else if (res.minutes >= 1) {
168            res.text = res.minutes + ":" + pad("" + res.seconds, 2, "0") + " min";
169        } else {
170            res.text = res.seconds + " sec";
171        }
172        return res;
173    }
174    return null;
175};
176
177/**
178 * Redraws the progress bar meter and text.
179 */
180RapidContext.Widget.ProgressBar.prototype._render = function () {
181    this.lastChild.min = this.min;
182    this.lastChild.max = this.max;
183    this.lastChild.value = this.value || (this.max - this.min) * this.ratio || null;
184    let percent = 0;
185    let info = [];
186    if (!this.noratio) {
187        percent = Math.round(this.ratio * 1000) / 10;
188        info.push(Math.round(percent) + "%");
189    }
190    if (!this.novalue && typeof(this.value) == "number") {
191        let pos = this.value - this.min;
192        let total = this.max - this.min;
193        info.push(pos + " of " + total);
194    }
195    if (this.text) {
196        info.push(this.text);
197    }
198    if (this.timeRemaining && percent > 0 && percent < 100) {
199        info.push(this.timeRemaining + " remaining");
200    }
201    this.firstChild.innerText = info.join(" \u2022 ");
202};
203