diff --git a/js/beestat.js b/js/beestat.js index 931d323..b5a988c 100644 --- a/js/beestat.js +++ b/js/beestat.js @@ -52,36 +52,9 @@ if ('serviceWorker' in navigator) { } /** - * Dispatch a breakpoint event every time a browser resize crosses one of the - * breakpoints. Typically a component will use this event to rerender itself - * when CSS breakpoints are not feasible or appropriate. + * Dispatch the resize event every now and then. */ -beestat.width = window.innerWidth; window.addEventListener('resize', rocket.throttle(100, function() { - var breakpoints = [ - 600, - 650, - 800, - 850, - 1000 - ]; - - breakpoints.forEach(function(breakpoint) { - if ( - ( - beestat.width > breakpoint && - window.innerWidth <= breakpoint - ) || - ( - beestat.width < breakpoint && - window.innerWidth >= breakpoint - ) - ) { - beestat.width = window.innerWidth; - beestat.dispatcher.dispatchEvent('breakpoint'); - } - }); - beestat.dispatcher.dispatchEvent('resize'); })); diff --git a/js/beestat/text_dimensions.js b/js/beestat/text_dimensions.js new file mode 100644 index 0000000..ccb3d7c --- /dev/null +++ b/js/beestat/text_dimensions.js @@ -0,0 +1,30 @@ +/** + * Get the dimensions of a text string. + * + * @param {string} text + * @param {number} font_size + * @param {number} font_weight + * + * @return {number} + */ +beestat.text_dimensions = function(text, font_size, font_weight) { + const div = document.createElement('div'); + div.style.fontSize = font_size + 'px'; + div.style.fontWeight = font_weight; + div.style.position = 'absolute'; + div.style.left = -1000; + div.style.top = -1000; + + div.textContent = text; + + document.body.appendChild(div); + + const bounding_box = div.getBoundingClientRect(); + + document.body.removeChild(div); + + return { + 'width': bounding_box.width, + 'height': bounding_box.height + }; +}; diff --git a/js/component.js b/js/component.js index cebad9f..551cce2 100644 --- a/js/component.js +++ b/js/component.js @@ -8,8 +8,8 @@ beestat.component = function() { this.layer_ = beestat.current_layer; - if (this.rerender_on_breakpoint_ === true) { - beestat.dispatcher.addEventListener('breakpoint', function() { + if (this.rerender_on_resize_ === true) { + beestat.dispatcher.addEventListener('resize', function() { self.rerender(); }); } diff --git a/js/component/card/air_quality_summary.js b/js/component/card/air_quality_summary.js index 316a639..236b4ab 100644 --- a/js/component/card/air_quality_summary.js +++ b/js/component/card/air_quality_summary.js @@ -35,7 +35,7 @@ beestat.component.card.air_quality_summary = function(thermostat_id) { }; beestat.extend(beestat.component.card.air_quality_summary, beestat.component.card); -beestat.component.card.air_quality_summary.prototype.rerender_on_breakpoint_ = true; +beestat.component.card.air_quality_summary.prototype.rerender_on_resize_ = true; /** * Decorate @@ -245,7 +245,7 @@ beestat.component.card.air_quality_summary.prototype.decorate_chart_ = function( tr.appendChild( $.createElement('td') - .innerText(new_hour + (beestat.width > 700 ? meridiem : '')) + .innerText(new_hour + (window.innerWidth > 700 ? meridiem : '')) .style({ 'text-align': 'center', 'font-size': beestat.style.font_size.small diff --git a/js/component/card/metrics.js b/js/component/card/metrics.js index 06b3a76..c3df250 100644 --- a/js/component/card/metrics.js +++ b/js/component/card/metrics.js @@ -30,7 +30,7 @@ beestat.component.card.metrics = function(thermostat_id) { }; beestat.extend(beestat.component.card.metrics, beestat.component.card); -beestat.component.card.metrics.prototype.rerender_on_breakpoint_ = true; +beestat.component.card.metrics.prototype.rerender_on_resize_ = true; /** * Decorate @@ -64,9 +64,9 @@ beestat.component.card.metrics.prototype.decorate_contents_ = function(parent) { this.decorate_empty_(parent); } else { let column_count = 1; - if (beestat.width > 1000) { + if (window.innerWidth > 1000) { column_count = 3; - } else if (beestat.width > 800) { + } else if (window.innerWidth > 800) { column_count = 2; } const column_span = 12 / column_count; diff --git a/js/component/chart.js b/js/component/chart.js index 2c3681c..61fe72d 100644 --- a/js/component/chart.js +++ b/js/component/chart.js @@ -661,7 +661,7 @@ beestat.component.chart.prototype.tooltip_formatter_helper_ = function(title, se beestat.component.chart.prototype.get_dock_tooltip_ = function() { return ( beestat.setting('ui.always_dock_tooltips') === true || - beestat.width < 600 + window.innerWidth < 600 ); }; diff --git a/js/component/header.js b/js/component/header.js index 2baa458..b4d7f0b 100644 --- a/js/component/header.js +++ b/js/component/header.js @@ -4,7 +4,7 @@ * @param {string} active_layer The currently active layer. */ beestat.component.header = function(active_layer) { - var self = this; + const self = this; this.active_layer_ = active_layer; @@ -19,12 +19,170 @@ beestat.component.header = function(active_layer) { }; beestat.extend(beestat.component.header, beestat.component); -beestat.component.header.prototype.rerender_on_breakpoint_ = true; +beestat.component.header.prototype.rerender_on_resize_ = true; +/** + * Decorate. + * + * @param {rocket.Elements} parent + */ beestat.component.header.prototype.decorate_ = function(parent) { - var self = this; + // Define base widths for every part of the header at different sizes. + const switcher_width = this.get_switcher_width_(); + this.dimensions_ = { + 'large': { + 'logo': 160, + 'navigation': 565, + 'switcher': switcher_width, + 'menu': 50, + 'right_margin': 16 + }, + 'medium': { + 'logo': 160, + 'navigation': 225, + 'switcher': switcher_width, + 'menu': 50, + 'right_margin': 16 + }, + 'small': { + 'logo': 55, + 'navigation': 225, + 'switcher': switcher_width, + 'menu': 50, + 'right_margin': 16 + } + }; - var pages = [ + /** + * Figure out which configuration will fit, preferring the largest first + * with the switcher, then without the switcher. Same pattern as we get + * smaller. + */ + if ( + window.innerWidth >= ( + this.dimensions_.large.logo + + this.dimensions_.large.navigation + + this.dimensions_.large.switcher + + this.dimensions_.large.menu + + this.dimensions_.large.right_margin + ) + ) { + this.dimension_ = 'large'; + this.switcher_enabled_ = true; + } else if ( + window.innerWidth >= ( + this.dimensions_.large.logo + + this.dimensions_.large.navigation + + this.dimensions_.large.menu + + this.dimensions_.large.right_margin + ) + ) { + this.dimension_ = 'large'; + this.switcher_enabled_ = false; + } else if ( + window.innerWidth >= ( + this.dimensions_.medium.logo + + this.dimensions_.medium.navigation + + this.dimensions_.medium.switcher + + this.dimensions_.medium.menu + + this.dimensions_.medium.right_margin + ) + ) { + this.dimension_ = 'medium'; + this.switcher_enabled_ = true; + } else if ( + window.innerWidth >= ( + this.dimensions_.medium.logo + + this.dimensions_.medium.navigation + + this.dimensions_.medium.menu + + this.dimensions_.medium.right_margin + ) + ) { + this.dimension_ = 'medium'; + this.switcher_enabled_ = false; + } else if ( + window.innerWidth >= ( + this.dimensions_.small.logo + + this.dimensions_.small.navigation + + this.dimensions_.small.switcher + + this.dimensions_.small.menu + + this.dimensions_.small.right_margin + ) + ) { + this.dimension_ = 'small'; + this.switcher_enabled_ = true; + } else if ( + window.innerWidth >= ( + this.dimensions_.small.logo + + this.dimensions_.small.navigation + + this.dimensions_.small.menu + + this.dimensions_.small.right_margin + ) + ) { + this.dimension_ = 'small'; + this.switcher_enabled_ = false; + } + + // Decorate all the parts into a flex row. + const row = $.createElement('div').style({ + 'display': 'flex', + 'align-items': 'center', + 'flex-grow': '1', + 'margin': '-' + (beestat.style.size.gutter / 2) + 'px 0 ' + (beestat.style.size.gutter / 4) + 'px -' + beestat.style.size.gutter + 'px' + }); + + this.decorate_logo_(row); + this.decorate_navigation_(row); + if (this.switcher_enabled_ === true) { + this.decorate_switcher_(row); + } + this.decorate_menu_(row); + + parent.appendChild(row); +}; + +/** + * Decorate the logo. + * + * @param {rocket.Elements} parent + */ +beestat.component.header.prototype.decorate_logo_ = function(parent) { + const column = $.createElement('div') + .style({ + 'flex': '0 0 ' + this.dimensions_[this.dimension_].logo + 'px', + 'padding': beestat.style.size.gutter + 'px 0 0 ' + beestat.style.size.gutter + 'px' + }); + + if (this.dimension_ === 'medium' || this.dimension_ === 'large') { + column.style({ + 'margin': '8px 0 4px 0' + }); + (new beestat.component.logo(32)).render(column); + } else { + // column.style({'flex': '0 0 ' + dimensions[dimension].logo + 'px'}); + const img = $.createElement('img') + .setAttribute('src', '/favicon.png') + .style({ + 'width': '32px', + 'height': '32px', + 'margin-top': '11px', + 'margin-bottom': '6px' + }); + column.appendChild(img); + } + + parent.appendChild(column); +}; + +/** + * Decorate the navigation buttons. + * + * @param {rocket.Elements} parent + */ +beestat.component.header.prototype.decorate_navigation_ = function(parent) { + const self = this; + + const pages = [ { 'layer': 'detail', 'text': 'Detail', @@ -52,55 +210,29 @@ beestat.component.header.prototype.decorate_ = function(parent) { } ]; - pages.push(); - - var gutter = beestat.style.size.gutter; - - var row = $.createElement('div').style({ - 'display': 'flex', - 'align-items': 'center', - 'flex-grow': '1', - 'margin': '-' + (gutter / 2) + 'px 0 ' + (gutter / 4) + 'px -' + gutter + 'px' + const column = $.createElement('div').style({ + 'padding': beestat.style.size.gutter + 'px 0 0 ' + beestat.style.size.gutter + 'px' }); - parent.appendChild(row); - // Logo - var column_logo = $.createElement('div').style({'padding': gutter + 'px 0 0 ' + gutter + 'px'}); - row.appendChild(column_logo); - if (beestat.width > 600) { - column_logo.style({ - 'flex': '0 0 160px', - 'margin': '8px 0 4px 0' + // If the swithcer is enabled, that takes up extra space. If not, this does. + if (this.switcher_enabled_ === true) { + column.style({ + 'flex': '0 0 ' + this.dimensions_[this.dimension_].navigation + 'px' }); - (new beestat.component.logo(32)).render(column_logo); } else { - column_logo.style({'flex': '0 0 32px'}); - var img = $.createElement('img') - .setAttribute('src', '/favicon.png') - .style({ - 'width': '32px', - 'height': '32px', - 'margin-top': '11px', - 'margin-bottom': '6px' - }); - column_logo.appendChild(img); + column.style({ + 'flex': '1' + }); } - // Navigation - var column_navigation = $.createElement('div').style({ - 'flex': '1', - 'padding': gutter + 'px 0 0 ' + gutter + 'px' - }); - row.appendChild(column_navigation); - - var tile_group = new beestat.component.tile_group(); + const tile_group = new beestat.component.tile_group(); pages.forEach(function(page) { - var button = new beestat.component.tile() + const button = new beestat.component.tile() .set_icon(page.icon) .set_shadow(false) .set_text_color(beestat.style.color.bluegray.dark); - if (beestat.width > 850) { + if (self.dimension_ === 'large') { button.set_text(page.text); } @@ -122,29 +254,120 @@ beestat.component.header.prototype.decorate_ = function(parent) { tile_group.add_tile(button); }); - tile_group.render(column_navigation); + tile_group.render(column); + parent.appendChild(column); +}; + +/** + * Decorate the thermostat switcher. + * + * @param {rocket.Elements} parent + */ +beestat.component.header.prototype.decorate_switcher_ = function(parent) { + const column = $.createElement('div').style({ + 'flex': '1', + 'padding': beestat.style.size.gutter + 'px 0 0 ' + beestat.style.size.gutter + 'px', + 'text-align': 'right' + }); + + const change_thermostat_tile_group = new beestat.component.tile_group(); + + const sorted_thermostats = $.values(beestat.cache.thermostat) + .sort(function(a, b) { + return a.name > b.name; + }); + + sorted_thermostats.forEach(function(thermostat) { + if (thermostat.thermostat_id !== beestat.setting('thermostat_id')) { + const change_thermostat_tile = new beestat.component.tile.thermostat.switcher(thermostat.thermostat_id) + .set_size('medium') + .set_text_color('#fff') + .set_background_color(beestat.style.color.bluegray.base) + .set_background_hover_color('#fff') + .set_text_hover_color(beestat.style.color.bluegray.dark) + .addEventListener('click', function() { + beestat.setting('thermostat_id', thermostat.thermostat_id, function() { + window.location.reload(); + }); + }); + + change_thermostat_tile_group.add_tile(change_thermostat_tile); + } + }); + + change_thermostat_tile_group.render(column); + + parent.appendChild(column); +}; + +/** + * Get the width of the thermostat switcher box. This could change due to any + * number of factors, but it should more or less work. + * + * @return {number} Width in pixels. + */ +beestat.component.header.prototype.get_switcher_width_ = function() { + let width = 0; + + const sorted_thermostats = $.values(beestat.cache.thermostat) + .sort(function(a, b) { + return a.name > b.name; + }); + + sorted_thermostats.forEach(function(thermostat) { + if (thermostat.thermostat_id !== beestat.setting('thermostat_id')) { + const change_thermostat_tile = new beestat.component.tile.thermostat.switcher( + thermostat.thermostat_id + ); + const text_dimensions = beestat.text_dimensions( + change_thermostat_tile.get_text_(), + 13, + 300 + ); + + width += text_dimensions.width; + + // Left/right padding on the button 8+8=16 + width += 16; + + // Left margin between buttons + width += 8; + } + }); + + // Left padding on the column + width += 16; + + return width; +}; + +/** + * Decorate the menu. + * + * @param {rocket.Elements} parent + */ +beestat.component.header.prototype.decorate_menu_ = function(parent) { // Menu - - var last_read_announcement_id = beestat.setting('last_read_announcement_id'); - var unread_announcement_count = Object.keys(beestat.cache.announcement) + const last_read_announcement_id = beestat.setting('last_read_announcement_id'); + const unread_announcement_count = Object.keys(beestat.cache.announcement) .filter(function(announcement_id) { return announcement_id > last_read_announcement_id; }).length; - var column_menu = $.createElement('div').style({ - 'flex': '0 0 50px', - 'padding': gutter + 'px 0 0 ' + gutter + 'px', + const column = $.createElement('div').style({ + 'flex': '0 0 ' + this.dimensions_[this.dimension_].menu + 'px', + 'padding': beestat.style.size.gutter + 'px 0 0 ' + beestat.style.size.gutter + 'px', 'text-align': 'right' }); - row.appendChild(column_menu); - var menu = new beestat.component.menu(); + + const menu = new beestat.component.menu(); if (unread_announcement_count > 0) { menu .set_bubble_text(unread_announcement_count) .set_bubble_color(beestat.style.color.red.base); } - menu.render(column_menu); + menu.render(column); if (Object.keys(beestat.cache.ecobee_thermostat).length > 1) { menu.add_menu_item(new beestat.component.menu_item() @@ -164,7 +387,7 @@ beestat.component.header.prototype.decorate_ = function(parent) { })); } - var announcements_menu_item = new beestat.component.menu_item() + const announcements_menu_item = new beestat.component.menu_item() .set_text('Announcements') .set_icon('bullhorn') .set_callback(function() { @@ -211,4 +434,7 @@ beestat.component.header.prototype.decorate_ = function(parent) { window.beestat_api_key_local ); })); + + parent.appendChild(column); }; + diff --git a/js/component/modal/change_thermostat.js b/js/component/modal/change_thermostat.js index 02ea7f0..a119b5c 100644 --- a/js/component/modal/change_thermostat.js +++ b/js/component/modal/change_thermostat.js @@ -34,6 +34,7 @@ beestat.component.modal.change_thermostat.prototype.decorate_contents_ = functio grid.appendChild(div); const tile = new beestat.component.tile.thermostat(thermostat.thermostat_id) + .set_size('large') .set_text_color('#fff') .set_display('block'); diff --git a/js/component/tile.js b/js/component/tile.js index e6ff42d..8cea68b 100644 --- a/js/component/tile.js +++ b/js/component/tile.js @@ -75,6 +75,13 @@ beestat.component.tile.prototype.decorate_ = function(parent) { }); } + // Width override + if (this.width_ !== undefined) { + Object.assign(this.container_.style, { + 'width': `${this.width_}px` + }); + } + // Tabbable if (tabbable === true) { this.container_.setAttribute('tabIndex', '0'); @@ -194,6 +201,9 @@ beestat.component.tile.prototype.decorate_right_ = function(parent) { const text_container = document.createElement('div'); text_container.innerText = this.get_text_(); text_container.style.fontWeight = beestat.style.font_weight.normal; + text_container.style.whiteSpace = 'nowrap'; + text_container.style.overflow = 'hidden'; + text_container.style.textOverflow = 'ellipsis'; parent.appendChild(text_container); } }; @@ -460,3 +470,18 @@ beestat.component.tile.prototype.set_bubble_color = function(bubble_color) { } return this; }; + +/** + * Set the width of the button. + * + * @param {number} width + * + * @return {beestat.component.tile} This. + */ +beestat.component.tile.prototype.set_width = function(width) { + this.width_ = width; + if (this.rendered_ === true) { + this.rerender(); + } + return this; +}; diff --git a/js/component/tile/floor_plan.js b/js/component/tile/floor_plan.js index 5290c01..b3a3fe8 100644 --- a/js/component/tile/floor_plan.js +++ b/js/component/tile/floor_plan.js @@ -7,7 +7,7 @@ beestat.component.tile.floor_plan = function(floor_plan_id) { this.floor_plan_id_ = floor_plan_id; - beestat.component.apply(this, arguments); + beestat.component.tile.apply(this, arguments); }; beestat.extend(beestat.component.tile.floor_plan, beestat.component.tile); diff --git a/js/component/tile/floor_plan_group.js b/js/component/tile/floor_plan_group.js index 7499d27..15bd757 100644 --- a/js/component/tile/floor_plan_group.js +++ b/js/component/tile/floor_plan_group.js @@ -6,7 +6,7 @@ beestat.component.tile.floor_plan_group = function(floor_plan_group) { this.floor_plan_group_ = floor_plan_group; - beestat.component.apply(this, arguments); + beestat.component.tile.apply(this, arguments); }; beestat.extend(beestat.component.tile.floor_plan_group, beestat.component.tile); diff --git a/js/component/tile/thermostat.js b/js/component/tile/thermostat.js index d3b8193..cc70e85 100644 --- a/js/component/tile/thermostat.js +++ b/js/component/tile/thermostat.js @@ -1,12 +1,12 @@ /** * A tile representing a thermostat. * - * @param {integer} thermostat_id + * @param {number} thermostat_id */ beestat.component.tile.thermostat = function(thermostat_id) { this.thermostat_id_ = thermostat_id; - beestat.component.apply(this, arguments); + beestat.component.tile.apply(this, arguments); }; beestat.extend(beestat.component.tile.thermostat, beestat.component.tile); diff --git a/js/component/tile/thermostat/switcher.js b/js/component/tile/thermostat/switcher.js new file mode 100644 index 0000000..ed3ec7c --- /dev/null +++ b/js/component/tile/thermostat/switcher.js @@ -0,0 +1,46 @@ +/** + * A tile representing a thermostat for the quick switch. + * + * @param {integer} thermostat_id + */ +beestat.component.tile.thermostat.switcher = function(thermostat_id) { + this.thermostat_id_ = thermostat_id; + + beestat.component.tile.thermostat.apply(this, arguments); +}; +beestat.extend(beestat.component.tile.thermostat.switcher, beestat.component.tile.thermostat); + +/** + * Get the icon for this tile. + * + * @return {string} The icon. + */ +beestat.component.tile.thermostat.switcher.prototype.get_icon_ = function() { + return undefined; +}; + +/** + * Get the text for this tile. + * + * @return {string} The first line of text. + */ +beestat.component.tile.thermostat.switcher.prototype.get_text_ = function() { + const thermostat = beestat.cache.thermostat[this.thermostat_id_]; + + const temperature = beestat.temperature({ + 'temperature': thermostat.temperature, + 'round': 0, + 'units': true + }); + + return thermostat.name + ' • ' + temperature; +}; + +/** + * Get the size of this tile. + * + * @return {string} The size of this tile. + */ +beestat.component.tile.thermostat.switcher.prototype.get_size_ = function() { + return 'medium'; +}; diff --git a/js/js.php b/js/js.php index 97c5e34..48ccad2 100755 --- a/js/js.php +++ b/js/js.php @@ -50,6 +50,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; @@ -149,6 +150,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;