diff options
author | Luke Shumaker <lukeshu@sbcglobal.net> | 2016-12-13 14:12:55 -0500 |
---|---|---|
committer | Luke Shumaker <lukeshu@sbcglobal.net> | 2016-12-13 21:41:38 -0500 |
commit | e7bacd45e1b44090781067159b63fe14b46b8611 (patch) | |
tree | d291a561cd58d4e42536b76d9ec91d26d72a87a5 /jarmon | |
parent | 85a578479406865502a3eb504577c1dfe64f34f5 (diff) |
Use HTML5 type=datetime-local inputs for ChartCoordinator.
Diffstat (limited to 'jarmon')
-rw-r--r-- | jarmon/external.doc.js | 6 | ||||
-rw-r--r-- | jarmon/jarmon.js | 301 |
2 files changed, 137 insertions, 170 deletions
diff --git a/jarmon/external.doc.js b/jarmon/external.doc.js index 119d1fb..10d9ea7 100644 --- a/jarmon/external.doc.js +++ b/jarmon/external.doc.js @@ -17,12 +17,6 @@ */ /** - * {@link http://jquerytools.github.io/ jQuery Tools} / dateinput - * @module jQueryTools/dateinput - * @see http://jquerytools.github.io/documentation/dateinput/index.html - */ - -/** * {@link http://jquerytools.github.io/ jQuery Tools} / tabs * @module jQueryTools/tabs * @see http://jquerytools.github.io/documentation/tabs/index.html diff --git a/jarmon/jarmon.js b/jarmon/jarmon.js index cef338b..f3f6e33 100644 --- a/jarmon/jarmon.js +++ b/jarmon/jarmon.js @@ -13,7 +13,6 @@ * - javascriptRRD/binaryXHR 0.6/1.1: http://javascriptrrd.sourceforge.net/docs/javascriptrrd_v1.1.1/doc/lib/binaryXHR_js.html * - jQuery 1.6.3: http://jquery.com/ * - Flot 1.7: http://www.flotcharts.org/ - * - jQueryTools/dateinput: http://jquerytools.github.io/documentation/dateinput/index.html * - jQueryTools/tabs: http://jquerytools.github.io/documentation/tabs/index.html * - jQueryTools/toolbox/history: http://jquerytools.github.io/documentation/toolbox/history.html */ @@ -1306,9 +1305,35 @@ jarmon.timeRangeShortcuts = [ * Presents the user with a form and a timeline with which they can choose a * time range and co-ordinates the refreshing of a series of charts. * + * The `ui` parameter should be a jQuery for a DOM node containing elements + * matching these selectors: + * + * - `select[name="shortcuts"]` : An empty `<select>` to be populated with + * {@linkcode jarmon.timeRangeShortcuts} + * - `[name="from"]` ; An input to select the start datetime + * - `[name="to"]` : An input to select the end datetime + * - `[name="tzoffset"]` : An input to select the timezone + * - `[name="action"]` : A button to update the charts + * - `.range-preview` : Placeholder for a Flot chart + * + * The `[name="to"]` and `[name="from"]` inputs should behave somewhat like + * WHATWG HTML (revision 2016-12-12) `type=datetime-local` inputs with `step=1`. + * Because `type=datetime-local` is not currently in a W3C spec, and not all + * common browsers implement it, and polyfills vary in quality; Jarmon avoids + * interacting with it too deeply, placing only a few requirements placed on the + * implementation: + * + * - The value MUST be settable via `el.value = string` where `string` is of the + * format `%Y-%m-%dT%H:%M:%S`. + * - The value MUST be gettable via `Date.parse(el.value+'Z')`. + * - The user interface SHOULD prohibit the user from selecting a value that is + * before than the `min` attribute, which is in the format + * `%Y-%m-%dT%H:%M:%S`. + * - The user interface SHOULD prohibit the user from selecting a value that is + * after than the `max` attribute, which is in the format `%Y-%m-%dT%H:%M:%S`. + * * @constructor * @requires jQuery - * @requires jQueryTools/dateinput * @requires Flot * * @param ui {module:jQuery.jQuery} A one element jQuery containing an input form and @@ -1336,97 +1361,98 @@ jarmon.ChartCoordinator = function(ui, charts) { } }; - var options = this.ui.find('select[name="from_standard"]'); - var default_index = 0; // Select the first shortcut by default - for(var i=0; i<jarmon.timeRangeShortcuts.length; i++) { - options.append($('<option />').text(jarmon.timeRangeShortcuts[i][0])); - if (jarmon.timeRangeShortcuts[i][2] === true) { - default_index = i; + this.inputs = { + shortcuts: this.ui.find('select[name="shortcuts"]'), + from: this.ui.find('[name="from"]'), + to: this.ui.find('[name="to"]'), + tzoffset: this.ui.find('[name="tzoffset"]'), + action: this.ui.find('[name="action"]') + }; + + // DOM manipulation //////////////////////////////////////////////////////// + + // (This is put in a closure to avoid leaking variables and making it hard + // for me to reason about the code.) + (function(options, tzoffsetEl) { + // Set up shortcuts + var option; + for(var i=0; i<jarmon.timeRangeShortcuts.length; i++) { + option = $('<option />').text(jarmon.timeRangeShortcuts[i][0]) + if (jarmon.timeRangeShortcuts[i][2] === true) { + option[0].setAttribute('selected', 'selected'); + } + options.append(option); } - } + // Append a custom option for when the user selects an area of the graph + options.append($('<option />').text('custom')); + + // Populate a list of tzoffset options if the element is present in the + // template as a select list + if (tzoffsetEl.is('select')) { + var label, val; + for(i=-12; i<=12; i++) { + label = 'UTC'; + val = i; + if(val >= 0) { + label += ' + '; + } else { + label += ' - '; + } + val = Math.abs(val).toString(); + if(val.length === 1) { + label += '0'; + } + label += val + '00'; + tzoffsetEl.append( + $('<option />').attr('value', i*60*60*1000).text(label)); + } + } + + // Default timezone offset based on localtime + var tzoffset = -1 * (new Date()).getTimezoneOffset() * 60 * 1000; + tzoffsetEl.val(tzoffset); + })(this.inputs.shortcuts, this.inputs.tzoffset); - // Append a custom option for when the user selects an area of the graph - options.append($('<option />').text('custom')); - // Select whichever timeRangeShortcut is the default - options.val(jarmon.timeRangeShortcuts[default_index][0]); + // Event bindings ////////////////////////////////////////////////////////// - options.bind('change', function(e) { + this.inputs.shortcuts.bind('change', function(e) { // No point in updating if the user chose custom. if($(this).val() !== 'custom') { self.update(); } }); - // Update the time ranges and redraw charts when the custom datetime inputs - // are changed - this.ui.find('[name="from_custom"]').bind('change', + this.inputs.from.bind('change', function(e) { - self.ui.find('[name="from_standard"]').val('custom'); - var tzoffset = parseInt(self.ui.find('[name="tzoffset"]').val(), 10); - self.setTimeRange( - new Date(this.value + ' UTC').getTime() - tzoffset, null); + self.inputs.shortcuts.val('custom'); self.update(); } ); - this.ui.find('[name="to_custom"]').bind('change', + this.inputs.to.bind('change', function(e) { - self.ui.find('[name="from_standard"]').val('custom'); - var tzoffset = parseInt(self.ui.find('[name="tzoffset"]').val(), 10); - self.setTimeRange( - null, new Date(this.value + ' UTC').getTime() - tzoffset); + self.inputs.shortcuts.val('custom'); self.update(); } ); - // Populate a list of tzoffset options if the element is present in the - // template as a select list - var tzoffsetEl = this.ui.find('[name="tzoffset"]'); - if(tzoffsetEl.is('select')) { - var label, val; - for(i=-12; i<=12; i++) { - label = 'UTC'; - val = i; - if(val >= 0) { - label += ' + '; - } else { - label += ' - '; - } - val = Math.abs(val).toString(); - if(val.length === 1) { - label += '0'; - } - label += val + '00'; - tzoffsetEl.append( - $('<option />').attr('value', i*60*60*1000).text(label)); - } - - tzoffsetEl.bind('change', function(e) { - self.update(); - }); - } - - // Default timezone offset based on localtime - var tzoffset = -1 * new Date().getTimezoneOffset() * 60 * 1000; - tzoffsetEl.val(tzoffset); + this.inputs.tzoffset.bind('change', function(e) { + self.update(); + }); - // Update the time ranges and redraw charts when the form is submitted - this.ui.find('[name="action"]').bind('click', function(e) { + this.inputs.action.bind('click', function(e) { self.update(); return false; }); // When a selection is made on the range timeline, or any of my charts // redraw all the charts. - $(document).bind( - 'plotselected', - {self: this}, + $(document).bind('plotselected', {}, function(e, ranges) { - var self = e.data.self; var eventSourceIsMine = false; // plotselected event may be from my range selector chart or - if( self.ui.has(e.target) ) { + if (self.ui.has(e.target) ) { eventSourceIsMine = true; } else { // ...it may come from one of the charts under my supervision @@ -1439,97 +1465,12 @@ jarmon.ChartCoordinator = function(ui, charts) { } if(eventSourceIsMine) { - // Update the prepared time range select box to value "custom" - self.ui.find('[name="from_standard"]').val('custom'); - - // Update all my charts self.setTimeRange(ranges.xaxis.from, ranges.xaxis.to); - self.update(); } } ); - - // Add dhtml calendars to the date input fields - this.ui.find(".timerange_control img") - .dateinput({ - 'format': 'dd mmm yyyy 00:00:00', - 'max': +1, - 'css': {'input': 'jquerytools_date'}}) - .bind('onBeforeShow', function(e) { - var classes = $(this).attr('class').split(' '); - var currentDate, input_selector; - for(var i=0; i<=classes.length; i++) { - input_selector = '[name="' + classes[i] + '"]'; - // Look for a neighboring input element whose name matches the - // class name of this calendar - // Parse the value as a date if the returned date.getTime - // returns NaN we know it's an invalid date - // XXX: is there a better way to check for valid date? - currentDate = new Date($(this).siblings(input_selector).val()); - if(! isNaN(currentDate.getTime()) ) { - $(this).data( - 'dateinput')._initial_val = currentDate.getTime(); - $(this).data('dateinput').setValue(currentDate); - break; - } - } - }) - .bind( - 'onHide', - {self: this}, - function(e) { - var self = e.data.self; - // Called after a calendar date has been chosen by the user. - // Use the sibling selector that we generated above - // before opening the calendar - var oldStamp = $(this).data('dateinput')._initial_val; - var newDate = $(this).data('dateinput').getValue(); - // Only update the form field if the date has changed. - if(oldStamp !== newDate.getTime()) { - // Update the prepared time range select box to - // value "custom" - self.ui.find('[name="from_standard"]').val('custom'); - var from = null; - var to = null; - if($(this).hasClass('from_custom')){ - from = newDate.getTime(); - } else { - to = newDate.getTime(); - } - self.setTimeRange(from, to); - self.update(); - } - }); - - // Avoid overlaps between the calendars - // XXX: This is a bit of hack, what if there's more than one set of calendar - // controls on a page? - this.ui.find(".timerange_control img.from_custom").bind( - 'onBeforeShow', - {self: this}, - function(e) { - var self = e.data.self; - var otherVal = new Date( - self.ui.find('.timerange_control [name="to_custom"]').val()); - - $(this).data('dateinput').setMax(otherVal); - } - ); - this.ui.find(".timerange_control img.to_custom").bind( - 'onBeforeShow', - {self: this}, - function(e) { - var self = e.data.self; - var otherVal = new Date( - self.ui.find('.timerange_control [name="from_custom"]').val()); - - $(this).data('dateinput').setMin(otherVal); - } - ); - }; - /** * Grab the start and end time from the ui form, highlight the range on the * range timeline and set the time range of all the charts and redraw. @@ -1539,27 +1480,25 @@ jarmon.ChartCoordinator = function(ui, charts) { **/ jarmon.ChartCoordinator.prototype.update = function() { var self = this; - var selection = this.ui.find('[name="from_standard"]').val(); + var shortcut = this.inputs.shortcuts.val(); - var now = new Date().getTime(); + var now = new Date(); for(var i=0; i<jarmon.timeRangeShortcuts.length; i++) { - if(jarmon.timeRangeShortcuts[i][0] === selection) { - var range = jarmon.timeRangeShortcuts[i][1](now); - this.setTimeRange(range[0], range[1]); + if(jarmon.timeRangeShortcuts[i][0] === shortcut) { + var range = jarmon.timeRangeShortcuts[i][1](now.getTime()); + this.inputs.from.val(this.unix2datetimelocal(range[0])) + this.inputs.to.val(this.unix2datetimelocal(range[1])); break; } } - var startTime = parseInt(this.ui.find('[name="from"]').val(), 10); - var endTime = parseInt(this.ui.find('[name="to"]').val(), 10); - var tzoffset = parseInt(this.ui.find('[name="tzoffset"]').val(), 10); + var startTime = this.datetimelocal2unix(this.inputs.from.val()); + var endTime = this.datetimelocal2unix(this.inputs.to.val()); + var tzoffset = parseInt(this.inputs.tzoffset.val(), 10); - this.ui.find('[name="from_custom"]').val( - new Date(startTime + tzoffset) - .toUTCString().split(' ').slice(1,5).join(' ')); - this.ui.find('[name="to_custom"]').val( - new Date(endTime + tzoffset) - .toUTCString().split(' ').slice(1,5).join(' ')); + this.inputs.from[0].setAttribute('max', this.inputs.to.val()); + this.inputs.to[0].setAttribute( 'min', this.inputs.from.val()); + this.inputs.to[0].setAttribute( 'max', now.toISOString().slice(0, -1)); this.rangePreviewOptions.xaxis.tzoffset = tzoffset; @@ -1629,6 +1568,38 @@ jarmon.ChartCoordinator.prototype.update = function() { }; /** + * Convert a UNIX millisecond timestamp into a [datetime-local string][]; using + * the `[name="tzoffoffset"]` input for the timezone. + * + * [datetime-local string]: https://html.spec.whatwg.org/multipage/infrastructure.html#concept-datetime-local + * + * @method + * @private + * @param unix {number} + * @return {string} + */ +jarmon.ChartCoordinator.prototype.unix2datetimelocal = function(unix) { + var tz = parseInt(this.inputs.tzoffset.val(), 10); + return (new Date(unix + tz)).toISOString().slice(0, -1); +} + +/** + * Convert a [datetime-local string][] into a UNIX millisecond timestamp; using + * the `[name="tzoffoffset"]` input for the timezone. + * + * [datetime-local string]: https://html.spec.whatwg.org/multipage/infrastructure.html#concept-datetime-local + * + * @method + * @private + * @param datetimelocal {string} + * @return {number} + */ +jarmon.ChartCoordinator.prototype.datetimelocal2unix = function(datetimelocal) { + var tz = parseInt(this.inputs.tzoffset.val(), 10); + return Date.parse(datetimelocal+'Z') - tz; +}; + +/** * Set the start and end time fields in the form and trigger an update * * @method @@ -1637,11 +1608,13 @@ jarmon.ChartCoordinator.prototype.update = function() { **/ jarmon.ChartCoordinator.prototype.setTimeRange = function(from, to) { if(typeof(from) !== 'undefined' && from !== null) { - this.ui.find('[name="from"]').val(from); + this.inputs.from.val(this.unix2datetimelocal(from)) } if(typeof(to) !== 'undefined' && to !== null) { - this.ui.find('[name="to"]').val(to); + this.inputs.to.val(this.unix2datetimelocal(to)); } + this.inputs.shortcuts.val('custom'); + this.update(); }; /** |