diff --git a/css/dashboard.css b/css/dashboard.css
index d0877fc..a2d2491 100644
--- a/css/dashboard.css
+++ b/css/dashboard.css
@@ -415,6 +415,9 @@ input[type=radio] {
.icon.air_filter:before { content: "\F0D43"; }
.icon.air_purifier:before { content: "\F0D44"; }
.icon.alarm_snooze:before { content: "\F068E"; }
+.icon.alpha_b:before { content: "\F0AEF"; }
+.icon.alpha_b_box:before { content: "\F0B09"; }
+.icon.arrow_expand_vertical:before { content: "\F084F"; }
.icon.arrow_left:before { content: "\F004D"; }
.icon.basket_fill:before { content: "\F0077"; }
.icon.basket_unfill:before { content: "\F0078"; }
@@ -461,21 +464,40 @@ input[type=radio] {
.icon.home_search:before { content: "\F13B0"; }
.icon.information:before { content: "\F02FC"; }
.icon.key:before { content: "\F0306"; }
+.icon.label:before { content: "\F0315"; }
.icon.layers:before { content: "\F0328"; }
.icon.layers_plus:before { content: "\F0E4D"; }
.icon.magnify_close:before { content: "\F0980"; }
-.icon.magnify_plus_outline:before { content: "\F06ED"; }
.icon.magnify_minus_outline:before { content: "\F06EC"; }
+.icon.magnify_plus_outline:before { content: "\F06ED"; }
.icon.map_marker:before { content: "\F05F8"; }
.icon.menu_down:before { content: "\F035D"; }
.icon.menu_up:before { content: "\F0360"; }
.icon.message:before { content: "\F0361"; }
.icon.network_strength_4:before { content: "\F08FA"; }
.icon.network_strength_off:before { content: "\F08FC"; }
-/*.icon.numeric_1_box:before { content: "\F03A4"; }
+.icon.numeric_0:before { content: "\F0B39"; }
+.icon.numeric_0_box:before { content: "\F03A1"; }
+.icon.numeric_10:before { content: "\F0FE9"; }
+.icon.numeric_10_box:before { content: "\F0F7D"; }
+.icon.numeric_1:before { content: "\F0B3A"; }
+.icon.numeric_1_box:before { content: "\F03A4"; }
+.icon.numeric_2:before { content: "\F0B3B"; }
+.icon.numeric_2_box:before { content: "\F03A7"; }
+.icon.numeric_3:before { content: "\F0B3C"; }
.icon.numeric_3_box:before { content: "\F03AA"; }
+.icon.numeric_4:before { content: "\F0B3D"; }
.icon.numeric_4_box:before { content: "\F03AD"; }
-.icon.numeric_7_box:before { content: "\F03B6"; }*/
+.icon.numeric_5:before { content: "\F0B3E"; }
+.icon.numeric_5_box:before { content: "\F03B1"; }
+.icon.numeric_6:before { content: "\F0B3F"; }
+.icon.numeric_6_box:before { content: "\F03B3"; }
+.icon.numeric_7:before { content: "\F0B40"; }
+.icon.numeric_7_box:before { content: "\F03B6"; }
+.icon.numeric_8:before { content: "\F0B41"; }
+.icon.numeric_8_box:before { content: "\F03B9"; }
+.icon.numeric_9:before { content: "\F0B42"; }
+.icon.numeric_9_box:before { content: "\F03BC"; }
.icon.open_in_new:before { content: "\F03CC"; }
.icon.patreon:before { content: "\F0882"; }
.icon.pound:before { content: "\F0423"; }
@@ -508,88 +530,9 @@ input[type=radio] {
.icon.wifi_strength_1_alert:before { content: "\F0920"; }
.icon.wifi_strength_4:before { content: "\F0928"; }
.icon.zigbee:before { content: "\F0D41"; }
-.icon.home_plus:before { content: "\F0975"; }
-.icon.home_switch:before { content: "\F1794"; }
-.icon.home_remove:before { content: "\F1247"; }
-.icon.arrow_expand_vertical:before { content: "\F084F"; }
-.icon.label:before { content: "\F0315"; }
-
-.icon.numeric_0:before { content: "\F0B39"; }
-.icon.numeric_0_box:before { content: "\F03A1"; }
-.icon.numeric_1:before { content: "\F0B3A"; }
-.icon.numeric_1_box:before { content: "\F03A4"; }
-.icon.numeric_10:before { content: "\F0FE9"; }
-.icon.numeric_10_box:before { content: "\F0F7D"; }
-.icon.numeric_2:before { content: "\F0B3B"; }
-.icon.numeric_2_box:before { content: "\F03A7"; }
-.icon.numeric_3:before { content: "\F0B3C"; }
-.icon.numeric_3_box:before { content: "\F03AA"; }
-.icon.numeric_4:before { content: "\F0B3D"; }
-.icon.numeric_4_box:before { content: "\F03AD"; }
-.icon.numeric_5:before { content: "\F0B3E"; }
-.icon.numeric_5_box:before { content: "\F03B1"; }
-.icon.numeric_6:before { content: "\F0B3F"; }
-.icon.numeric_6_box:before { content: "\F03B3"; }
-.icon.numeric_7:before { content: "\F0B40"; }
-.icon.numeric_7_box:before { content: "\F03B6"; }
-.icon.numeric_8:before { content: "\F0B41"; }
-.icon.numeric_8_box:before { content: "\F03B9"; }
-.icon.numeric_9:before { content: "\F0B42"; }
-.icon.numeric_9_box:before { content: "\F03BC"; }
-.icon.alpha_a:before { content: "\F0AEE"; }
-.icon.alpha_a_box:before { content: "\F0B08"; }
-.icon.alpha_b:before { content: "\F0AEF"; }
-.icon.alpha_b_box:before { content: "\F0B09"; }
-.icon.alpha_c:before { content: "\F0AF0"; }
-.icon.alpha_c_box:before { content: "\F0B0A"; }
-.icon.alpha_d:before { content: "\F0AF1"; }
-.icon.alpha_d_box:before { content: "\F0B0B"; }
-.icon.alpha_e:before { content: "\F0AF2"; }
-.icon.alpha_e_box:before { content: "\F0B0C"; }
-.icon.alpha_f:before { content: "\F0AF3"; }
-.icon.alpha_f_box:before { content: "\F0B0D"; }
-.icon.alpha_g:before { content: "\F0AF4"; }
-.icon.alpha_g_box:before { content: "\F0B0E"; }
-.icon.alpha_h:before { content: "\F0AF5"; }
-.icon.alpha_h_box:before { content: "\F0B0F"; }
-.icon.alpha_i:before { content: "\F0AF6"; }
-.icon.alpha_i_box:before { content: "\F0B10"; }
-.icon.alpha_j:before { content: "\F0AF7"; }
-.icon.alpha_j_box:before { content: "\F0B11"; }
-.icon.alpha_k:before { content: "\F0AF8"; }
-.icon.alpha_k_box:before { content: "\F0B12"; }
-.icon.alpha_l:before { content: "\F0AF9"; }
-.icon.alpha_l_box:before { content: "\F0B13"; }
-.icon.alpha_m:before { content: "\F0AFA"; }
-.icon.alpha_m_box:before { content: "\F0B14"; }
-.icon.alpha_n:before { content: "\F0AFB"; }
-.icon.alpha_n_box:before { content: "\F0B15"; }
-.icon.alpha_o:before { content: "\F0AFC"; }
-.icon.alpha_o_box:before { content: "\F0B16"; }
-.icon.alpha_p:before { content: "\F0AFD"; }
-.icon.alpha_p_box:before { content: "\F0B17"; }
-.icon.alpha_q:before { content: "\F0AFE"; }
-.icon.alpha_q_box:before { content: "\F0B18"; }
-.icon.alpha_r:before { content: "\F0AFF"; }
-.icon.alpha_r_box:before { content: "\F0B19"; }
-.icon.alpha_s:before { content: "\F0B00"; }
-.icon.alpha_s_box:before { content: "\F0B1A"; }
-.icon.alpha_t:before { content: "\F0B01"; }
-.icon.alpha_t_box:before { content: "\F0B1B"; }
-.icon.alpha_u:before { content: "\F0B02"; }
-.icon.alpha_u_box:before { content: "\F0B1C"; }
-.icon.alpha_v:before { content: "\F0B03"; }
-.icon.alpha_v_box:before { content: "\F0B1D"; }
-.icon.alpha_w:before { content: "\F0B04"; }
-.icon.alpha_w_box:before { content: "\F0B1E"; }
-.icon.alpha_x:before { content: "\F0B05"; }
-.icon.alpha_x_box:before { content: "\F0B1F"; }
-.icon.alpha_y:before { content: "\F0B06"; }
-.icon.alpha_y_box:before { content: "\F0B20"; }
-.icon.alpha_z:before { content: "\F0B07"; }
-.icon.alpha_z_box:before { content: "\F0B21"; }
-
-
+.icon.pencil:before { content: "\F03EB"; }
+.icon.plus:before { content: "\F0415"; }
+.icon.delete:before { content: "\F01B4"; }
.icon.f16:before { font-size: 16px; }
.icon.f24:before { font-size: 24px; }
diff --git a/js/component/card/floor_plan_editor.js b/js/component/card/floor_plan_editor.js
index 313207b..3c88317 100644
--- a/js/component/card/floor_plan_editor.js
+++ b/js/component/card/floor_plan_editor.js
@@ -14,7 +14,9 @@ beestat.component.card.floor_plan_editor = function(thermostat_id) {
self.rerender();
// Center the content if the floor plan changed.
- self.floor_plan_.center_content();
+ if (self.floor_plan_ !== undefined) {
+ self.floor_plan_.center_content();
+ }
}, 10);
beestat.dispatcher.addEventListener(
@@ -33,10 +35,12 @@ beestat.component.card.floor_plan_editor = function(thermostat_id) {
}
// The first time this component renders center the content.
- this.addEventListener('render', function() {
- self.floor_plan_.center_content();
- self.removeEventListener('render');
- });
+ if (self.floor_plan_ !== undefined) {
+ this.addEventListener('render', function() {
+ self.floor_plan_.center_content();
+ self.removeEventListener('render');
+ });
+ }
};
beestat.extend(beestat.component.card.floor_plan_editor, beestat.component.card);
@@ -52,10 +56,10 @@ beestat.component.card.floor_plan_editor.prototype.decorate_contents_ = function
const center_container = $.createElement('div').style('text-align', 'center');
parent.appendChild(center_container);
- center_container.appendChild($.createElement('p').innerText('You haven\'t created any floor plans yet.'));
const get_started_button = new beestat.component.tile()
- .set_icon('home_plus')
- .set_text('Get Started')
+ .set_icon('plus')
+ .set_text('Create my first floor plan')
+ .set_size('large')
.set_background_color(beestat.style.color.green.dark)
.set_background_hover_color(beestat.style.color.green.light)
.render(center_container)
@@ -251,9 +255,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_floor_ = f
div = $.createElement('div');
grid.appendChild(div);
const elevation_input = new beestat.component.input.text()
- .set_label('Elevation (inches)')
- .set_placeholder(this.state_.active_group.elevation)
- .set_value(this.state_.active_group.elevation || '')
+ .set_label('Elevation (feet)')
+ .set_placeholder(this.state_.active_group.elevation / 12)
+ .set_value(this.state_.active_group.elevation / 12 || '')
.set_width('100%')
.set_maxlength('5')
.set_requirements({
@@ -262,12 +266,11 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_floor_ = f
})
.render(div);
- elevation_input.set_value(this.state_.active_group.elevation);
-
elevation_input.addEventListener('change', function() {
if (elevation_input.meets_requirements() === true) {
- self.state_.active_group.elevation = elevation_input.get_value();
+ self.state_.active_group.elevation = elevation_input.get_value() * 12;
self.update_floor_plan_();
+ self.rerender();
} else {
elevation_input.set_value(self.state_.active_group.elevation);
}
@@ -277,9 +280,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_floor_ = f
div = $.createElement('div');
grid.appendChild(div);
const height_input = new beestat.component.input.text()
- .set_label('Ceiling Height (inches)')
- .set_placeholder(this.state_.active_group.height)
- .set_value(this.state_.active_group.height || '')
+ .set_label('Ceiling Height (feet)')
+ .set_placeholder(this.state_.active_group.height / 12)
+ .set_value(this.state_.active_group.height / 12 || '')
.set_width('100%')
.set_maxlength('4')
.set_requirements({
@@ -289,11 +292,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_floor_ = f
})
.render(div);
- height_input.set_value(this.state_.active_group.height);
-
height_input.addEventListener('change', function() {
if (height_input.meets_requirements() === true) {
- self.state_.active_group.height = height_input.get_value();
+ self.state_.active_group.height = height_input.get_value() * 12;
self.update_floor_plan_();
} else {
height_input.set_value(self.state_.active_group.height);
@@ -353,9 +354,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_room_ = fu
div = $.createElement('div');
grid.appendChild(div);
const elevation_input = new beestat.component.input.text()
- .set_label('Elevation (inches)')
- .set_placeholder(this.state_.active_group.elevation)
- .set_value(this.state_.active_room.elevation || '')
+ .set_label('Elevation (feet)')
+ .set_placeholder(this.state_.active_group.elevation / 12)
+ .set_value(this.state_.active_room.elevation / 12 || '')
.set_width('100%')
.set_maxlength('5')
.set_requirements({
@@ -363,14 +364,11 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_room_ = fu
})
.render(div);
- if (this.state_.active_room.elevation !== undefined) {
- elevation_input.set_value(this.state_.active_room.elevation);
- }
-
elevation_input.addEventListener('change', function() {
if (elevation_input.meets_requirements() === true) {
- self.state_.active_room.elevation = elevation_input.get_value();
+ self.state_.active_room.elevation = elevation_input.get_value() * 12;
self.update_floor_plan_();
+ self.rerender();
} else {
elevation_input.set_value('');
}
@@ -380,9 +378,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_room_ = fu
div = $.createElement('div');
grid.appendChild(div);
const height_input = new beestat.component.input.text()
- .set_label('Ceiling Height (inches)')
- .set_placeholder(this.state_.active_group.height)
- .set_value(this.state_.active_room.height || '')
+ .set_label('Ceiling Height (feet)')
+ .set_placeholder(this.state_.active_group.height / 12)
+ .set_value(this.state_.active_room.height / 12 || '')
.set_width('100%')
.set_maxlength('4')
.set_requirements({
@@ -391,13 +389,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_room_ = fu
})
.render(div);
- if (this.state_.active_room.height !== undefined) {
- height_input.set_value(this.state_.active_room.height);
- }
-
height_input.addEventListener('change', function() {
if (height_input.meets_requirements() === true) {
- self.state_.active_room.height = height_input.get_value();
+ self.state_.active_room.height = height_input.get_value() * 12;
self.update_floor_plan_();
} else {
height_input.set_value('');
@@ -491,22 +485,26 @@ beestat.component.card.floor_plan_editor.prototype.get_subtitle_ = function() {
};
/**
- * Update the floor plan in the database.
+ * Update the floor plan in the database. This is throttled so the update can
+ * only run so fast.
*/
beestat.component.card.floor_plan_editor.prototype.update_floor_plan_ = function() {
- new beestat.api()
- .add_call(
- 'floor_plan',
- 'update',
- {
- 'attributes': {
- 'floor_plan_id': beestat.setting('floor_plan_id'),
- 'data': beestat.cache.floor_plan[beestat.setting('floor_plan_id')].data
- }
- },
- 'update_floor_plan'
- )
- .send();
+ window.clearTimeout(this.update_timeout_);
+ this.update_timeout_ = window.setTimeout(function() {
+ new beestat.api()
+ .add_call(
+ 'floor_plan',
+ 'update',
+ {
+ 'attributes': {
+ 'floor_plan_id': beestat.setting('floor_plan_id'),
+ 'data': beestat.cache.floor_plan[beestat.setting('floor_plan_id')].data
+ }
+ },
+ 'update_floor_plan'
+ )
+ .send();
+ }, 1000);
};
/**
@@ -522,7 +520,7 @@ beestat.component.card.floor_plan_editor.prototype.decorate_top_right_ = functio
if (window.is_demo === false) {
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Add New')
- .set_icon('home_plus')
+ .set_icon('plus')
.set_callback(function() {
new beestat.component.modal.create_floor_plan(
self.thermostat_id_
@@ -532,22 +530,34 @@ beestat.component.card.floor_plan_editor.prototype.decorate_top_right_ = functio
if (Object.keys(beestat.cache.floor_plan).length > 1) {
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Switch')
- .set_icon('home_switch')
+ .set_icon('swap_horizontal')
.set_callback(function() {
(new beestat.component.modal.change_floor_plan()).render();
}));
}
+ if (beestat.setting('floor_plan_id') !== null) {
+ menu.add_menu_item(new beestat.component.menu_item()
+ .set_text('Edit')
+ .set_icon('pencil')
+ .set_callback(function() {
+ new beestat.component.modal.update_floor_plan(
+ beestat.setting('floor_plan_id')
+ ).render();
+ }));
+ }
+
if (beestat.setting('floor_plan_id') !== null) {
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Delete')
- .set_icon('home_remove')
+ .set_icon('delete')
.set_callback(function() {
new beestat.component.modal.delete_floor_plan(
beestat.setting('floor_plan_id')
).render();
}));
}
+
}
menu.add_menu_item(new beestat.component.menu_item()
diff --git a/js/component/card/my_home.js b/js/component/card/my_home.js
index b18b61a..ea177f2 100644
--- a/js/component/card/my_home.js
+++ b/js/component/card/my_home.js
@@ -18,9 +18,20 @@ beestat.component.card.my_home = function(thermostat_id) {
beestat.extend(beestat.component.card.my_home, beestat.component.card);
beestat.component.card.my_home.prototype.decorate_contents_ = function(parent) {
- this.decorate_system_type_(parent);
- this.decorate_region_(parent);
- this.decorate_property_(parent);
+ const system_container = document.createElement('div');
+ system_container.style.marginBottom = `${beestat.style.size.gutter}px`;
+ parent.appendChild(system_container);
+ this.decorate_system_type_($(system_container));
+
+ const region_container = document.createElement('div');
+ region_container.style.marginBottom = `${beestat.style.size.gutter}px`;
+ parent.appendChild(region_container);
+ this.decorate_region_($(region_container));
+
+ const property_container = document.createElement('div');
+ property_container.style.marginBottom = `${beestat.style.size.gutter}px`;
+ parent.appendChild(property_container);
+ this.decorate_property_($(property_container));
};
/**
diff --git a/js/component/floor_plan.js b/js/component/floor_plan.js
index a2c3168..716c102 100644
--- a/js/component/floor_plan.js
+++ b/js/component/floor_plan.js
@@ -116,6 +116,53 @@ beestat.component.floor_plan.prototype.render = function(parent) {
}
} else if (e.key.toLowerCase() === 's') {
self.toggle_snapping_();
+ } else if (
+ e.key.toLowerCase() === 'c' &&
+ e.ctrlKey === true
+ ) {
+ self.state_.copied_room = beestat.clone(self.state_.active_room);
+ } else if (
+ e.key.toLowerCase() === 'v' &&
+ e.ctrlKey === true
+ ) {
+ if (self.state_.copied_room !== undefined) {
+ self.add_room_(self.state_.copied_room);
+ }
+ } else if (
+ e.key.toLowerCase() === 'z' &&
+ e.ctrlKey === true
+ ) {
+ console.log('undo');
+ } else if (
+ e.key === 'ArrowLeft' ||
+ e.key === 'ArrowRight' ||
+ e.key === 'ArrowUp' ||
+ e.key === 'ArrowDown'
+ ) {
+ const entity =
+ self.state_.active_point_entity ||
+ self.state_.active_wall_entity ||
+ self.state_.active_room_entity;
+
+ if (entity !== undefined) {
+ const x = entity.get_x();
+ const y = entity.get_y();
+
+ switch (e.key) {
+ case 'ArrowLeft':
+ entity.set_xy(x === null ? null : x - 1, y);
+ break;
+ case 'ArrowRight':
+ entity.set_xy(x === null ? null : x + 1, y);
+ break;
+ case 'ArrowUp':
+ entity.set_xy(x, y === null ? null : y - 1);
+ break;
+ case 'ArrowDown':
+ entity.set_xy(x, y === null ? null : y + 1);
+ break;
+ }
+ }
}
}
};
@@ -366,7 +413,9 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
.set_text_color(beestat.style.color.gray.light)
.set_background_color(beestat.style.color.bluegray.base)
.set_background_hover_color(beestat.style.color.bluegray.light)
- .addEventListener('click', this.add_room_.bind(this))
+ .addEventListener('click', function() {
+ self.add_room_();
+ })
);
// Remove room
@@ -492,18 +541,32 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
this.button_group_floors_ = new beestat.component.tile_group();
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
- floor_plan.data.groups.forEach(function(group) {
+
+ const sorted_groups = Object.values(floor_plan.data.groups)
+ .sort(function(a, b) {
+ return a.elevation > b.elevation;
+ });
+
+ let icon_number = 1;
+ sorted_groups.forEach(function(group) {
const button = new beestat.component.tile()
.set_title(group.name)
.set_text_hover_color(beestat.style.color.lightblue.light)
.set_text_color(beestat.style.color.lightblue.base);
+ let icon;
+ if (group.elevation < 0) {
+ icon = 'alpha_b';
+ } else {
+ icon = 'numeric_' + icon_number++;
+ }
+
if (group === self.state_.active_group) {
button
- .set_icon(group.icon + '_box');
+ .set_icon(icon + '_box');
} else {
button
- .set_icon(group.icon)
+ .set_icon(icon)
.addEventListener('click', function() {
if (self.state_.active_room_entity !== undefined) {
self.state_.active_room_entity.set_active(false);
@@ -557,32 +620,57 @@ beestat.component.floor_plan.prototype.toggle_snapping_ = function() {
/**
* Add a new room.
+ *
+ * @param {object} room Optional room to copy from.
*/
-beestat.component.floor_plan.prototype.add_room_ = function() {
- const new_room_size = 120;
+beestat.component.floor_plan.prototype.add_room_ = function(room) {
const svg_view_box = this.view_box_;
- const new_room = {
- 'x': svg_view_box.x + (svg_view_box.width / 2) - (new_room_size / 2),
- 'y': svg_view_box.y + (svg_view_box.height / 2) - (new_room_size / 2),
- 'points': [
- {
- 'x': 0,
- 'y': 0
- },
- {
- 'x': new_room_size,
- 'y': 0
- },
- {
- 'x': new_room_size,
- 'y': new_room_size
- },
- {
- 'x': 0,
- 'y': new_room_size
- }
- ]
- };
+
+ let new_room;
+ if (room === undefined) {
+ const new_room_size = 120;
+ new_room = {
+ 'x': svg_view_box.x + (svg_view_box.width / 2) - (new_room_size / 2),
+ 'y': svg_view_box.y + (svg_view_box.height / 2) - (new_room_size / 2),
+ 'points': [
+ {
+ 'x': 0,
+ 'y': 0
+ },
+ {
+ 'x': new_room_size,
+ 'y': 0
+ },
+ {
+ 'x': new_room_size,
+ 'y': new_room_size
+ },
+ {
+ 'x': 0,
+ 'y': new_room_size
+ }
+ ]
+ };
+ } else {
+ let min_x = Infinity;
+ let max_x = -Infinity;
+ let min_y = Infinity;
+ let max_y = -Infinity;
+
+ room.points.forEach(function(point) {
+ min_x = Math.min(room.x + point.x, min_x);
+ max_x = Math.max(room.x + point.x, max_x);
+ min_y = Math.min(room.y + point.y, min_y);
+ max_y = Math.max(room.y + point.y, max_y);
+ });
+
+ new_room = {
+ 'x': svg_view_box.x + (svg_view_box.width / 2) - ((max_x - min_x) / 2),
+ 'y': svg_view_box.y + (svg_view_box.height / 2) - ((max_y - min_y) / 2),
+ 'points': beestat.clone(room.points)
+ };
+ }
+
this.state_.active_group.rooms.push(new_room);
this.state_.active_room = new_room;
diff --git a/js/component/floor_plan_entity/point.js b/js/component/floor_plan_entity/point.js
index 259ba25..6f62465 100644
--- a/js/component/floor_plan_entity/point.js
+++ b/js/component/floor_plan_entity/point.js
@@ -146,6 +146,8 @@ beestat.component.floor_plan_entity.point.prototype.set_xy = function(x, y) {
this.update_rect_();
+ this.dispatchEvent('update');
+
return this;
};
@@ -243,8 +245,6 @@ beestat.component.floor_plan_entity.point.prototype.after_mousemove_handler_ = f
desired_x,
desired_y
);
-
- this.dispatchEvent('update');
};
/**
@@ -358,3 +358,21 @@ beestat.component.floor_plan_entity.point.prototype.set_active = function(active
return this;
};
+
+/**
+ * Get X
+ *
+ * @return {number} x
+ */
+beestat.component.floor_plan_entity.point.prototype.get_x = function() {
+ return this.point_.x;
+};
+
+/**
+ * Get Y
+ *
+ * @return {number} y
+ */
+beestat.component.floor_plan_entity.point.prototype.get_y = function() {
+ return this.point_.y;
+};
diff --git a/js/component/floor_plan_entity/room.js b/js/component/floor_plan_entity/room.js
index 89ead7a..6e5f6ed 100644
--- a/js/component/floor_plan_entity/room.js
+++ b/js/component/floor_plan_entity/room.js
@@ -114,7 +114,7 @@ beestat.component.floor_plan_entity.prototype.decorate_points_ = function(parent
point_entity.addEventListener('update', function() {
self.update_polygon_();
self.update_walls_();
- // self.dispatchEvent('update');
+ self.dispatchEvent('update');
});
// When a point is done moving normalize the points
@@ -178,6 +178,7 @@ beestat.component.floor_plan_entity.prototype.decorate_walls_ = function(parent)
self.update_polygon_();
self.update_points_();
self.update_walls_();
+ self.dispatchEvent('update');
});
// Clear any active points on drag start.
@@ -381,6 +382,8 @@ beestat.component.floor_plan_entity.room.prototype.set_xy = function(x, y) {
this.room_.x = Math.round(clamped_x);
this.room_.y = Math.round(clamped_y);
+ this.dispatchEvent('update');
+
return beestat.component.floor_plan_entity.prototype.set_xy.apply(
this,
[
diff --git a/js/component/floor_plan_entity/wall.js b/js/component/floor_plan_entity/wall.js
index bef694c..868b7d1 100644
--- a/js/component/floor_plan_entity/wall.js
+++ b/js/component/floor_plan_entity/wall.js
@@ -326,6 +326,8 @@ beestat.component.floor_plan_entity.wall.prototype.set_xy = function(x, y) {
this.point_2_.y = Math.round(clamped_y - this.room_.get_y());
}
+ this.dispatchEvent('update');
+
return this;
};
@@ -396,8 +398,6 @@ beestat.component.floor_plan_entity.wall.prototype.after_mousemove_handler_ = fu
desired_y
);
}
-
- this.dispatchEvent('update');
};
/**
@@ -577,3 +577,21 @@ beestat.component.floor_plan_entity.wall.prototype.set_active = function(active)
return this;
};
+
+/**
+ * Get X
+ *
+ * @return {number} x
+ */
+beestat.component.floor_plan_entity.wall.prototype.get_x = function() {
+ return this.is_vertical_() === true ? this.point_1_.x : null;
+};
+
+/**
+ * Get Y
+ *
+ * @return {number} y
+ */
+beestat.component.floor_plan_entity.wall.prototype.get_y = function() {
+ return this.is_horizontal_() === true ? this.point_1_.y : null;
+};
diff --git a/js/component/modal/create_floor_plan.js b/js/component/modal/create_floor_plan.js
index 8b33e45..4074a68 100644
--- a/js/component/modal/create_floor_plan.js
+++ b/js/component/modal/create_floor_plan.js
@@ -21,7 +21,7 @@ beestat.component.modal.create_floor_plan.prototype.decorate_contents_ = functio
const self = this;
const thermostat = beestat.cache.thermostat[this.thermostat_id_];
- parent.appendChild($.createElement('p').innerHTML('Describe your home to help create this floor plan. You can change these values later.'));
+ parent.appendChild($.createElement('p').innerHTML('Describe your home to help create this floor plan.'));
// Name
(new beestat.component.title('Give your floor plan a name')).render(parent);
@@ -46,7 +46,12 @@ beestat.component.modal.create_floor_plan.prototype.decorate_contents_ = functio
}
// Floor count
- (new beestat.component.title('How many floors does your home have?')).render(parent);
+ const floor_container = document.createElement('div');
+ floor_container.style.marginBottom = `${beestat.style.size.gutter}px`;
+ parent.appendChild(floor_container);
+
+ (new beestat.component.title('How many floors does your home have?'))
+ .render($(floor_container));
const floor_count_input = new beestat.component.input.text()
.set_icon('layers')
@@ -57,7 +62,7 @@ beestat.component.modal.create_floor_plan.prototype.decorate_contents_ = functio
'type': 'integer',
'required': true
})
- .render(parent);
+ .render($(floor_container));
floor_count_input.addEventListener('change', function() {
self.state_.floor_count = floor_count_input.get_value();
@@ -76,7 +81,7 @@ beestat.component.modal.create_floor_plan.prototype.decorate_contents_ = functio
// Basement
const basement_checkbox = new beestat.component.input.checkbox()
.set_label('One of these floors is a basement')
- .render(parent);
+ .render($(floor_container));
basement_checkbox.addEventListener('change', function() {
self.state_.basement = basement_checkbox.get_checked();
@@ -94,7 +99,6 @@ beestat.component.modal.create_floor_plan.prototype.decorate_contents_ = functio
.set_maxlength(2)
.set_requirements({
'min_value': 1,
- 'max_value': 24,
'type': 'integer',
'required': true
})
@@ -108,7 +112,7 @@ beestat.component.modal.create_floor_plan.prototype.decorate_contents_ = functio
if (self.state_.height !== undefined) {
height_input.set_value(self.state_.height);
} else if (self.state_.error.height !== true) {
- height_input.set_value(9);
+ height_input.set_value(8);
}
// Address
@@ -116,7 +120,7 @@ beestat.component.modal.create_floor_plan.prototype.decorate_contents_ = functio
parent.appendChild($.createElement('p').innerHTML('Addresses are pulled directly from your ecobee data.'));
const radio_group = new beestat.component.radio_group();
- const addresses = $.values(beestat.cache.address);
+ const addresses = Object.values(beestat.cache.address);
addresses.forEach(function(address) {
if (
address.normalized !== null &&
@@ -234,7 +238,6 @@ beestat.component.modal.create_floor_plan.prototype.get_buttons_ = function() {
];
for (let i = 0; i < self.state_.floor_count; i++) {
attributes.data.groups.push({
- 'icon': floor === 0 ? 'alpha_b' : ('numeric_' + floor),
'name': floor === 0 ? 'Basement' : (ordinals[floor - 1] + ' Floor'),
'elevation': elevation,
'height': self.state_.height * 12,
diff --git a/js/component/modal/delete_floor_plan.js b/js/component/modal/delete_floor_plan.js
index 5a673f8..e14932a 100644
--- a/js/component/modal/delete_floor_plan.js
+++ b/js/component/modal/delete_floor_plan.js
@@ -16,21 +16,14 @@ beestat.extend(beestat.component.modal.delete_floor_plan, beestat.component.moda
* @param {rocket.Elements} parent
*/
beestat.component.modal.delete_floor_plan.prototype.decorate_contents_ = function(parent) {
- parent.appendChild(
- $.createElement('p').innerHTML(
- 'Are you sure you want to delete this floor plan?'
- )
- );
+ const p = document.createElement('p');
+ p.innerText = 'Are you sure you want to delete this floor plan?';
+ parent.appendChild(p);
- const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
- floor_plan.data.groups.forEach(function(group) {
- parent.appendChild(
- $.createElement('div')
- .innerHTML(
- group.name + ': ' + group.rooms.length + ' room' + (group.rooms.length === 1 ? '' : 's')
- )
- );
- });
+ new beestat.component.tile.floor_plan(this.floor_plan_id_)
+ .set_background_color(beestat.style.color.bluegray.base)
+ .set_text_color('#fff')
+ .render(parent);
};
/**
diff --git a/js/component/modal/update_floor_plan.js b/js/component/modal/update_floor_plan.js
new file mode 100644
index 0000000..56559b0
--- /dev/null
+++ b/js/component/modal/update_floor_plan.js
@@ -0,0 +1,232 @@
+/**
+ * Update a floor plan.
+ *
+ * @param {integer} floor_plan_id
+ */
+beestat.component.modal.update_floor_plan = function(floor_plan_id) {
+ this.floor_plan_id_ = floor_plan_id;
+
+ beestat.component.modal.apply(this, arguments);
+
+ this.state_.error = {};
+};
+beestat.extend(beestat.component.modal.update_floor_plan, beestat.component.modal);
+
+/**
+ * Decorate
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.modal.update_floor_plan.prototype.decorate_contents_ = function(parent) {
+ const self = this;
+
+ const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
+
+ parent.appendChild($.createElement('p').innerHTML('Make changes to your floor plan below.'));
+
+ // Name
+ (new beestat.component.title('Give your floor plan a name')).render(parent);
+
+ const name_input = new beestat.component.input.text()
+ .set_icon('label')
+ .set_maxlength(255)
+ .set_requirements({
+ 'required': true
+ })
+ .render(parent);
+
+ name_input.addEventListener('change', function() {
+ self.state_.name = name_input.get_value();
+ self.state_.error.name = !name_input.meets_requirements();
+ });
+
+ if (self.state_.name !== undefined) {
+ name_input.set_value(self.state_.name);
+ } else if (self.state_.error.name !== true) {
+ name_input.set_value(floor_plan.name);
+ }
+
+ // Floors
+ (new beestat.component.title('Floors')).render(parent);
+ parent.appendChild($.createElement('p').innerHTML('To add or remove floors, create a new floor plan. Change floor settings like name, elevation, and ceiling height on the editor.'));
+
+ const grid = document.createElement('div');
+
+ Object.assign(grid.style, {
+ 'display': 'grid',
+ 'grid-template-columns': 'repeat(auto-fit, minmax(150px, 1fr))',
+ 'column-gap': `${beestat.style.size.gutter}px`,
+ 'row-gap': `${beestat.style.size.gutter}px`,
+ 'margin-bottom': `${beestat.style.size.gutter}px`
+ });
+
+ parent.appendChild(grid);
+
+ const sorted_groups = Object.values(floor_plan.data.groups)
+ .sort(function(a, b) {
+ return a.elevation > b.elevation;
+ });
+
+ sorted_groups.forEach(function(group) {
+ new beestat.component.tile.floor_plan_group(group)
+ .set_background_color(beestat.style.color.bluegray.base)
+ .set_text_color('#fff')
+ .set_display('block')
+ .render($(grid));
+ });
+
+ // Address
+ (new beestat.component.title('What is the address for this home?')).render(parent);
+ parent.appendChild($.createElement('p').innerHTML('Addresses are pulled directly from your ecobee data.'));
+
+ const radio_group = new beestat.component.radio_group();
+ const addresses = Object.values(beestat.cache.address);
+ addresses.forEach(function(address) {
+ if (
+ address.normalized !== null &&
+ address.normalized.metadata !== undefined &&
+ address.normalized.metadata.latitude !== undefined &&
+ address.normalized.metadata.latitude !== null &&
+ address.normalized.metadata.longitude !== undefined &&
+ address.normalized.metadata.longitude !== null
+ ) {
+ const address_parts = [
+ address.normalized.components.primary_number,
+ address.normalized.components.street_predirection,
+ address.normalized.components.street_name,
+ address.normalized.components.street_suffix,
+ address.normalized.components.city_name + ',',
+ address.normalized.components.state_abbreviation,
+ address.normalized.components.zipcode
+ ];
+
+ let radio = new beestat.component.input.radio()
+ .set_label(address_parts.join(' '))
+ .set_value(address.address_id);
+
+ if (address.address_id === floor_plan.address_id) {
+ radio.set_checked(true);
+ self.state_.address_id = Number(address.address_id);
+ }
+
+ radio_group.add_radio(radio);
+ }
+ });
+
+ radio_group.add_radio(
+ new beestat.component.input.radio()
+ .set_label('Not Listed')
+ .set_checked(floor_plan.address_id === null)
+ );
+
+ radio_group.addEventListener('change', function() {
+ if (radio_group.get_value() === undefined) {
+ delete self.state_.address_id;
+ } else {
+ self.state_.address_id = Number(radio_group.get_value());
+ }
+ });
+
+ radio_group.render(parent);
+
+ this.decorate_error_(parent);
+};
+
+/**
+ * Get title.
+ *
+ * @return {string} The title.
+ */
+beestat.component.modal.update_floor_plan.prototype.get_title_ = function() {
+ return 'Edit Floor Plan';
+};
+
+/**
+ * Get the buttons that go on the bottom of this modal.
+ *
+ * @return {[beestat.component.button]} The buttons.
+ */
+beestat.component.modal.update_floor_plan.prototype.get_buttons_ = function() {
+ const self = this;
+
+ const 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_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('Update Floor Plan')
+ .addEventListener('click', function() {
+ // Fail if there are errors.
+ if (
+ self.state_.error.name === true
+ ) {
+ self.rerender();
+ return;
+ }
+
+ const attributes = {
+ 'floor_plan_id': self.floor_plan_id_,
+ 'name': self.state_.name,
+ 'address_id': self.state_.address_id === undefined ? null : self.state_.address_id
+ };
+
+ self.dispose();
+ new beestat.api()
+ .add_call(
+ 'floor_plan',
+ 'update',
+ {
+ 'attributes': attributes
+ },
+ 'update_floor_plan'
+ )
+ .add_call(
+ 'floor_plan',
+ 'read_id',
+ {},
+ 'floor_plan'
+ )
+ .set_callback(function(response) {
+ beestat.cache.set('floor_plan', response.floor_plan);
+ })
+ .send();
+ });
+
+ return [
+ cancel,
+ save
+ ];
+};
+
+/**
+ * Decorate the error area.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.modal.update_floor_plan.prototype.decorate_error_ = function(parent) {
+ let has_error = false;
+
+ var div = $.createElement('div').style({
+ 'background': beestat.style.color.red.base,
+ 'color': '#fff',
+ 'border-radius': beestat.style.size.border_radius,
+ 'padding': beestat.style.size.gutter
+ });
+
+ if (this.state_.error.name === true) {
+ div.appendChild($.createElement('div').innerText('Name is required.'));
+ has_error = true;
+ }
+
+ if (has_error === true) {
+ parent.appendChild(div);
+ }
+};
diff --git a/js/component/tile.js b/js/component/tile.js
new file mode 100644
index 0000000..3cae5f3
--- /dev/null
+++ b/js/component/tile.js
@@ -0,0 +1,419 @@
+/**
+ * A block with an optional icon and up to two lines of text.
+ */
+beestat.component.tile = function() {
+ beestat.component.apply(this, arguments);
+};
+beestat.extend(beestat.component.tile, beestat.component);
+
+/**
+ * Decorate
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.tile.prototype.decorate_ = function(parent) {
+ const self = this;
+
+ const background_color = this.background_color_ || 'none';
+ const text_color = this.text_color_ || '#fff';
+ const tabbable = this.tabbable_ || false;
+ const display = this.display_ === 'block' ? 'flex' : 'inline-flex';
+ let border_radius;
+ if (this.type_ === 'pill') {
+ border_radius = (this.get_size_() === 'large' ? 48 : 32);
+ } else {
+ border_radius = beestat.style.size.border_radius;
+ }
+
+ this.container_ = document.createElement('div');
+
+ Object.assign(this.container_.style, {
+ 'background': background_color,
+ 'border-radius': `${border_radius}px`,
+ 'height': `${(this.get_size_() === 'large' ? 48 : 32)}px`,
+ 'display': display,
+ 'align-items': 'center',
+ 'color': text_color,
+ 'user-select': 'none',
+ 'transition': 'color 200ms ease, background 200ms ease',
+ 'outline-offset': '1px',
+ 'text-align': 'left'
+ });
+
+ parent.appendChild(this.container_);
+
+ // Padding. Basically for icon only make it a nice square button.
+ if (this.get_text_() === undefined) {
+ Object.assign(this.container_.style, {
+ 'width': `${(this.get_size_() === 'large' ? 48 : 32)}px`,
+ 'justify-content': 'center'
+ });
+ } else {
+ Object.assign(this.container_.style, {
+ 'padding-left': `${(beestat.style.size.gutter / 2)}px`,
+ 'padding-right': `${(beestat.style.size.gutter / 2)}px`,
+ });
+ }
+
+ // Tabbable
+ if (tabbable === true) {
+ this.container_.setAttribute('tabIndex', '0');
+ }
+
+ // Title
+ if (this.title_ !== undefined) {
+ this.container_.setAttribute('title', this.title_);
+ }
+
+ // Hover
+ if (
+ this.text_hover_color_ !== undefined ||
+ this.background_hover_color_ !== undefined
+ ) {
+ this.container_.style.cursor = 'pointer';
+
+ const mouseenter_style = {};
+ if (this.text_hover_color_ !== undefined) {
+ mouseenter_style.color = this.text_hover_color_;
+ }
+ if (this.background_hover_color_ !== undefined) {
+ mouseenter_style.background = this.background_hover_color_;
+ }
+
+ const mouseleave_style = {};
+ mouseleave_style.color =
+ (this.text_color_ !== undefined) ? this.text_color_ : '';
+ mouseleave_style.background =
+ (this.background_color_ !== undefined) ? this.background_color_ : '';
+
+ this.container_.addEventListener('mouseenter', function() {
+ Object.assign(self.container_.style, mouseenter_style);
+ });
+ this.container_.addEventListener('mouseleave', function() {
+ Object.assign(self.container_.style, mouseleave_style);
+ });
+ }
+
+ // Focus
+ if (tabbable === true) {
+ this.container_.addEventListener('focus', function() {
+ self.container_.style.outline = '2px solid #fff';
+ });
+ this.container_.addEventListener('blur', function() {
+ self.container_.style.outline = 'none';
+ });
+ this.container_.addEventListener('keydown', function(e) {
+ if (
+ e.key === 'Enter' ||
+ e.key === ' '
+ ) {
+ self.dispatchEvent(new window.Event('click'));
+ }
+ });
+ }
+
+ // Left and right container
+ const left_container = document.createElement('div');
+ this.decorate_left_(left_container);
+ this.container_.appendChild(left_container);
+
+ const right_container = document.createElement('div');
+ this.decorate_right_(right_container);
+ this.container_.appendChild(right_container);
+
+ // Events
+ this.container_.addEventListener('click', function() {
+ self.dispatchEvent('click');
+ });
+
+ this.container_.addEventListener('mousedown', function() {
+ self.dispatchEvent('mousedown');
+ });
+};
+
+beestat.component.tile.prototype.decorate_left_ = function(parent) {
+ if (this.get_icon_() !== undefined) {
+ const icon_container = document.createElement('div');
+ if (this.get_text_() !== undefined) {
+ icon_container.style.marginRight = (beestat.style.size.gutter / 2) + 'px';
+ }
+ parent.appendChild(icon_container);
+
+ new beestat.component.icon(this.get_icon_())
+ .set_bubble_text(this.bubble_text_)
+ .set_bubble_color(this.bubble_color_)
+ .set_size(this.get_size_() === 'large' ? 32 : 24)
+ .render($(icon_container));
+ }
+};
+
+/**
+ * Decorate the right side of the tile.
+ *
+ * @param {HTMLElement} parent
+ */
+beestat.component.tile.prototype.decorate_right_ = function(parent) {
+ if (Array.isArray(this.get_text_()) === true) {
+ const line_1_container = document.createElement('div');
+ line_1_container.innerText = this.get_text_()[0];
+ line_1_container.style.fontWeight = beestat.style.font_weight.bold;
+ parent.appendChild(line_1_container);
+
+ const line_2_container = document.createElement('div');
+ line_2_container.innerText = this.get_text_()[1];
+ line_2_container.style.fontWeight = beestat.style.font_weight.light;
+ parent.appendChild(line_2_container);
+ } else if (this.get_text_() !== undefined) {
+ const text_container = document.createElement('div');
+ text_container.innerText = this.get_text_();
+ text_container.style.fontWeight = beestat.style.font_weight.normal;
+ parent.appendChild(text_container);
+ }
+};
+
+/**
+ * Set the icon.
+ *
+ * @param {string} icon
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_icon = function(icon) {
+ this.icon_ = icon;
+
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+
+ return this;
+};
+
+/**
+ * Set the size. Default is small.
+ *
+ * @param {string} size large|small
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_size = function(size) {
+ this.size_ = size;
+
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+
+ return this;
+};
+
+/**
+ * Get the size of this tile.
+ *
+ * @return {string} The size of this tile.
+ */
+beestat.component.tile.prototype.get_size_ = function() {
+ return this.size_;
+};
+
+/**
+ * Get the icon for this tile.
+ *
+ * @return {string} The icon.
+ */
+beestat.component.tile.prototype.get_icon_ = function() {
+ return this.icon_;
+};
+
+/**
+ * Set the text of the button.
+ *
+ * @param {string|array} text A single string or array of strings. If an array is passed multiple lines of text will be shown.
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_text = function(text) {
+ this.text_ = text;
+
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+
+ return this;
+};
+
+/**
+ * Get the text for this tile.
+ *
+ * @return {string} The text for this tile.
+ */
+beestat.component.tile.prototype.get_text_ = function() {
+ return this.text_;
+};
+
+/**
+ * Set the background color.
+ *
+ * @param {string} background_color
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_background_color = function(background_color) {
+ this.background_color_ = background_color;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
+
+/**
+ * Set the text color.
+ *
+ * @param {string} text_color
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_text_color = function(text_color) {
+ this.text_color_ = text_color;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
+
+/**
+ * Set the background hover color.
+ *
+ * @param {string} background_hover_color
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_background_hover_color = function(background_hover_color) {
+ this.background_hover_color_ = background_hover_color;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
+
+/**
+ * Set the text hover color.
+ *
+ * @param {string} text_hover_color
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_text_hover_color = function(text_hover_color) {
+ this.text_hover_color_ = text_hover_color;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
+
+/**
+ * Set the title for the tile.
+ *
+ * @param {string} title
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_title = function(title) {
+ this.title_ = title;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
+
+/**
+ * Get the container for this tile.
+ *
+ * @return {array} The container for this tile.
+ */
+beestat.component.tile.prototype.get_container = function() {
+ return this.container_;
+};
+
+/**
+ * Set whether or not this is tabbable. Default false.
+ *
+ * @param {boolean} tabbable
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_tabbable = function(tabbable) {
+ this.tabbable_ = tabbable;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
+
+/**
+ * Set display mode.
+ *
+ * @param {string} display inline|block; default inline.
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_display = function(display) {
+ this.display_ = display;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
+
+/**
+ * Set the type.
+ *
+ * @param {string} type Valid value is "pill" for now.
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_type = function(type) {
+ this.type_ = type;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
+
+/**
+ * Do the normal event listener stuff. Only exists for chaining purposes.
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.addEventListener = function() {
+ rocket.EventTarget.prototype.addEventListener.apply(this, arguments);
+ return this;
+};
+
+/**
+ * Set the text of the bubble.
+ *
+ * @param {string} bubble_text
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_bubble_text = function(bubble_text) {
+ this.bubble_text_ = bubble_text;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
+
+/**
+ * Set the color of the bubble.
+ *
+ * @param {string} bubble_color
+ *
+ * @return {beestat.component.tile} This.
+ */
+beestat.component.tile.prototype.set_bubble_color = function(bubble_color) {
+ this.bubble_color_ = bubble_color;
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ return this;
+};
diff --git a/js/component/tile/floor_plan.js b/js/component/tile/floor_plan.js
new file mode 100644
index 0000000..1ed5486
--- /dev/null
+++ b/js/component/tile/floor_plan.js
@@ -0,0 +1,48 @@
+/**
+ * A tile representing a floor plan.
+ *
+ * @param {integer} floor_plan_id
+ */
+beestat.component.tile.floor_plan = function(floor_plan_id) {
+ this.floor_plan_id_ = floor_plan_id;
+
+ beestat.component.apply(this, arguments);
+};
+beestat.extend(beestat.component.tile.floor_plan, beestat.component.tile);
+
+/**
+ * Get the icon for this tile.
+ *
+ * @return {string} The icon.
+ */
+beestat.component.tile.floor_plan.prototype.get_icon_ = function() {
+ return 'floor_plan';
+};
+
+/**
+ * Get the text for this tile.
+ *
+ * @return {string} The first line of text.
+ */
+beestat.component.tile.floor_plan.prototype.get_text_ = function() {
+ const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
+
+ const line_2_parts = [];
+ let floor_count = floor_plan.data.groups.length;
+ line_2_parts.push(floor_count + (floor_count === 1 ? ' Floor' : ' Floors'));
+ line_2_parts.push(beestat.floor_plan.get_area(this.floor_plan_id_).toLocaleString() + ' sqft');
+
+ return [
+ floor_plan.name,
+ line_2_parts.join(' • ')
+ ];
+};
+
+/**
+ * Get the size of this tile.
+ *
+ * @return {string} The size of this tile.
+ */
+beestat.component.tile.floor_plan.prototype.get_size_ = function() {
+ return 'large';
+};
diff --git a/js/component/tile/floor_plan_group.js b/js/component/tile/floor_plan_group.js
new file mode 100644
index 0000000..36c424e
--- /dev/null
+++ b/js/component/tile/floor_plan_group.js
@@ -0,0 +1,46 @@
+/**
+ * A tile representing a floor plan group.
+ *
+ * @param {object} floor_plan_group
+ */
+beestat.component.tile.floor_plan_group = function(floor_plan_group) {
+ this.floor_plan_group_ = floor_plan_group;
+
+ beestat.component.apply(this, arguments);
+};
+beestat.extend(beestat.component.tile.floor_plan_group, beestat.component.tile);
+
+/**
+ * Get the icon for this tile.
+ *
+ * @return {string} The icon.
+ */
+beestat.component.tile.floor_plan_group.prototype.get_icon_ = function() {
+ return 'layers';
+};
+
+/**
+ * Get the text for this tile.
+ *
+ * @return {string} The first line of text.
+ */
+beestat.component.tile.floor_plan_group.prototype.get_text_ = function() {
+ const line_2_parts = [];
+ let room_count = this.floor_plan_group_.rooms.length;
+ line_2_parts.push(room_count + (room_count === 1 ? ' Room' : ' Rooms'));
+ line_2_parts.push(beestat.floor_plan.get_area_group(this.floor_plan_group_).toLocaleString() + ' sqft');
+
+ return [
+ this.floor_plan_group_.name,
+ line_2_parts.join(' • ')
+ ];
+};
+
+/**
+ * Get the size of this tile.
+ *
+ * @return {string} The size of this tile.
+ */
+beestat.component.tile.floor_plan_group.prototype.get_size_ = function() {
+ return 'large';
+};
diff --git a/js/component/tile/thermostat.js b/js/component/tile/thermostat.js
new file mode 100644
index 0000000..d3b8193
--- /dev/null
+++ b/js/component/tile/thermostat.js
@@ -0,0 +1,49 @@
+/**
+ * A tile representing a thermostat.
+ *
+ * @param {integer} thermostat_id
+ */
+beestat.component.tile.thermostat = function(thermostat_id) {
+ this.thermostat_id_ = thermostat_id;
+
+ beestat.component.apply(this, arguments);
+};
+beestat.extend(beestat.component.tile.thermostat, beestat.component.tile);
+
+/**
+ * Get the icon for this tile.
+ *
+ * @return {string} The icon.
+ */
+beestat.component.tile.thermostat.prototype.get_icon_ = function() {
+ return 'thermostat';
+};
+
+/**
+ * Get the text for this tile.
+ *
+ * @return {string} The first line of text.
+ */
+beestat.component.tile.thermostat.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.prototype.get_size_ = function() {
+ return 'large';
+};
diff --git a/js/component/title.js b/js/component/title.js
index 2e194bc..2019430 100644
--- a/js/component/title.js
+++ b/js/component/title.js
@@ -19,7 +19,6 @@ beestat.component.title.prototype.decorate_ = function(parent) {
.style({
'font-size': beestat.style.font_size.normal,
'font-weight': beestat.style.font_weight.bold,
- 'margin-top': (beestat.style.size.gutter),
'margin-bottom': (beestat.style.size.gutter / 2)
})
.innerText(this.title_);
diff --git a/js/js.php b/js/js.php
index c2b13e9..ae9e3cb 100755
--- a/js/js.php
+++ b/js/js.php
@@ -116,6 +116,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;
@@ -126,6 +127,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;