Code refactoring into multiple manageble files
This commit is contained in:
parent
07ec766383
commit
1da473d52a
7 changed files with 1518 additions and 1054 deletions
133
src/config-manager.js
Normal file
133
src/config-manager.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { DEFAULT_ACTIVITIES, DEFAULT_CONFIG } from './constants.js';
|
||||
|
||||
/**
|
||||
* Manages card configuration, including merging defaults with user config
|
||||
*/
|
||||
export class ConfigManager {
|
||||
constructor() {
|
||||
this._config = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and sets the configuration
|
||||
* @param {Object} config - User configuration
|
||||
* @throws {Error} If entities are not defined
|
||||
*/
|
||||
setConfig(config) {
|
||||
if (!config.entities || !Array.isArray(config.entities)) {
|
||||
throw new Error('You need to define entities');
|
||||
}
|
||||
|
||||
const mergedActivities = this._mergeActivities(config.activities);
|
||||
|
||||
this._config = {
|
||||
entities: config.entities,
|
||||
map_provider: config.map_provider || DEFAULT_CONFIG.map_provider,
|
||||
google_api_key: config.google_api_key || DEFAULT_CONFIG.google_api_key,
|
||||
map_type: config.map_type || DEFAULT_CONFIG.map_type,
|
||||
default_zoom: config.default_zoom || DEFAULT_CONFIG.default_zoom,
|
||||
update_interval: config.update_interval || DEFAULT_CONFIG.update_interval,
|
||||
marker_border_radius: config.marker_border_radius || DEFAULT_CONFIG.marker_border_radius,
|
||||
badge_border_radius: config.badge_border_radius || DEFAULT_CONFIG.badge_border_radius,
|
||||
zones: config.zones || DEFAULT_CONFIG.zones,
|
||||
activities: mergedActivities,
|
||||
debug: config.debug || DEFAULT_CONFIG.debug
|
||||
};
|
||||
|
||||
return this._config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges user activity colors with default activity icons and names
|
||||
* @param {Object} userActivities - User-provided activity configurations
|
||||
* @returns {Object} Merged activity configurations
|
||||
*/
|
||||
_mergeActivities(userActivities) {
|
||||
const activities = {};
|
||||
|
||||
Object.keys(DEFAULT_ACTIVITIES).forEach(key => {
|
||||
activities[key] = {
|
||||
icon: DEFAULT_ACTIVITIES[key].icon,
|
||||
name: DEFAULT_ACTIVITIES[key].name,
|
||||
color: (userActivities && userActivities[key] && userActivities[key].color)
|
||||
? userActivities[key].color
|
||||
: DEFAULT_ACTIVITIES[key].color
|
||||
};
|
||||
});
|
||||
|
||||
return activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if specific config properties have changed
|
||||
* @param {Object} oldConfig - Previous configuration
|
||||
* @param {Array<string>} properties - Properties to check
|
||||
* @returns {boolean} True if any property changed
|
||||
*/
|
||||
hasChanged(oldConfig, properties) {
|
||||
if (!oldConfig) return false;
|
||||
|
||||
return properties.some(prop => {
|
||||
return JSON.stringify(oldConfig[prop]) !== JSON.stringify(this._config[prop]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current configuration
|
||||
* @returns {Object} Current configuration
|
||||
*/
|
||||
getConfig() {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds URL parameters for the iframe
|
||||
* @returns {URLSearchParams} URL parameters
|
||||
*/
|
||||
buildIframeParams() {
|
||||
if (!this._config) {
|
||||
throw new Error('Configuration not set');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
provider: this._config.map_provider,
|
||||
apikey: this._config.google_api_key || '',
|
||||
maptype: this._config.map_type,
|
||||
zoom: this._config.default_zoom,
|
||||
mode: 'proxy',
|
||||
debug: this._config.debug ? '1' : '0'
|
||||
});
|
||||
|
||||
// Add entities
|
||||
const entitiesParam = this._config.entities
|
||||
.map(e => `${e.person}${e.activity ? ':' + e.activity : ''}`)
|
||||
.join(',');
|
||||
params.append('entities', entitiesParam);
|
||||
|
||||
// Add zones (only colors)
|
||||
const zonesParam = Object.entries(this._config.zones)
|
||||
.map(([state, config]) => `${state}:${encodeURIComponent(config.color)}`)
|
||||
.join(',');
|
||||
params.append('zones', zonesParam);
|
||||
|
||||
// Add activities
|
||||
const activitiesParam = Object.entries(this._config.activities)
|
||||
.map(([state, config]) => `${state}:${config.icon}:${encodeURIComponent(config.color)}`)
|
||||
.join(',');
|
||||
params.append('activities', activitiesParam);
|
||||
|
||||
// Add border radius
|
||||
params.append('marker_radius', encodeURIComponent(this._config.marker_border_radius));
|
||||
params.append('badge_radius', encodeURIComponent(this._config.badge_border_radius));
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a stub configuration for initial setup
|
||||
* @returns {Object} Stub configuration
|
||||
*/
|
||||
static getStubConfig() {
|
||||
return { ...DEFAULT_CONFIG };
|
||||
}
|
||||
}
|
||||
50
src/constants.js
Normal file
50
src/constants.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Default activity configurations
|
||||
* Maps activity types from Google/iOS activity detection to icons and friendly names
|
||||
*/
|
||||
export const DEFAULT_ACTIVITIES = {
|
||||
unknown: { icon: 'mdi-human-male', color: '#000000', name: 'Unknown' },
|
||||
still: { icon: 'mdi-human-male', color: '#000000', name: 'Still' },
|
||||
on_foot: { icon: 'mdi-walk', color: '#000000', name: 'On Foot' },
|
||||
walking: { icon: 'mdi-walk', color: '#000000', name: 'Walking' },
|
||||
running: { icon: 'mdi-run', color: '#000000', name: 'Running' },
|
||||
on_bicycle: { icon: 'mdi-bike', color: '#000000', name: 'Cycling' },
|
||||
in_vehicle: { icon: 'mdi-car', color: '#000000', name: 'In Vehicle' },
|
||||
in_road_vehicle: { icon: 'mdi-car', color: '#000000', name: 'In Vehicle' },
|
||||
in_four_wheeler_vehicle: { icon: 'mdi-car', color: '#000000', name: 'In Vehicle' },
|
||||
in_car: { icon: 'mdi-car', color: '#000000', name: 'In Vehicle' },
|
||||
Automotive: { icon: 'mdi-car', color: '#000000', name: 'In Vehicle' },
|
||||
in_rail_vehicle: { icon: 'mdi-train', color: '#000000', name: 'On Train' },
|
||||
in_bus: { icon: 'mdi-bus', color: '#000000', name: 'On Bus' },
|
||||
tilting: { icon: 'mdi-phone-rotate-landscape', color: '#000000', name: 'Tilting' }
|
||||
};
|
||||
|
||||
/**
|
||||
* Default zone configurations
|
||||
*/
|
||||
export const DEFAULT_ZONES = {
|
||||
home: { color: '#cef595' },
|
||||
not_home: { color: '#757575' }
|
||||
};
|
||||
|
||||
/**
|
||||
* Default card configuration
|
||||
*/
|
||||
export const DEFAULT_CONFIG = {
|
||||
entities: [],
|
||||
map_provider: 'osm',
|
||||
google_api_key: '',
|
||||
map_type: 'hybrid',
|
||||
default_zoom: 13,
|
||||
update_interval: 10, // in seconds
|
||||
marker_border_radius: '50%',
|
||||
badge_border_radius: '50%',
|
||||
debug: false,
|
||||
zones: DEFAULT_ZONES,
|
||||
activities: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Activities that should be hidden from the configuration UI
|
||||
*/
|
||||
export const HIDDEN_ACTIVITIES = ['Unknown'];
|
||||
248
src/editor-handlers.js
Normal file
248
src/editor-handlers.js
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/**
|
||||
* Handles event listeners for the configuration editor
|
||||
*/
|
||||
export class EditorHandlers {
|
||||
/**
|
||||
* Attaches all event listeners to the editor
|
||||
* @param {HTMLElement} element - Root element
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Function} onChange - Callback when config changes
|
||||
* @param {Function} onRender - Callback to trigger re-render
|
||||
*/
|
||||
static attachListeners(element, config, onChange, onRender) {
|
||||
this._attachMapProviderListeners(element, config, onChange);
|
||||
this._attachBasicConfigListeners(element, config, onChange);
|
||||
this._attachBorderRadiusListeners(element, config, onChange);
|
||||
this._attachEntityListeners(element, config, onChange, onRender);
|
||||
this._attachZoneListeners(element, config, onChange, onRender);
|
||||
this._attachActivityListeners(element, config, onChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches map provider listeners
|
||||
* @param {HTMLElement} element - Root element
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Function} onChange - Callback when config changes
|
||||
*/
|
||||
static _attachMapProviderListeners(element, config, onChange) {
|
||||
element.querySelector('#map_provider')?.addEventListener('selected', (e) => {
|
||||
config.map_provider = e.target.value;
|
||||
|
||||
// Show/hide Google Maps fields
|
||||
const googleApiKeyRow = element.querySelector('#google-api-key-row');
|
||||
const mapTypeRow = element.querySelector('#map-type-row');
|
||||
const display = e.target.value === 'google' ? 'block' : 'none';
|
||||
|
||||
if (googleApiKeyRow) googleApiKeyRow.style.display = display;
|
||||
if (mapTypeRow) mapTypeRow.style.display = display;
|
||||
|
||||
onChange();
|
||||
});
|
||||
|
||||
// Toggle API key visibility
|
||||
element.querySelector('#toggle-api-key-visibility')?.addEventListener('click', (e) => {
|
||||
const apiKeyField = element.querySelector('#google_api_key');
|
||||
const toggleIcon = e.currentTarget.querySelector('ha-icon');
|
||||
|
||||
if (apiKeyField) {
|
||||
const isPassword = apiKeyField.getAttribute('type') === 'password';
|
||||
apiKeyField.setAttribute('type', isPassword ? 'text' : 'password');
|
||||
toggleIcon.setAttribute('icon', isPassword ? 'mdi:eye-off' : 'mdi:eye');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches basic config listeners (API key, map type, zoom, etc.)
|
||||
* @param {HTMLElement} element - Root element
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Function} onChange - Callback when config changes
|
||||
*/
|
||||
static _attachBasicConfigListeners(element, config, onChange) {
|
||||
element.querySelector('#google_api_key')?.addEventListener('change', (e) => {
|
||||
config.google_api_key = e.target.value;
|
||||
onChange();
|
||||
});
|
||||
|
||||
element.querySelector('#map_type')?.addEventListener('selected', (e) => {
|
||||
config.map_type = e.target.value;
|
||||
onChange();
|
||||
});
|
||||
|
||||
element.querySelector('#default_zoom')?.addEventListener('change', (e) => {
|
||||
config.default_zoom = parseInt(e.target.value);
|
||||
onChange();
|
||||
});
|
||||
|
||||
element.querySelector('#update_interval')?.addEventListener('change', (e) => {
|
||||
config.update_interval = parseInt(e.target.value);
|
||||
onChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches border radius slider listeners
|
||||
* @param {HTMLElement} element - Root element
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Function} onChange - Callback when config changes
|
||||
*/
|
||||
static _attachBorderRadiusListeners(element, config, onChange) {
|
||||
// Marker border radius
|
||||
const markerSlider = element.querySelector('#marker_border_radius');
|
||||
const markerValueDisplay = element.querySelector('#marker-radius-value');
|
||||
|
||||
markerSlider?.addEventListener('input', (e) => {
|
||||
markerValueDisplay.textContent = e.target.value + '%';
|
||||
});
|
||||
|
||||
markerSlider?.addEventListener('change', (e) => {
|
||||
config.marker_border_radius = e.target.value + '%';
|
||||
onChange();
|
||||
});
|
||||
|
||||
// Badge border radius
|
||||
const badgeSlider = element.querySelector('#badge_border_radius');
|
||||
const badgeValueDisplay = element.querySelector('#badge-radius-value');
|
||||
|
||||
badgeSlider?.addEventListener('input', (e) => {
|
||||
badgeValueDisplay.textContent = e.target.value + '%';
|
||||
});
|
||||
|
||||
badgeSlider?.addEventListener('change', (e) => {
|
||||
config.badge_border_radius = e.target.value + '%';
|
||||
onChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches entity listeners
|
||||
* @param {HTMLElement} element - Root element
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Function} onChange - Callback when config changes
|
||||
* @param {Function} onRender - Callback to trigger re-render
|
||||
*/
|
||||
static _attachEntityListeners(element, config, onChange, onRender) {
|
||||
// Entity input fields
|
||||
element.querySelectorAll('input.entity-input[data-entity-idx]').forEach(el => {
|
||||
el.addEventListener('change', (e) => {
|
||||
const idx = parseInt(e.target.dataset.entityIdx);
|
||||
const field = e.target.dataset.entityField;
|
||||
|
||||
if (idx < config.entities.length) {
|
||||
config.entities[idx][field] = e.target.value || '';
|
||||
onChange();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete entity buttons
|
||||
element.querySelectorAll('[data-entity-delete]').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('ha-icon-button');
|
||||
if (button) {
|
||||
const idx = parseInt(button.dataset.entityDelete);
|
||||
config.entities.splice(idx, 1);
|
||||
onRender();
|
||||
onChange();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add entity button
|
||||
element.querySelector('#add-entity')?.addEventListener('click', () => {
|
||||
config.entities.push({ person: '', activity: '' });
|
||||
onRender();
|
||||
onChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches zone listeners
|
||||
* @param {HTMLElement} element - Root element
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Function} onChange - Callback when config changes
|
||||
* @param {Function} onRender - Callback to trigger re-render
|
||||
*/
|
||||
static _attachZoneListeners(element, config, onChange, onRender) {
|
||||
// Zone state name inputs
|
||||
element.querySelectorAll('input[data-zone-idx][data-zone-field="state"]').forEach(el => {
|
||||
el.addEventListener('change', (e) => {
|
||||
const idx = parseInt(e.target.dataset.zoneIdx);
|
||||
const zones = Object.entries(config.zones);
|
||||
|
||||
if (idx >= zones.length) return;
|
||||
|
||||
const [oldState, oldConfig] = zones[idx];
|
||||
const newValue = e.target.value;
|
||||
|
||||
if (newValue && oldState !== newValue) {
|
||||
delete config.zones[oldState];
|
||||
config.zones[newValue] = oldConfig;
|
||||
}
|
||||
|
||||
onChange();
|
||||
});
|
||||
});
|
||||
|
||||
// Zone color inputs
|
||||
element.querySelectorAll('input[data-zone-idx][data-zone-field="color"]').forEach(el => {
|
||||
el.addEventListener('change', (e) => {
|
||||
const idx = parseInt(e.target.dataset.zoneIdx);
|
||||
const zones = Object.entries(config.zones);
|
||||
|
||||
if (idx >= zones.length) return;
|
||||
|
||||
const [state, zoneConfig] = zones[idx];
|
||||
zoneConfig.color = e.target.value;
|
||||
onChange();
|
||||
});
|
||||
});
|
||||
|
||||
// Delete zone buttons
|
||||
element.querySelectorAll('[data-zone-delete]').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('ha-icon-button');
|
||||
if (button) {
|
||||
const idx = parseInt(button.dataset.zoneDelete);
|
||||
const zones = Object.entries(config.zones);
|
||||
const [state] = zones[idx];
|
||||
delete config.zones[state];
|
||||
onRender();
|
||||
onChange();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add zone button
|
||||
element.querySelector('#add-zone')?.addEventListener('click', () => {
|
||||
config.zones[''] = { color: '#757575' };
|
||||
onRender();
|
||||
onChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches activity listeners
|
||||
* @param {HTMLElement} element - Root element
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Function} onChange - Callback when config changes
|
||||
*/
|
||||
static _attachActivityListeners(element, config, onChange) {
|
||||
// Activity color inputs
|
||||
element.querySelectorAll('input[data-activity-states]').forEach(el => {
|
||||
el.addEventListener('change', (e) => {
|
||||
const states = e.target.dataset.activityStates.split(',');
|
||||
const newColor = e.target.value;
|
||||
|
||||
// Update color for all states in this group
|
||||
states.forEach(state => {
|
||||
if (config.activities[state]) {
|
||||
config.activities[state].color = newColor;
|
||||
}
|
||||
});
|
||||
|
||||
onChange();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
568
src/editor-ui.js
Normal file
568
src/editor-ui.js
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
import { HIDDEN_ACTIVITIES } from './constants.js';
|
||||
|
||||
/**
|
||||
* Generates HTML for the configuration editor
|
||||
*/
|
||||
export class EditorUI {
|
||||
/**
|
||||
* Generates the complete editor HTML
|
||||
* @param {Object} config - Current configuration
|
||||
* @param {Object} hass - Home Assistant instance
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static generateHTML(config, hass) {
|
||||
const entitiesHtml = this._generateEntitiesHTML(config, hass);
|
||||
const zonesHtml = this._generateZonesHTML(config);
|
||||
const activitiesHtml = this._generateActivitiesHTML(config);
|
||||
const datalists = this._generateDatalistsHTML(hass);
|
||||
|
||||
return `
|
||||
${this._generateStyles()}
|
||||
<div class="config-container">
|
||||
<div class="config-header">
|
||||
Map Badge Card Configuration
|
||||
</div>
|
||||
|
||||
${this._generateMapProviderSection(config)}
|
||||
${this._generateAppearanceSection(config)}
|
||||
${this._generateEntitiesSection(entitiesHtml, datalists)}
|
||||
${this._generateZonesSection(zonesHtml)}
|
||||
${this._generateActivitiesSection(activitiesHtml)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates styles for the editor
|
||||
* @returns {string} Style HTML
|
||||
*/
|
||||
static _generateStyles() {
|
||||
return `
|
||||
<style>
|
||||
.config-container {
|
||||
padding: 24px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.config-header {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 32px;
|
||||
color: var(--primary-text-color);
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 3px solid var(--primary-color);
|
||||
}
|
||||
.config-row {
|
||||
margin: 24px 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.config-row ha-textfield,
|
||||
.config-row ha-select {
|
||||
width: 100%;
|
||||
--mdc-theme-primary: var(--primary-color);
|
||||
}
|
||||
.config-note {
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--secondary-background-color);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--primary-color);
|
||||
}
|
||||
.config-section {
|
||||
margin: 32px 0;
|
||||
padding: 24px;
|
||||
background: var(--card-background-color);
|
||||
border-radius: 16px;
|
||||
border: 2px solid var(--divider-color);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.config-section:hover {
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.12);
|
||||
}
|
||||
.config-section-header {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
color: var(--primary-text-color);
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid var(--divider-color);
|
||||
}
|
||||
.config-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-end;
|
||||
padding: 12px;
|
||||
background: var(--secondary-background-color);
|
||||
border-radius: 12px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.config-item:hover {
|
||||
background: var(--divider-color);
|
||||
}
|
||||
.add-button {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
--mdc-theme-primary: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
ha-icon-button[data-entity-delete],
|
||||
ha-icon-button[data-zone-delete],
|
||||
ha-icon-button[data-activity-delete] {
|
||||
--mdc-icon-button-size: 40px;
|
||||
color: var(--error-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.input-wrapper label {
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.entity-input {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 14px;
|
||||
border: 2px solid var(--divider-color);
|
||||
border-radius: 8px;
|
||||
background: var(--card-background-color);
|
||||
color: var(--primary-text-color);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.entity-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(var(--rgb-primary-color), 0.1);
|
||||
}
|
||||
.entity-input:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.entity-input::placeholder {
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.radius-slider {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--divider-color);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.radius-slider::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--divider-color);
|
||||
}
|
||||
.radius-slider::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--divider-color);
|
||||
}
|
||||
.radius-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
cursor: grab;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
margin-top: -6px;
|
||||
}
|
||||
.radius-slider::-webkit-slider-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.radius-slider::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
cursor: grab;
|
||||
border: none;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
}
|
||||
.radius-slider::-moz-range-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
#toggle-api-key-visibility {
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
#toggle-api-key-visibility:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@media (min-width: 600px) {
|
||||
.config-row {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates map provider section HTML
|
||||
* @param {Object} config - Configuration object
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static _generateMapProviderSection(config) {
|
||||
const googleFieldsDisplay = config.map_provider === 'google' ? 'block' : 'none';
|
||||
|
||||
return `
|
||||
<div class="config-section">
|
||||
<div class="config-section-header">
|
||||
Map Provider Settings
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<ha-select
|
||||
id="map_provider"
|
||||
label="Map Provider"
|
||||
value="${config.map_provider || 'osm'}">
|
||||
<mwc-list-item value="osm">OpenStreetMap (No API Key Required)</mwc-list-item>
|
||||
<mwc-list-item value="google">Google Maps</mwc-list-item>
|
||||
</ha-select>
|
||||
<div class="config-note">OpenStreetMap is free and requires no authentication</div>
|
||||
</div>
|
||||
|
||||
<div class="config-row" id="google-api-key-row" style="display: ${googleFieldsDisplay}">
|
||||
<div style="position: relative;">
|
||||
<ha-textfield
|
||||
id="google_api_key"
|
||||
label="Google API Key"
|
||||
value="${config.google_api_key || ''}"
|
||||
placeholder="AIzaSy..."
|
||||
type="password">
|
||||
</ha-textfield>
|
||||
<ha-icon-button
|
||||
id="toggle-api-key-visibility"
|
||||
style="position: absolute; right: 0; top: 8px;">
|
||||
<ha-icon icon="mdi:eye"></ha-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
<div class="config-note">Required only for Google Maps - Get your API key from Google Cloud Console</div>
|
||||
</div>
|
||||
|
||||
<div class="config-row" id="map-type-row" style="display: ${googleFieldsDisplay}">
|
||||
<ha-select
|
||||
id="map_type"
|
||||
label="Map Type"
|
||||
value="${config.map_type || 'hybrid'}">
|
||||
<mwc-list-item value="hybrid">Hybrid</mwc-list-item>
|
||||
<mwc-list-item value="satellite">Satellite</mwc-list-item>
|
||||
<mwc-list-item value="roadmap">Roadmap</mwc-list-item>
|
||||
<mwc-list-item value="terrain">Terrain</mwc-list-item>
|
||||
</ha-select>
|
||||
<div class="config-note">Map type (Google Maps only)</div>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<ha-textfield
|
||||
id="default_zoom"
|
||||
label="Default Zoom"
|
||||
value="${config.default_zoom || 13}"
|
||||
type="number"
|
||||
min="1"
|
||||
max="21">
|
||||
</ha-textfield>
|
||||
<div class="config-note">1 = World view, 21 = Maximum zoom</div>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<ha-textfield
|
||||
id="update_interval"
|
||||
label="Update Interval (seconds)"
|
||||
value="${config.update_interval || 10}"
|
||||
type="number"
|
||||
min="1">
|
||||
</ha-textfield>
|
||||
<div class="config-note">How often to refresh location data</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates appearance section HTML
|
||||
* @param {Object} config - Configuration object
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static _generateAppearanceSection(config) {
|
||||
return `
|
||||
<div class="config-section">
|
||||
<div class="config-section-header">
|
||||
Appearance Settings
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--primary-text-color);">
|
||||
Marker Border Radius
|
||||
</label>
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||
<input
|
||||
type="range"
|
||||
id="marker_border_radius"
|
||||
min="0"
|
||||
max="50"
|
||||
value="${parseInt(config.marker_border_radius) || 50}"
|
||||
class="radius-slider">
|
||||
<span id="marker-radius-value" style="min-width: 50px; text-align: right; font-weight: 600;">${config.marker_border_radius || '50%'}</span>
|
||||
</div>
|
||||
<div class="config-note">0% for square, 50% for circle</div>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--primary-text-color);">
|
||||
Badge Border Radius
|
||||
</label>
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
|
||||
<input
|
||||
type="range"
|
||||
id="badge_border_radius"
|
||||
min="0"
|
||||
max="50"
|
||||
value="${parseInt(config.badge_border_radius) || 50}"
|
||||
class="radius-slider">
|
||||
<span id="badge-radius-value" style="min-width: 50px; text-align: right; font-weight: 600;">${config.badge_border_radius || '50%'}</span>
|
||||
</div>
|
||||
<div class="config-note">0% for square, 50% for circle</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates entities section HTML
|
||||
* @param {string} entitiesHtml - Pre-generated entities HTML
|
||||
* @param {string} datalists - Pre-generated datalists HTML
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static _generateEntitiesSection(entitiesHtml, datalists) {
|
||||
return `
|
||||
<div class="config-section">
|
||||
<div class="config-section-header">
|
||||
Entities
|
||||
</div>
|
||||
<div id="entities-container">${entitiesHtml}</div>
|
||||
${datalists}
|
||||
<ha-button class="add-button" id="add-entity">
|
||||
Add Entity
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates zones section HTML
|
||||
* @param {string} zonesHtml - Pre-generated zones HTML
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static _generateZonesSection(zonesHtml) {
|
||||
return `
|
||||
<div class="config-section">
|
||||
<div class="config-section-header">
|
||||
Zone Configuration
|
||||
</div>
|
||||
<div id="zones-container">${zonesHtml}</div>
|
||||
<ha-button class="add-button" id="add-zone">
|
||||
Add Zone
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates activities section HTML
|
||||
* @param {string} activitiesHtml - Pre-generated activities HTML
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static _generateActivitiesSection(activitiesHtml) {
|
||||
return `
|
||||
<div class="config-section">
|
||||
<div class="config-section-header">
|
||||
Activity Configuration
|
||||
</div>
|
||||
<div id="activities-container">${activitiesHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates entities HTML
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Object} hass - Home Assistant instance
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static _generateEntitiesHTML(config, hass) {
|
||||
return config.entities
|
||||
.map((entity, idx) => `
|
||||
<div class="config-item">
|
||||
<div class="input-wrapper">
|
||||
<label>Person Entity</label>
|
||||
<input
|
||||
type="text"
|
||||
id="entity-person-${idx}"
|
||||
class="entity-input"
|
||||
value="${entity.person || ''}"
|
||||
data-entity-idx="${idx}"
|
||||
data-entity-field="person"
|
||||
placeholder="person.example"
|
||||
list="person-entities-list">
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<label>Activity Sensor</label>
|
||||
<input
|
||||
type="text"
|
||||
id="entity-activity-${idx}"
|
||||
class="entity-input"
|
||||
value="${entity.activity || ''}"
|
||||
data-entity-idx="${idx}"
|
||||
data-entity-field="activity"
|
||||
placeholder="sensor.phone_activity"
|
||||
list="sensor-entities-list">
|
||||
</div>
|
||||
<ha-icon-button
|
||||
data-entity-delete="${idx}">
|
||||
<ha-icon icon="mdi:delete"></ha-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates zones HTML
|
||||
* @param {Object} config - Configuration object
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static _generateZonesHTML(config) {
|
||||
return Object.entries(config.zones)
|
||||
.map(([state, zoneConfig], idx) => `
|
||||
<div class="config-item">
|
||||
<div class="input-wrapper">
|
||||
<label>State Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="zone-state-${idx}"
|
||||
class="entity-input"
|
||||
value="${state}"
|
||||
data-zone-idx="${idx}"
|
||||
data-zone-field="state"
|
||||
placeholder="home">
|
||||
</div>
|
||||
<div class="input-wrapper" style="flex: 0 0 100px;">
|
||||
<label>Color</label>
|
||||
<input type="color" value="${zoneConfig.color}" data-zone-idx="${idx}" data-zone-field="color" class="entity-input" style="height: 40px; padding: 4px;">
|
||||
</div>
|
||||
<ha-icon-button
|
||||
data-zone-delete="${idx}">
|
||||
<ha-icon icon="mdi:delete"></ha-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates activities HTML
|
||||
* @param {Object} config - Configuration object
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static _generateActivitiesHTML(config) {
|
||||
// Group activities by unique name
|
||||
const uniqueActivities = new Map();
|
||||
const activityGroups = {};
|
||||
|
||||
Object.entries(config.activities).forEach(([state, activityConfig]) => {
|
||||
const displayName = activityConfig.name || state.replace(/_/g, ' ');
|
||||
|
||||
// Skip hidden activities
|
||||
if (HIDDEN_ACTIVITIES.includes(displayName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!uniqueActivities.has(displayName)) {
|
||||
uniqueActivities.set(displayName, { ...activityConfig, states: [state] });
|
||||
activityGroups[displayName] = [state];
|
||||
} else {
|
||||
activityGroups[displayName].push(state);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(uniqueActivities.entries())
|
||||
.map(([displayName, activityConfig]) => {
|
||||
const haIconFormat = activityConfig.icon.replace('mdi-', 'mdi:');
|
||||
const states = activityGroups[displayName].join(',');
|
||||
return `
|
||||
<div class="config-item" style="align-items: center; gap: 16px;">
|
||||
<ha-icon icon="${haIconFormat}" style="--mdc-icon-size: 24px; color: var(--primary-text-color); flex-shrink: 0;"></ha-icon>
|
||||
<div class="input-wrapper" style="flex: 1;">
|
||||
<label>${displayName}</label>
|
||||
</div>
|
||||
<div class="input-wrapper" style="width: 100px;">
|
||||
<label>Color</label>
|
||||
<input type="color" value="${activityConfig.color || '#000000'}" data-activity-states="${states}" style="width: 100%; height: 40px; border-radius: 4px; border: 1px solid var(--divider-color); cursor: pointer;">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates datalists HTML for autocomplete
|
||||
* @param {Object} hass - Home Assistant instance
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
static _generateDatalistsHTML(hass) {
|
||||
if (!hass || !hass.states) {
|
||||
return '<datalist id="person-entities-list"></datalist><datalist id="sensor-entities-list"></datalist>';
|
||||
}
|
||||
|
||||
const personEntities = Object.keys(hass.states)
|
||||
.filter(e => e.startsWith('person.'))
|
||||
.sort();
|
||||
|
||||
const sensorEntities = Object.keys(hass.states)
|
||||
.filter(e => e.startsWith('sensor.'))
|
||||
.sort();
|
||||
|
||||
const personDatalist = `
|
||||
<datalist id="person-entities-list">
|
||||
${personEntities.map(e => {
|
||||
const friendlyName = hass.states[e]?.attributes?.friendly_name || e;
|
||||
return `<option value="${e}">${friendlyName}</option>`;
|
||||
}).join('')}
|
||||
</datalist>
|
||||
`;
|
||||
|
||||
const sensorDatalist = `
|
||||
<datalist id="sensor-entities-list">
|
||||
${sensorEntities.map(e => {
|
||||
const friendlyName = hass.states[e]?.attributes?.friendly_name || e;
|
||||
return `<option value="${e}">${friendlyName}</option>`;
|
||||
}).join('')}
|
||||
</datalist>
|
||||
`;
|
||||
|
||||
return personDatalist + sensorDatalist;
|
||||
}
|
||||
}
|
||||
159
src/entity-data-fetcher.js
Normal file
159
src/entity-data-fetcher.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* Manages fetching and caching entity data from Home Assistant
|
||||
*/
|
||||
export class EntityDataFetcher {
|
||||
constructor(debugMode = false) {
|
||||
this._entityCache = {};
|
||||
this._debug = debugMode;
|
||||
this._hass = null;
|
||||
this._entities = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Home Assistant instance
|
||||
* @param {Object} hass - Home Assistant instance
|
||||
*/
|
||||
setHass(hass) {
|
||||
this._hass = hass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the entities to fetch
|
||||
* @param {Array} entities - Array of entity configurations
|
||||
*/
|
||||
setEntities(entities) {
|
||||
this._entities = entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets debug mode
|
||||
* @param {boolean} debug - Debug mode flag
|
||||
*/
|
||||
setDebugMode(debug) {
|
||||
this._debug = debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs debug messages
|
||||
* @param {string} message - Message to log
|
||||
* @param {...any} args - Additional arguments
|
||||
*/
|
||||
_log(message, ...args) {
|
||||
if (this._debug) {
|
||||
console.log(`[EntityDataFetcher ${new Date().toISOString()}] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches entity data from Home Assistant
|
||||
* @returns {Promise<Object|null>} Prepared entity data or null if no valid data
|
||||
*/
|
||||
async fetchEntities() {
|
||||
if (!this._hass || !this._entities) {
|
||||
this._log('Cannot fetch entities: hass or entities not available');
|
||||
return null;
|
||||
}
|
||||
|
||||
this._log('Fetching entities from hass...');
|
||||
|
||||
let hasValidData = false;
|
||||
|
||||
for (const entityConfig of this._entities) {
|
||||
try {
|
||||
// Fetch person entity from hass states
|
||||
const personState = this._hass.states[entityConfig.person];
|
||||
if (!personState) {
|
||||
console.error(`[EntityDataFetcher] Entity not found: ${entityConfig.person}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for valid GPS data
|
||||
if (!personState.attributes.latitude || !personState.attributes.longitude) {
|
||||
this._log(`No GPS data for ${entityConfig.person}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch activity entity if specified
|
||||
let activityState = null;
|
||||
if (entityConfig.activity) {
|
||||
activityState = this._hass.states[entityConfig.activity];
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
this._entityCache[entityConfig.person] = {
|
||||
person: personState,
|
||||
activity: activityState,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
hasValidData = true;
|
||||
|
||||
this._log(`Cached entity ${entityConfig.person}:`, personState.state,
|
||||
'Location:', personState.attributes.latitude, personState.attributes.longitude);
|
||||
} catch (error) {
|
||||
console.error(`[EntityDataFetcher] Error fetching ${entityConfig.person}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValidData) {
|
||||
this._log('No valid entity data to return');
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.prepareEntityData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares entity data for sending to iframe
|
||||
* @returns {Object} Formatted entity data
|
||||
*/
|
||||
prepareEntityData() {
|
||||
const entityData = {};
|
||||
|
||||
for (const [entityId, data] of Object.entries(this._entityCache)) {
|
||||
// Build picture URL
|
||||
let pictureUrl = data.person.attributes.entity_picture || '';
|
||||
if (pictureUrl && pictureUrl.startsWith('/')) {
|
||||
pictureUrl = window.location.origin + pictureUrl;
|
||||
}
|
||||
|
||||
entityData[entityId] = {
|
||||
state: data.person.state,
|
||||
attributes: {
|
||||
latitude: data.person.attributes.latitude,
|
||||
longitude: data.person.attributes.longitude,
|
||||
friendly_name: data.person.attributes.friendly_name,
|
||||
entity_picture: pictureUrl
|
||||
},
|
||||
activity: data.activity ? data.activity.state : 'unknown'
|
||||
};
|
||||
}
|
||||
|
||||
return entityData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity cache
|
||||
* @returns {Object} Entity cache
|
||||
*/
|
||||
getCache() {
|
||||
return this._entityCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if cache has data
|
||||
* @returns {boolean} True if cache has data
|
||||
*/
|
||||
hasData() {
|
||||
return Object.keys(this._entityCache).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp of the most recent cache update
|
||||
* @returns {number} Most recent timestamp
|
||||
*/
|
||||
getLastUpdateTime() {
|
||||
if (!this.hasData()) return 0;
|
||||
return Math.max(...Object.values(this._entityCache).map(e => e.timestamp || 0));
|
||||
}
|
||||
}
|
||||
209
src/iframe-messenger.js
Normal file
209
src/iframe-messenger.js
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* Handles communication with the map iframe via postMessage
|
||||
*/
|
||||
export class IframeMessenger {
|
||||
constructor(debugMode = false) {
|
||||
this._iframe = null;
|
||||
this._iframeReady = false;
|
||||
this._debug = debugMode;
|
||||
this._retryCount = 0;
|
||||
this._messageListener = null;
|
||||
this._readyCallback = null;
|
||||
this._dataRequestCallback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs debug messages
|
||||
* @param {string} message - Message to log
|
||||
* @param {...any} args - Additional arguments
|
||||
*/
|
||||
_log(message, ...args) {
|
||||
if (this._debug) {
|
||||
console.log(`[IframeMessenger ${new Date().toISOString()}] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the iframe element
|
||||
* @param {HTMLIFrameElement} iframe - Iframe element
|
||||
*/
|
||||
setIframe(iframe) {
|
||||
this._iframe = iframe;
|
||||
this._iframeReady = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the iframe ready state
|
||||
* @returns {boolean} True if iframe is ready
|
||||
*/
|
||||
isReady() {
|
||||
return this._iframeReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the iframe as ready
|
||||
*/
|
||||
markReady() {
|
||||
this._iframeReady = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets debug mode
|
||||
* @param {boolean} debug - Debug mode flag
|
||||
*/
|
||||
setDebugMode(debug) {
|
||||
this._debug = debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callback for when iframe is ready
|
||||
* @param {Function} callback - Callback function
|
||||
*/
|
||||
onReady(callback) {
|
||||
this._readyCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callback for when iframe requests data
|
||||
* @param {Function} callback - Callback function
|
||||
*/
|
||||
onDataRequest(callback) {
|
||||
this._dataRequestCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening for messages from iframe
|
||||
*/
|
||||
startListening() {
|
||||
if (this._messageListener) {
|
||||
window.removeEventListener('message', this._messageListener);
|
||||
}
|
||||
|
||||
this._messageListener = (event) => {
|
||||
if (!event.data) return;
|
||||
|
||||
switch (event.data.type) {
|
||||
case 'iframe-ready':
|
||||
this._log('Iframe reports ready');
|
||||
this._iframeReady = true;
|
||||
if (this._readyCallback) {
|
||||
this._readyCallback();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'request-data':
|
||||
this._log('Iframe requesting data');
|
||||
if (this._dataRequestCallback) {
|
||||
this._dataRequestCallback();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'data-received':
|
||||
this._log('Iframe confirmed data received');
|
||||
this._retryCount = 0;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', this._messageListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening for messages
|
||||
*/
|
||||
stopListening() {
|
||||
if (this._messageListener) {
|
||||
window.removeEventListener('message', this._messageListener);
|
||||
this._messageListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends entity data to the iframe
|
||||
* @param {Object} data - Entity data to send
|
||||
* @returns {boolean} True if sent successfully
|
||||
*/
|
||||
sendData(data) {
|
||||
if (!this._iframe || !this._iframe.contentWindow) {
|
||||
this._log('Cannot send data: iframe not available');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
this._log('No data to send to iframe');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = {
|
||||
type: 'entity-update',
|
||||
data: data,
|
||||
timestamp: Date.now(),
|
||||
debug: this._debug
|
||||
};
|
||||
|
||||
this._log('Sending data to iframe:', message);
|
||||
this._iframe.contentWindow.postMessage(message, '*');
|
||||
this._retryCount = 0;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[IframeMessenger] Error sending data to iframe:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends configuration update to the iframe
|
||||
* @param {Object} zones - Zone configurations
|
||||
* @param {Object} activities - Activity configurations
|
||||
* @param {string} markerBorderRadius - Marker border radius
|
||||
* @param {string} badgeBorderRadius - Badge border radius
|
||||
* @returns {boolean} True if sent successfully
|
||||
*/
|
||||
sendConfigUpdate(zones, activities, markerBorderRadius, badgeBorderRadius) {
|
||||
if (!this._iframe || !this._iframe.contentWindow) {
|
||||
this._log('Cannot send config update: iframe not available');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = {
|
||||
type: 'config-update',
|
||||
zones: zones,
|
||||
activities: activities,
|
||||
marker_border_radius: markerBorderRadius,
|
||||
badge_border_radius: badgeBorderRadius,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this._log('Sending config update to iframe:', message);
|
||||
this._iframe.contentWindow.postMessage(message, '*');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[IframeMessenger] Error sending config update to iframe:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the retry count
|
||||
* @returns {number} Current retry count
|
||||
*/
|
||||
getRetryCount() {
|
||||
return this._retryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the retry count
|
||||
*/
|
||||
incrementRetryCount() {
|
||||
this._retryCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the retry count
|
||||
*/
|
||||
resetRetryCount() {
|
||||
this._retryCount = 0;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue