diff --git a/js/.eslintrc.json b/js/.eslintrc.json index 0a19574..0e3cc4f 100644 --- a/js/.eslintrc.json +++ b/js/.eslintrc.json @@ -65,6 +65,7 @@ "vars-on-top": "off", "operator-assignment": "off", "no-else-return": "off", + "prefer-object-spread": "off", // Node.js and CommonJS "callback-return": "off", diff --git a/js/beestat/date.js b/js/beestat/date.js new file mode 100644 index 0000000..ca0d8a0 --- /dev/null +++ b/js/beestat/date.js @@ -0,0 +1,26 @@ +/** + * Format a date. + * + * @param {object} args Instructions on how to format: + * date (required) - Temperature to work with + * + * @return {string} The formatted date. + */ +beestat.date = function(args) { + // Allow passing a single argument of date for convenience. + if (typeof args !== 'object' || args === null) { + args = { + 'date': args + }; + } + + const m = moment(args.date); + if ( + args.date !== undefined && + m.isValid() === true + ) { + return m.format(beestat.setting('date_format')); + } else { + return ''; + } +}; diff --git a/js/beestat/setting.js b/js/beestat/setting.js index 01fa6ff..447b7b0 100644 --- a/js/beestat/setting.js +++ b/js/beestat/setting.js @@ -81,7 +81,9 @@ beestat.setting = function(argument_1, opt_value, opt_callback) { 'visualize.heat_map_absolute.occupancy.max': 100, 'visualize.hide_affiliate': false, 'visualize.three_d.show_labels': false, - 'visualize.three_d.auto_rotate': false + 'visualize.three_d.auto_rotate': false, + + 'date_format': 'M/D/YYYY' }; // Figure out what we're trying to do. diff --git a/js/component.js b/js/component.js index 5e6bc8f..cebad9f 100644 --- a/js/component.js +++ b/js/component.js @@ -30,8 +30,13 @@ beestat.component.prototype.render = function(parent) { var self = this; if (parent !== undefined) { - this.component_container_ = $.createElement('div') - .style('position', 'relative'); + this.component_container_ = $.createElement('div'); + Object.assign(this.component_container_[0].style, Object.assign( + { + 'position': 'relative' + }, + this.style_ + )); this.decorate_(this.component_container_); parent.appendChild(this.component_container_); } else { @@ -93,3 +98,16 @@ beestat.component.prototype.dispose = function() { beestat.component.prototype.decorate_ = function() { // Left for the subclass to implement. }; + +/** + * Add custom styling to a component container. Mostly useful for when a + * component needs margins, etc applied depending on the context. + * + * @param {object} style + * + * @return {beestat.component} This + */ +beestat.component.prototype.style = function(style) { + this.style_ = style; + return this.rerender(); +}; diff --git a/js/component/card/three_d.js b/js/component/card/three_d.js index a920ec0..1452970 100644 --- a/js/component/card/three_d.js +++ b/js/component/card/three_d.js @@ -190,10 +190,10 @@ beestat.component.card.three_d.prototype.decorate_contents_ = function(parent) { } } else { required_begin = moment( - beestat.setting('visualize.range_static_begin') + ' 00:00:00' + beestat.setting('visualize.range_static.begin') + ' 00:00:00' ); required_end = moment( - beestat.setting('visualize.range_static_end') + ' 23:59:59' + beestat.setting('visualize.range_static.end') + ' 23:59:59' ); } @@ -379,7 +379,7 @@ beestat.component.card.three_d.prototype.decorate_drawing_pane_ = function(paren } } else { this.date_m_ = moment( - beestat.setting('visualize.range_static_begin') + ' 00:00:00' + beestat.setting('visualize.range_static.begin') + ' 00:00:00' ); } diff --git a/js/component/card/visualize_settings.js b/js/component/card/visualize_settings.js index f50c4e1..1f75a96 100644 --- a/js/component/card/visualize_settings.js +++ b/js/component/card/visualize_settings.js @@ -21,34 +21,34 @@ beestat.extend(beestat.component.card.visualize_settings, beestat.component.card * @param {rocket.Elements} parent */ beestat.component.card.visualize_settings.prototype.decorate_contents_ = function(parent) { - const grid_1 = document.createElement('div'); - Object.assign(grid_1.style, { + const grid = document.createElement('div'); + Object.assign(grid.style, { 'display': 'grid', 'grid-template-columns': 'repeat(auto-fit, minmax(min(350px, 100%), 1fr))', 'grid-gap': `${beestat.style.size.gutter}px`, 'margin-bottom': `${beestat.style.size.gutter}px` }); - parent.appendChild(grid_1); + parent.appendChild(grid); - const type_container = document.createElement('div'); - this.decorate_data_type_(type_container); - grid_1.appendChild(type_container); + const left_container = document.createElement('div'); + grid.appendChild(left_container); + const right_container = document.createElement('div'); + grid.appendChild(right_container); - const time_period_container = document.createElement('div'); - this.decorate_time_period_(time_period_container); - grid_1.appendChild(time_period_container); - - const grid_2 = document.createElement('div'); - Object.assign(grid_2.style, { - 'display': 'grid', - 'grid-template-columns': 'repeat(auto-fit, minmax(min(350px, 100%), 1fr))', - 'grid-gap': `${beestat.style.size.gutter}px` + const data_type_container = document.createElement('div'); + Object.assign(data_type_container.style, { + 'margin-bottom': `${beestat.style.size.gutter}px` }); - parent.appendChild(grid_2); + this.decorate_data_type_(data_type_container); + left_container.appendChild(data_type_container); const heat_map_values_container = document.createElement('div'); this.decorate_heat_map_values_(heat_map_values_container); - grid_2.appendChild(heat_map_values_container); + left_container.appendChild(heat_map_values_container); + + const time_period_container = document.createElement('div'); + this.decorate_time_period_(time_period_container); + right_container.appendChild(time_period_container); // If at least one sensor is on the floor plan and the data is loading. if ( @@ -121,18 +121,27 @@ beestat.component.card.visualize_settings.prototype.decorate_heat_map_values_ = const types = [ { 'code': 'relative', - 'name': 'Relative', + 'name': 'Dynamic', 'icon': 'arrow_expand_horizontal' }, { 'code': 'absolute', - 'name': 'Absolute', + 'name': 'Static', 'icon': 'arrow_horizontal_lock' } ]; + const container = document.createElement('div'); + Object.assign(container.style, { + 'display': 'flex', + 'flex-wrap': 'wrap', + 'grid-gap': `${beestat.style.size.gutter}px` + }); + parent.appendChild(container); + const color = beestat.style.color.orange.base; const tile_group = new beestat.component.tile_group(); + types.forEach(function(type) { const tile = new beestat.component.tile() .set_background_hover_color(color) @@ -152,12 +161,11 @@ beestat.component.card.visualize_settings.prototype.decorate_heat_map_values_ = } tile_group.add_tile(tile); }); - tile_group.render($(parent)); + tile_group.render($(container)); if (beestat.setting('visualize.heat_map_values') === 'absolute') { const min_max_container = document.createElement('div'); - min_max_container.style.marginTop = `${beestat.style.size.gutter}px`; - parent.appendChild(min_max_container); + container.appendChild(min_max_container); let type; let inputmode; @@ -249,7 +257,7 @@ beestat.component.card.visualize_settings.prototype.decorate_heat_map_values_ = span = document.createElement('span'); span.style.display = 'inline-block'; min.render($(span)); - parent.appendChild(span); + min_max_container.appendChild(span); span = document.createElement('span'); span.innerText = 'to'; @@ -258,12 +266,12 @@ beestat.component.card.visualize_settings.prototype.decorate_heat_map_values_ = 'margin-left': `${beestat.style.size.gutter}px`, 'margin-right': `${beestat.style.size.gutter}px` }); - parent.appendChild(span); + min_max_container.appendChild(span); span = document.createElement('span'); span.style.display = 'inline-block'; max.render($(span)); - parent.appendChild(span); + min_max_container.appendChild(span); span = document.createElement('span'); switch (beestat.setting('visualize.data_type')) { @@ -279,7 +287,7 @@ beestat.component.card.visualize_settings.prototype.decorate_heat_map_values_ = 'display': 'inline-block', 'margin-left': `${beestat.style.size.gutter}px` }); - parent.appendChild(span); + min_max_container.appendChild(span); } }; @@ -295,7 +303,7 @@ beestat.component.card.visualize_settings.prototype.decorate_time_period_ = func const color = beestat.style.color.purple.base; - const tile_group = new beestat.component.tile_group(); + const tile_group_dynamic = new beestat.component.tile_group(); // Current Day const current_day_tile = new beestat.component.tile() @@ -313,13 +321,15 @@ beestat.component.card.visualize_settings.prototype.decorate_time_period_ = func current_day_tile .set_background_color(beestat.style.color.bluegray.light) .addEventListener('click', function() { - beestat.setting('visualize.range_type', 'dynamic'); - beestat.setting('visualize.range_dynamic', 0); + beestat.setting({ + 'visualize.range_type': 'dynamic', + 'visualize.range_dynamic': 0 + }); beestat.cache.delete('data.three_d__runtime_sensor'); self.rerender(); }); } - tile_group.add_tile(current_day_tile); + tile_group_dynamic.add_tile(current_day_tile); // Yesterday const yesterday_tile = new beestat.component.tile() @@ -337,13 +347,15 @@ beestat.component.card.visualize_settings.prototype.decorate_time_period_ = func yesterday_tile .set_background_color(beestat.style.color.bluegray.light) .addEventListener('click', function() { - beestat.setting('visualize.range_type', 'dynamic'); - beestat.setting('visualize.range_dynamic', 1); + beestat.setting({ + 'visualize.range_type': 'dynamic', + 'visualize.range_dynamic': 1 + }); beestat.cache.delete('data.three_d__runtime_sensor'); self.rerender(); }); } - tile_group.add_tile(yesterday_tile); + tile_group_dynamic.add_tile(yesterday_tile); // Current Week const week_tile = new beestat.component.tile() @@ -361,37 +373,65 @@ beestat.component.card.visualize_settings.prototype.decorate_time_period_ = func week_tile .set_background_color(beestat.style.color.bluegray.light) .addEventListener('click', function() { - beestat.setting('visualize.range_type', 'dynamic'); - beestat.setting('visualize.range_dynamic', 7); + beestat.setting({ + 'visualize.range_type': 'dynamic', + 'visualize.range_dynamic': 7 + }); beestat.cache.delete('data.three_d__runtime_sensor'); self.rerender(); }); } - tile_group.add_tile(week_tile); + tile_group_dynamic.add_tile(week_tile); // Custom -/* const custom_tile = new beestat.component.tile() + const tile_group_static = new beestat.component.tile_group(); + const custom_tile = new beestat.component.tile() .set_background_hover_color(color) .set_text_color('#fff') .set_icon('calendar_edit') .set_text('Custom'); - if ( - beestat.setting('visualize.range_type') === 'static' - ) { + custom_tile + .addEventListener('click', function() { + new beestat.component.modal.visualize_custom().render(); + }); + + if (beestat.setting('visualize.range_type') === 'static') { custom_tile.set_background_color(color); } else { - custom_tile - .set_background_color(beestat.style.color.bluegray.light) - .addEventListener('click', function() { - // TODO MODAL - beestat.setting('visualize.range_type', 'static'); - self.rerender(); - }); + custom_tile.set_background_color(beestat.style.color.bluegray.light); } - tile_group.add_tile(custom_tile);*/ + tile_group_static.add_tile(custom_tile); - tile_group.render($(parent)); + // Static range + if (beestat.setting('visualize.range_type') === 'static') { + const static_range_tile = new beestat.component.tile() + .set_shadow(false) + .set_text( + beestat.date(beestat.setting('visualize.range_static.begin')) + + ' to ' + + beestat.date(beestat.setting('visualize.range_static.end')) + ); + tile_group_static.add_tile(static_range_tile); + + const static_range_edit_tile = new beestat.component.tile() + .set_background_color(beestat.style.color.bluegray.light) + .set_background_hover_color(color) + .set_text_color('#fff') + .set_icon('pencil'); + static_range_edit_tile + .addEventListener('click', function() { + new beestat.component.modal.visualize_custom().render(); + }); + tile_group_static.add_tile(static_range_edit_tile); + } + + tile_group_dynamic + .style({ + 'margin-bottom': `${beestat.style.size.gutter}px` + }) + .render($(parent)); + tile_group_static.render($(parent)); }; /** diff --git a/js/component/input/text.js b/js/component/input/text.js index 04c7220..54b6de4 100644 --- a/js/component/input/text.js +++ b/js/component/input/text.js @@ -32,6 +32,11 @@ beestat.component.input.text = function() { ) / 10 ** self.transform_.decimals; } break; + case 'date': + self.input_.value = moment(self.input_.value).format( + beestat.setting('date_format') + ) + break; } } @@ -59,9 +64,7 @@ beestat.component.input.text.prototype.decorate_ = function(parent) { 'padding': `${beestat.style.size.gutter / 2}px`, 'color': '#ffffff', 'outline': 'none', - 'transition': 'background 200ms ease', - 'margin-bottom': `${beestat.style.size.gutter}px`, - 'border-bottom': `2px solid ${beestat.style.color.lightblue.base}` + 'transition': 'background 200ms ease' }); // Set input width; interpret string widths literally (ex: 100%) diff --git a/js/component/modal/create_floor_plan.js b/js/component/modal/create_floor_plan.js index 2d152b6..94f3fba 100644 --- a/js/component/modal/create_floor_plan.js +++ b/js/component/modal/create_floor_plan.js @@ -287,7 +287,7 @@ beestat.component.modal.create_floor_plan.prototype.get_buttons_ = function() { beestat.component.modal.create_floor_plan.prototype.decorate_error_ = function(parent) { let has_error = false; - var div = $.createElement('div').style({ + const div = $.createElement('div').style({ 'background': beestat.style.color.red.base, 'color': '#fff', 'border-radius': beestat.style.size.border_radius, diff --git a/js/component/modal/runtime_sensor_detail_custom.js b/js/component/modal/runtime_sensor_detail_custom.js index 9b4f269..3b1760a 100644 --- a/js/component/modal/runtime_sensor_detail_custom.js +++ b/js/component/modal/runtime_sensor_detail_custom.js @@ -22,7 +22,7 @@ beestat.extend(beestat.component.modal.runtime_sensor_detail_custom, beestat.com * @param {rocket.Elements} parent */ beestat.component.modal.runtime_sensor_detail_custom.prototype.decorate_contents_ = function(parent) { - parent.appendChild($.createElement('p').innerHTML('Choose a custom range to display on the Sensor Detail chart. Max range is 7 days at a time and 30 days in the past. This limit will be raised in the future.')); + parent.appendChild($.createElement('p').innerHTML('Choose a custom range to display on the Sensor Detail chart. Max range is 7 days at a time and 3 months in the past.')); this.decorate_range_type_(parent); @@ -99,7 +99,7 @@ beestat.component.modal.runtime_sensor_detail_custom.prototype.decorate_range_st var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; var min = moment.max( moment(thermostat.sync_begin), - moment().subtract(1, 'month') + moment().subtract(3, 'month') ); var max = moment(thermostat.sync_end); diff --git a/js/component/modal/visualize_custom.js b/js/component/modal/visualize_custom.js new file mode 100644 index 0000000..ad18e4e --- /dev/null +++ b/js/component/modal/visualize_custom.js @@ -0,0 +1,240 @@ +/** + * Custom date range for the Runtime Detail chart. + */ +beestat.component.modal.visualize_custom = function() { + beestat.component.modal.apply(this, arguments); + + this.state_.range_begin = beestat.setting('visualize.range_static.begin'); + this.state_.range_end = beestat.setting('visualize.range_static.end'); + + this.state_.error = { + 'range_diff_invalid': { + 'triggered': false, + 'message': 'Max range is seven days' + }, + 'range_begin_invalid': { + 'triggered': false, + 'message': 'Begin date is invalid' + }, + 'range_end_invalid': { + 'triggered': false, + 'message': 'End date is invalid' + } + }; +}; +beestat.extend(beestat.component.modal.visualize_custom, beestat.component.modal); + +/** + * Decorate. + * + * @param {rocket.Elements} parent + */ +beestat.component.modal.visualize_custom.prototype.decorate_contents_ = function(parent) { + const instructions_container = document.createElement('p'); + instructions_container.innerText = 'Choose a date range of up to seven days. Multi-day ranges will be be averaged into a single 24-hour span.'; + parent.appendChild(instructions_container); + + this.decorate_inputs_(parent[0]); + + if (this.has_error_() === true) { + this.decorate_error_(parent[0]); + } +}; + +/** + * Decorate the static range inputs. + * + * @param {HTMLDivElement} parent + */ +beestat.component.modal.visualize_custom.prototype.decorate_inputs_ = function(parent) { + const self = this; + + const container = document.createElement('div'); + Object.assign(container.style, { + 'display': 'flex', + 'grid-gap': `${beestat.style.size.gutter}px`, + 'align-items': 'center' + }); + parent.appendChild(container); + + // Range begin + this.range_begin_input_ = new beestat.component.input.text() + .set_width(110) + .set_maxlength(10) + .set_requirements({ + 'required': true, + 'type': 'date' + }) + .set_transform({ + 'type': 'date' + }) + .set_icon('calendar') + .set_value( + beestat.date(this.state_.range_begin) + ) + .render($(container)); + + this.range_begin_input_.addEventListener('blur', function() { + self.state_.range_begin = this.get_value(); + }); + + // To + const to = document.createElement('div'); + to.innerText = 'to'; + container.appendChild(to); + + // Range end + this.range_end_input_ = new beestat.component.input.text() + .set_width(110) + .set_maxlength(10) + .set_requirements({ + 'required': true, + 'type': 'date' + }) + .set_transform({ + 'type': 'date' + }) + .set_icon('calendar') + .set_value( + beestat.date(this.state_.range_end) + ) + .render($(container)); + + this.range_end_input_.addEventListener('blur', function() { + self.state_.range_end = this.get_value(); + }); +}; + +/** + * Decorate the error area. + * + * @param {HTMLDivElement} parent + */ +beestat.component.modal.visualize_custom.prototype.decorate_error_ = function(parent) { + const container = document.createElement('div'); + Object.assign(container.style, { + 'background': beestat.style.color.red.base, + 'color': '#fff', + 'border-radius': `${beestat.style.size.border_radius}px`, + 'padding': `${beestat.style.size.gutter}px`, + 'margin-top': `${beestat.style.size.gutter}px` + }); + parent.appendChild(container); + + for (let key in this.state_.error) { + if (this.state_.error[key].triggered === true) { + const error_div = document.createElement('div'); + error_div.innerText = this.state_.error[key].message; + container.appendChild(error_div); + } + } +}; + +/** + * Check and see whether not there is currently an error. + * + * @return {boolean} + */ +beestat.component.modal.visualize_custom.prototype.has_error_ = function() { + this.check_error_(); + + for (let key in this.state_.error) { + if (this.state_.error[key].triggered === true) { + return true; + } + } + + return false; +}; + +/** + * Check to see if there are any errors and update the state. + */ +beestat.component.modal.visualize_custom.prototype.check_error_ = function() { + this.state_.error.range_begin_invalid.triggered = + !this.range_begin_input_.meets_requirements(); + + this.state_.error.range_end_invalid.triggered = + !this.range_end_input_.meets_requirements(); + + this.state_.error.range_diff_invalid.triggered = false; + if ( + this.range_begin_input_.meets_requirements() === true && + this.range_end_input_.meets_requirements() === true + ) { + const range_begin_m = moment(this.range_begin_input_.get_value()); + const range_end_m = moment(this.range_end_input_.get_value()); + + if (Math.abs(range_begin_m.diff(range_end_m, 'day')) > 7) { + this.state_.error.range_diff_invalid.triggered = true; + } + } +}; + +/** + * Get title. + * + * @return {string} Title + */ +beestat.component.modal.visualize_custom.prototype.get_title_ = function() { + return 'Visualize - Custom Range'; +}; + +/** + * Get the buttons that go on the bottom of this modal. + * + * @return {[beestat.component.button]} The buttons. + */ +beestat.component.modal.visualize_custom.prototype.get_buttons_ = function() { + var self = this; + + var cancel = new beestat.component.tile() + .set_background_color('#fff') + .set_text_color(beestat.style.color.gray.base) + .set_text_hover_color(beestat.style.color.red.base) + .set_shadow(false) + .set_text('Cancel') + .addEventListener('click', function() { + self.dispose(); + }); + + const save = new beestat.component.tile() + .set_background_color(beestat.style.color.green.base) + .set_background_hover_color(beestat.style.color.green.light) + .set_text_color('#fff') + .set_text('Save') + .addEventListener('click', function() { + this + .set_background_color(beestat.style.color.gray.base) + .set_background_hover_color() + .removeEventListener('click'); + + if (self.has_error_() === true) { + self.rerender(); + } else { + if (moment(self.state_.range_begin).isAfter(moment(self.state_.range_end)) === true) { + var temp = self.state_.range_begin; + self.state_.range_begin = self.state_.range_end; + self.state_.range_end = temp; + } + + beestat.cache.delete('data.three_d__runtime_sensor'); + beestat.setting( + { + 'visualize.range_type': 'static', + 'visualize.range_static.begin': self.state_.range_begin, + 'visualize.range_static.end': self.state_.range_end + }, + undefined, + function() { + self.dispose(); + } + ); + } + }); + + return [ + cancel, + save + ]; +}; diff --git a/js/js.php b/js/js.php index 539c4e7..b94d290 100755 --- a/js/js.php +++ b/js/js.php @@ -47,6 +47,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; // Layer echo '' . PHP_EOL; @@ -127,6 +128,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL;