Release 2.9.0
This commit is contained in:
@@ -3279,4 +3279,55 @@ table.table thead th.sort-desc .table-sort-indicator {
|
||||
.card-body.table-responsive > table.table > thead th {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
.weather-preview-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.weather-preview-card-body {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.weather-preview-card-footer {
|
||||
min-height: 3.5rem;
|
||||
}
|
||||
|
||||
.weather-preview-icon {
|
||||
font-size: 4rem;
|
||||
color: #e0a11a;
|
||||
}
|
||||
|
||||
.weather-preview-forecast-icon {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
color: #e0a11a;
|
||||
margin: 1rem 0 0.65rem;
|
||||
}
|
||||
|
||||
.weather-daily-forecast {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.weather-daily-forecast > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.weather-daily-forecast {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.weather-daily-forecast {
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.weather-preview-hourly-item {
|
||||
width: 7.5rem;
|
||||
}
|
||||
@@ -610,7 +610,7 @@
|
||||
} catch (_error) {
|
||||
actionPath = String(form.action || '');
|
||||
}
|
||||
var shouldReloadAfterSuccess = /^\/announcements\/\d+\/(?:play|stop)$/.test(actionPath);
|
||||
var shouldReloadAfterSuccess = /^\/announcements\/\d+\/(?:play|stop)$/.test(actionPath) || /^\/data-sources\/(?:api-sources|rss-feeds|weather)\/\d+$/.test(actionPath);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@@ -799,6 +799,28 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var refreshButton = target.closest('[data-manual-refresh-url]');
|
||||
if (refreshButton) {
|
||||
event.preventDefault();
|
||||
if (refreshButton.disabled) {
|
||||
return;
|
||||
}
|
||||
refreshButton.disabled = true;
|
||||
fetch(refreshButton.getAttribute('data-manual-refresh-url'), {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Refresh failed');
|
||||
}
|
||||
window.location.assign(response.url);
|
||||
}).catch(function () {
|
||||
refreshButton.disabled = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var button = target.closest('button[name="save_action"], button[name="action"]');
|
||||
if (!button) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var label = document.getElementById('weather-label');
|
||||
var results = document.getElementById('weather-location-results');
|
||||
var latitude = document.getElementById('weather-latitude');
|
||||
var longitude = document.getElementById('weather-longitude');
|
||||
var latitudeValue = document.getElementById('weather-latitude-value');
|
||||
var longitudeValue = document.getElementById('weather-longitude-value');
|
||||
var timezone = document.getElementById('weather-timezone');
|
||||
var manualCoordinates = document.getElementById('weather-manual-coordinates');
|
||||
var timer;
|
||||
var requestId = 0;
|
||||
|
||||
if (!label || !results || !latitude || !longitude || !timezone) {
|
||||
return;
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
results.replaceChildren();
|
||||
results.classList.add('d-none');
|
||||
}
|
||||
|
||||
function selectResult(result) {
|
||||
label.value = result.label;
|
||||
latitude.value = result.latitude;
|
||||
longitude.value = result.longitude;
|
||||
timezone.value = result.timezone;
|
||||
latitudeValue.value = result.latitude;
|
||||
longitudeValue.value = result.longitude;
|
||||
latitude.disabled = true;
|
||||
longitude.disabled = true;
|
||||
latitude.removeAttribute('name');
|
||||
longitude.removeAttribute('name');
|
||||
latitudeValue.name = 'latitude';
|
||||
longitudeValue.name = 'longitude';
|
||||
manualCoordinates.classList.remove('d-none');
|
||||
clearResults();
|
||||
}
|
||||
|
||||
function enableManualCoordinates() {
|
||||
latitude.disabled = false;
|
||||
longitude.disabled = false;
|
||||
latitude.name = 'latitude';
|
||||
longitude.name = 'longitude';
|
||||
latitudeValue.removeAttribute('name');
|
||||
longitudeValue.removeAttribute('name');
|
||||
manualCoordinates.classList.add('d-none');
|
||||
}
|
||||
|
||||
function renderResults(items) {
|
||||
results.replaceChildren();
|
||||
if (!items.length) {
|
||||
clearResults();
|
||||
return;
|
||||
}
|
||||
items.forEach(function (item) {
|
||||
var button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'list-group-item list-group-item-action';
|
||||
button.textContent = item.label;
|
||||
button.addEventListener('click', function () { selectResult(item); });
|
||||
results.appendChild(button);
|
||||
});
|
||||
results.classList.remove('d-none');
|
||||
}
|
||||
|
||||
label.addEventListener('input', function () {
|
||||
enableManualCoordinates();
|
||||
window.clearTimeout(timer);
|
||||
var query = label.value.trim();
|
||||
if (query.length < 2) {
|
||||
clearResults();
|
||||
return;
|
||||
}
|
||||
timer = window.setTimeout(function () {
|
||||
var currentRequestId = ++requestId;
|
||||
fetch('/data-sources/weather/geocode?q=' + encodeURIComponent(query), { credentials: 'same-origin' })
|
||||
.then(function (response) { if (!response.ok) throw new Error('Lookup failed'); return response.json(); })
|
||||
.then(function (data) { if (currentRequestId === requestId) renderResults(data.results || []); })
|
||||
.catch(function () { if (currentRequestId === requestId) clearResults(); });
|
||||
}, 300);
|
||||
});
|
||||
|
||||
manualCoordinates.addEventListener('click', enableManualCoordinates);
|
||||
|
||||
if (latitude.value && longitude.value) {
|
||||
selectResult({ label: label.value, latitude: latitude.value, longitude: longitude.value, timezone: timezone.value });
|
||||
}
|
||||
|
||||
var refreshButton = document.querySelector('[data-weather-refresh-url]');
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener('click', function () {
|
||||
refreshButton.disabled = true;
|
||||
fetch(refreshButton.getAttribute('data-weather-refresh-url'), { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (response) {
|
||||
if (!response.ok) throw new Error('Refresh failed');
|
||||
window.location.assign(response.url);
|
||||
})
|
||||
.catch(function () { refreshButton.disabled = false; });
|
||||
});
|
||||
}
|
||||
|
||||
var forecastButtons = document.querySelectorAll('[data-weather-forecast-mode]');
|
||||
var dailyForecast = document.getElementById('weather-daily-forecast');
|
||||
var hourlyForecast = document.getElementById('weather-hourly-forecast');
|
||||
forecastButtons.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var hourly = button.getAttribute('data-weather-forecast-mode') === 'hourly';
|
||||
dailyForecast.classList.toggle('d-none', hourly);
|
||||
hourlyForecast.classList.toggle('d-none', !hourly);
|
||||
forecastButtons.forEach(function (item) {
|
||||
var selected = item === button;
|
||||
item.classList.toggle('btn-primary', selected);
|
||||
item.classList.toggle('btn-outline-secondary', !selected);
|
||||
item.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
if (!results.contains(event.target) && event.target !== label) {
|
||||
clearResults();
|
||||
}
|
||||
});
|
||||
}());
|
||||
@@ -88,6 +88,7 @@
|
||||
h4: ['class', 'style'],
|
||||
h5: ['class', 'style'],
|
||||
h6: ['class', 'style'],
|
||||
i: ['class', 'style', 'aria-hidden'],
|
||||
img: ['src', 'alt', 'title', 'width', 'height', 'class', 'style', 'loading', 'decoding'],
|
||||
col: ['class', 'style', 'span', 'width'],
|
||||
colgroup: ['class', 'style', 'span'],
|
||||
@@ -159,6 +160,7 @@
|
||||
h4: ['class', 'style'],
|
||||
h5: ['class', 'style'],
|
||||
h6: ['class', 'style'],
|
||||
i: ['class', 'style', 'aria-hidden'],
|
||||
li: ['class', 'style'],
|
||||
ol: ['class', 'style', 'start'],
|
||||
p: ['class', 'style'],
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
// Weather region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
var placeholderUtils = window.placeholderUtils || {};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value == null ? '' : value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function getLocationById(locationId, locations) {
|
||||
return (Array.isArray(locations) ? locations : []).find(function (location) {
|
||||
return Number(location.id) === Number(locationId);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function iconForCode(code) {
|
||||
var value = Number(code);
|
||||
if (!Number.isFinite(value)) return 'bi-cloud';
|
||||
if (value === 0) return 'bi-sun';
|
||||
if (value <= 3) return 'bi-cloud-sun';
|
||||
if (value <= 48) return 'bi-cloud-fog';
|
||||
if (value <= 67 || value > 77 && value <= 82) return 'bi-cloud-rain';
|
||||
if (value <= 77) return 'bi-cloud-snow';
|
||||
return 'bi-cloud-lightning-rain';
|
||||
}
|
||||
|
||||
function conditionForCode(code) {
|
||||
var value = Number(code);
|
||||
if (value === 0) return 'Clear sky';
|
||||
if (value <= 3) return 'Partly cloudy';
|
||||
if (value <= 48) return 'Foggy';
|
||||
if (value <= 67) return 'Rain';
|
||||
if (value <= 77) return 'Snow';
|
||||
if (value <= 82) return 'Showers';
|
||||
return 'Thunderstorm';
|
||||
}
|
||||
|
||||
function getSnapshot(location) {
|
||||
return location && location.responseJson && typeof location.responseJson === 'object' ? location.responseJson : null;
|
||||
}
|
||||
|
||||
function getDefaultStyle() {
|
||||
return { font_family: 'Arial', font_size: 32, font_color: '#000000' };
|
||||
}
|
||||
|
||||
function getPlaceholderFields(forecastMode) {
|
||||
var fields = ['location_label', 'name', 'temp_unit', 'wind_unit', 'precip_unit'];
|
||||
if (forecastMode === 'current') {
|
||||
return fields.concat(['current.temp', 'current.feels_like', 'current.humidity', 'current.wind', 'current.precip', 'current.uv_index', 'current.cloud_cover', 'current.icon']);
|
||||
}
|
||||
var prefix = forecastMode === 'hourly' ? 'hourly' : 'daily';
|
||||
var fieldsByMode = forecastMode === 'hourly' ? ['time', 'code', 'temp', 'precip', 'wind', 'uv_index', 'cloud_cover'] : ['time', 'code', 'temp_max', 'temp_min', 'precip', 'wind', 'uv_index', 'cloud_cover', 'sunrise', 'sunset'];
|
||||
var count = forecastMode === 'hourly' ? 24 : 7;
|
||||
for (var index = 0; index < count; index += 1) {
|
||||
fieldsByMode.forEach(function (field) { fields.push(prefix + '.' + index + '.' + field); });
|
||||
fields.push(prefix + '.' + index + '.icon');
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function renderLimitedPlaceholderChips(fields, visibleCount) {
|
||||
if (visibleCount >= fields.length) return fields.map(function (field) {
|
||||
return window.placeholderChips && typeof window.placeholderChips.renderChip === 'function'
|
||||
? window.placeholderChips.renderChip(field)
|
||||
: '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
}).join('');
|
||||
var visibleFields = fields.slice(0, visibleCount);
|
||||
var overflowFields = fields.slice(visibleFields.length);
|
||||
var renderChip = function (field) {
|
||||
return window.placeholderChips && typeof window.placeholderChips.renderChip === 'function'
|
||||
? window.placeholderChips.renderChip(field)
|
||||
: '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
};
|
||||
var visibleMarkup = visibleFields.map(renderChip).join('');
|
||||
if (!overflowFields.length) return visibleMarkup;
|
||||
return visibleMarkup + '<details class="w-100 mt-1" data-placeholder-chips-more><summary class="small text-body-secondary">Show ' + escapeHtml(overflowFields.length) + ' more fields</summary><div class="d-flex flex-wrap gap-2 mt-1">' + overflowFields.map(renderChip).join('') + '</div></details>';
|
||||
}
|
||||
|
||||
function normalizeWeatherExpression(expression) {
|
||||
var value = String(expression || '').trim();
|
||||
var currentAliases = {
|
||||
temp: 'current.temperature_2m', temp_unit: 'current.temperature_unit', feels_like: 'current.apparent_temperature', humidity: 'current.relative_humidity_2m', code: 'current.weather_code', wind: 'current.wind_speed_10m', wind_unit: 'current.wind_speed_unit', precip: 'current.precipitation', precip_unit: 'current.precipitation_unit', uv_index: 'current.uv_index', cloud_cover: 'current.cloud_cover', icon: 'current.weather_code.icon'
|
||||
};
|
||||
var globalAliases = { temp_unit: 'temperature_unit', wind_unit: 'wind_speed_unit', precip_unit: 'precipitation_unit' };
|
||||
if (globalAliases[value]) return globalAliases[value];
|
||||
var currentField = value.replace(/^current\./, '').match(/^([^.()]+)((?:\(.*\))|(?:\..*))?$/);
|
||||
if (currentField && currentAliases[currentField[1]]) return currentAliases[currentField[1]] + (currentField[2] || '');
|
||||
var indexed = value.match(/^(daily|hourly)\.(\d+)\.(.+)$/);
|
||||
if (!indexed) return value;
|
||||
var prefix = indexed[1];
|
||||
var index = indexed[2];
|
||||
var fieldMatch = indexed[3].match(/^([^.()]+)((?:\(.*\))|(?:\..*))?$/);
|
||||
var field = fieldMatch ? fieldMatch[1] : indexed[3];
|
||||
var aliases = prefix === 'daily' ? { time: 'time', code: 'weather_code', temp_max: 'temperature_2m_max', temp_min: 'temperature_2m_min', precip: 'precipitation_sum', wind: 'wind_speed_10m_max', uv_index: 'uv_index_max', cloud_cover: 'cloud_cover_mean', sunrise: 'sunrise', sunset: 'sunset', icon: 'weather_code' } : { time: 'time', code: 'weather_code', temp: 'temperature_2m', precip: 'precipitation', wind: 'wind_speed_10m', uv_index: 'uv_index', cloud_cover: 'cloud_cover', icon: 'weather_code' };
|
||||
if (!Object.prototype.hasOwnProperty.call(aliases, field)) return value;
|
||||
return field === 'icon' ? prefix + '.' + aliases[field] + '.' + index + '.icon' + (fieldMatch[2] || '') : prefix + '.' + aliases[field] + '.' + index + (fieldMatch[2] || '');
|
||||
}
|
||||
|
||||
function withWeatherUnits(snapshot, location) {
|
||||
var data = Object.assign({}, snapshot, { location_label: location.location_label || '', name: location.name || '', timezone: location.timezone || '', temp_unit: location.temperature_unit === 'fahrenheit' ? '°F' : '°C', wind_unit: location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'm/s' : 'km/h', precip_unit: location.precipitation_unit === 'inch' ? 'in' : 'mm' });
|
||||
var temperatureUnit = location.temperature_unit === 'fahrenheit' ? '°F' : '°C';
|
||||
var windUnit = location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'm/s' : 'km/h';
|
||||
var precipitationUnit = location.precipitation_unit === 'inch' ? 'in' : 'mm';
|
||||
data.current = Object.assign({}, snapshot.current || {}, { temperature_unit: temperatureUnit, wind_speed_unit: windUnit, precipitation_unit: precipitationUnit });
|
||||
data.daily = Object.assign({}, snapshot.daily || {}, { temperature_unit: temperatureUnit });
|
||||
data.hourly = Object.assign({}, snapshot.hourly || {}, { temperature_unit: temperatureUnit });
|
||||
return data;
|
||||
}
|
||||
|
||||
function substituteVariables(html, value) {
|
||||
return String(html || '').replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
|
||||
if (!value || typeof value !== 'object' || typeof placeholderUtils.resolvePlaceholderExpression !== 'function' || typeof placeholderUtils.formatPlaceholderValue !== 'function') return '';
|
||||
var rawExpression = String(expression).trim();
|
||||
var normalizedExpression = normalizeWeatherExpression(rawExpression);
|
||||
var iconMatch = normalizedExpression.match(/^(?:current\.weather_code|daily\.weather_code\.\d+|hourly\.weather_code\.\d+)\.icon(?:\((\d+)(?:\s*,\s*(\d+))?\))?$/);
|
||||
if (iconMatch) {
|
||||
var codeExpression = normalizedExpression.replace(/\.icon(?:\(.*\))?$/, '');
|
||||
var sizeExpression = rawExpression.match(/\.icon\((\d+)(?:\s*,\s*(\d+))?\)$/);
|
||||
var weatherCode = placeholderUtils.resolvePlaceholderExpression(value, codeExpression, { timeZone: value.timezone });
|
||||
var width = sizeExpression ? Math.max(1, Math.min(1000, Number(sizeExpression[1]))) : 0;
|
||||
var height = sizeExpression && sizeExpression[2] ? Math.max(1, Math.min(1000, Number(sizeExpression[2]))) : width;
|
||||
var style = width ? ' style="display:inline-block;vertical-align:middle;font-size:' + width + 'px;line-height:' + height + 'px;width:' + width + 'px;height:' + height + 'px;"' : '';
|
||||
var icon = '<i class="bi ' + iconForCode(weatherCode) + '"' + style + ' aria-hidden="true"></i>';
|
||||
return width ? '<span style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:' + width + 'px;height:' + height + 'px;border:1px dashed rgba(120,120,120,.75);">' + icon + '</span>' : icon;
|
||||
}
|
||||
return escapeHtml(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(value, normalizeWeatherExpression(expression), { timeZone: value.timezone })));
|
||||
});
|
||||
}
|
||||
|
||||
function renderTransformInfo(regionId) {
|
||||
var offcanvasId = 'weather-placeholder-info-' + regionId;
|
||||
return '<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" data-bs-toggle="offcanvas" data-bs-target="#' + offcanvasId + '" aria-controls="' + offcanvasId + '">More info</button>' +
|
||||
'<div class="offcanvas offcanvas-end fw-normal" tabindex="-1" id="' + offcanvasId + '" aria-labelledby="' + offcanvasId + '-label">' +
|
||||
'<div class="offcanvas-header"><h2 class="offcanvas-title fs-5" id="' + offcanvasId + '-label">Weather placeholder transforms</h2><button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button></div>' +
|
||||
'<div class="offcanvas-body"><p class="small text-body-secondary">Placeholders read values from the selected weather snapshot. Nested fields use dots.</p>' +
|
||||
'<h3 class="h6 mt-3">Weather icons</h3><p class="small text-body-secondary">Use the icon property to insert a Bootstrap weather icon. Add width and height in pixels when sizing is needed.</p><ul class="small"><li><code>{{current.icon}}</code></li><li><code>{{current.icon(48,48)}}</code></li><li><code>{{daily.0.icon(32,24)}}</code></li></ul>' +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderTextTransforms() : '') +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderDateFormatTokens() : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderPreview(region, content, context) {
|
||||
var locations = context && context.weatherLocations ? context.weatherLocations : [];
|
||||
var location = getLocationById(content && content.weather_location_id, locations);
|
||||
var snapshot = getSnapshot(location);
|
||||
var value = String(content && content.value || '');
|
||||
if (!location || !snapshot) return '<div class="template-region weather"><div class="slide-preview-placeholder">Select a weather location with cached data</div></div>';
|
||||
var weatherData = withWeatherUnits(snapshot, location);
|
||||
if (value) {
|
||||
var fontSize = Math.max(8, Number(content && content.font_size || region && (region.font_size || region.fontSize) || 32) || 32);
|
||||
return '<div class="template-region weather" style="width:100%;height:100%;overflow:hidden;font-family:Arial;font-size:' + fontSize + 'px;line-height:1.5;color:#000000;"><div class="template-region-text-scale" style="width:100%;height:100%;overflow:hidden;">' + (utils.sanitizePreviewHtml ? utils.sanitizePreviewHtml(substituteVariables(value, weatherData)) : substituteVariables(value, weatherData)) + '</div></div>';
|
||||
}
|
||||
var current = snapshot.current || {};
|
||||
var daily = snapshot.daily || {};
|
||||
var code = current.weather_code;
|
||||
var days = (daily.time || []).slice(0, 7).map(function (day, index) {
|
||||
return '<div class="weather-region-day"><strong>' + escapeHtml(index === 0 ? 'Today' : String(day).slice(5)) + '</strong><i class="bi ' + iconForCode(daily.weather_code && daily.weather_code[index]) + '"></i><span>' + escapeHtml(daily.temperature_2m_max && daily.temperature_2m_max[index] !== undefined ? daily.temperature_2m_max[index] + '°' : '-') + '</span></div>';
|
||||
}).join('');
|
||||
return '<div class="template-region weather" style="width:100%;height:100%;overflow:hidden;padding:3%;font-family:Arial,sans-serif;"><div style="display:flex;align-items:center;justify-content:space-between;"><div><div style="font-size:.8em;opacity:.72;">' + escapeHtml(location.location_label || location.name) + '</div><strong style="font-size:2em;">' + escapeHtml(current.temperature_2m === undefined ? '-' : current.temperature_2m) + '°</strong><div>' + escapeHtml(conditionForCode(code)) + '</div></div><i class="bi ' + iconForCode(code) + '" style="font-size:3em;"></i></div><div style="display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:.35em;margin-top:1em;">' + days + '</div></div>';
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = context.current || {};
|
||||
var locations = Array.isArray(context.weatherLocations) ? context.weatherLocations : [];
|
||||
var options = locations.map(function (location) {
|
||||
return '<option value="' + escapeHtml(location.id) + '"' + (Number(location.id) === Number(current.weather_location_id) ? ' selected' : '') + '>' + escapeHtml(location.name || location.location_label || ('Location ' + location.id)) + '</option>';
|
||||
}).join('');
|
||||
var currentValue = current.value !== undefined ? current.value : current.text !== undefined ? current.text : '';
|
||||
var forecastMode = ['current', 'hourly'].includes(current.forecast_mode) ? current.forecast_mode : 'daily';
|
||||
var placeholderFields = getPlaceholderFields(forecastMode);
|
||||
var visiblePlaceholderCount = forecastMode === 'current' ? placeholderFields.length : 5 + (forecastMode === 'hourly' ? 8 : 11);
|
||||
var chips = renderLimitedPlaceholderChips(placeholderFields, visiblePlaceholderCount);
|
||||
return '<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + escapeHtml(region.id) + '"><div class="card-header template-field-head"><strong>' + escapeHtml(region.label) + '</strong><div class="template-field-actions"><span class="chip">Weather</span></div></div><div class="card-body p-3 d-grid gap-3"><div class="d-flex justify-content-end"><div class="btn-group btn-group-sm template-editor-size-controls" role="group" aria-label="Editor size controls"><button type="button" class="btn btn-outline-secondary" data-editor-size-action="decrease" aria-label="Decrease editor size">-</button><button type="button" class="btn btn-outline-secondary" data-editor-size-action="increase" aria-label="Increase editor size">+</button></div></div><div class="editor-holder" data-region-id="' + escapeHtml(region.id) + '"><textarea class="editor-source" rows="10">' + escapeHtml(currentValue) + '</textarea></div><div class="row g-3 align-items-end"><div class="col-12 col-md-8"><label class="form-label" for="region_weather_location_id_' + escapeHtml(region.id) + '">Weather location</label><select id="region_weather_location_id_' + escapeHtml(region.id) + '" name="region_weather_location_id_' + escapeHtml(region.id) + '" class="form-select"><option value="">Select a location</option>' + options + '</select></div><div class="col-12 col-md-4"><label class="form-label" for="region_weather_forecast_mode_' + escapeHtml(region.id) + '">Forecast</label><select id="region_weather_forecast_mode_' + escapeHtml(region.id) + '" name="region_weather_forecast_mode_' + escapeHtml(region.id) + '" class="form-select"><option value="current"' + (forecastMode === 'current' ? ' selected' : '') + '>Current</option><option value="daily"' + (forecastMode === 'daily' ? ' selected' : '') + '>7 days</option><option value="hourly"' + (forecastMode === 'hourly' ? ' selected' : '') + '>24 hour</option></select></div></div><input type="hidden" name="region_text_' + escapeHtml(region.id) + '" value="' + escapeHtml(currentValue) + '" /><div class="api-region-placeholder-section"><div class="api-region-placeholder-title api-region-placeholder-title-help d-flex align-items-center gap-2">Available placeholders ' + renderTransformInfo(region.id) + '</div><div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + chips + '</div></div></div></div>';
|
||||
}
|
||||
|
||||
document.addEventListener('change', function (event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.matches || !target.matches('select[name^="region_weather_forecast_mode_"]')) return;
|
||||
var card = target.closest('[data-region-id]');
|
||||
var chipContainer = card && card.querySelector('[data-placeholder-chips]');
|
||||
if (!chipContainer || !window.placeholderChips || typeof window.placeholderChips.renderChips !== 'function') return;
|
||||
var forecastMode = target.value === 'hourly' ? 'hourly' : target.value === 'current' ? 'current' : 'daily';
|
||||
var placeholderFields = getPlaceholderFields(forecastMode);
|
||||
var visiblePlaceholderCount = forecastMode === 'current' ? placeholderFields.length : 5 + (forecastMode === 'hourly' ? 8 : 11);
|
||||
chipContainer.innerHTML = renderLimitedPlaceholderChips(placeholderFields, visiblePlaceholderCount);
|
||||
});
|
||||
|
||||
function buildEditorCardContext(context) {
|
||||
return { region: context.region, current: context.current || {}, weatherLocations: context.weatherLocations || [] };
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card, existingContent, locations) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
var input = card && card.querySelector('select[name="region_weather_location_id_' + region.id + '"]');
|
||||
var modeInput = card && card.querySelector('select[name="region_weather_forecast_mode_' + region.id + '"]');
|
||||
var textInput = card && card.querySelector('input[name="region_text_' + region.id + '"]');
|
||||
var textArea = card && card.querySelector('textarea.editor-source');
|
||||
return { value: textInput ? textInput.value : textArea ? textArea.value : current.value || '', weather_location_id: input ? input.value : current.weather_location_id, forecast_mode: modeInput ? modeInput.value : current.forecast_mode || 'daily', weatherLocations: locations || [] };
|
||||
}
|
||||
|
||||
registry.register('weather', { label: 'Weather', getDefaultStyle: getDefaultStyle, getDefaultRegionSize: function () { return { width: 720, height: 260 }; }, renderPreview: renderPreview, renderEditorCard: renderEditorCard, buildEditorCardContext: buildEditorCardContext, buildPreviewRenderContext: buildPreviewRenderContext });
|
||||
}());
|
||||
@@ -617,6 +617,8 @@ export function createSlideFormEditorController(options) {
|
||||
remove_script_host: false,
|
||||
convert_urls: true,
|
||||
paste_data_images: false,
|
||||
paste_block_drop: true,
|
||||
resize: false,
|
||||
automatic_uploads: true,
|
||||
images_file_types: imageUploadFileTypes,
|
||||
images_upload_handler: uploadEditorImage,
|
||||
|
||||
@@ -9,6 +9,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
var rssFeeds = Array.isArray(settings.rssFeeds) ? settings.rssFeeds : [];
|
||||
var apiSources = Array.isArray(settings.apiSources) ? settings.apiSources : [];
|
||||
var timetableGroups = Array.isArray(settings.timetableGroups) ? settings.timetableGroups : [];
|
||||
var weatherLocations = Array.isArray(settings.weatherLocations) ? settings.weatherLocations : [];
|
||||
var getRegionTypeModule = typeof settings.getRegionTypeModule === 'function' ? settings.getRegionTypeModule : function () { return null; };
|
||||
var requestPreviewRender = typeof settings.requestPreviewRender === 'function' ? settings.requestPreviewRender : function () {};
|
||||
var placeholderUtils = window.placeholderUtils || {};
|
||||
@@ -537,7 +538,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
if (module && typeof module.buildEditorCardContext === 'function') {
|
||||
return module.buildEditorCardContext({
|
||||
region: region,
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'time-date' || region.region_type === 'timetable' ? (existingContent[region.region_key] || {}) : getCurrentRegionValue(region),
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'time-date' || region.region_type === 'timetable' || region.region_type === 'weather' ? (existingContent[region.region_key] || {}) : getCurrentRegionValue(region),
|
||||
currentDuration: getCurrentRegionVideoDuration(region),
|
||||
regionRatio: reduceAspectRatio(region.width, region.height),
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
@@ -546,7 +547,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
uploadVideoMaxBytes: uploadVideoMaxBytes,
|
||||
uploadMimeTypes: uploadMimeTypes,
|
||||
disableAudio: (existingContent[region.region_key] || {}).disable_audio,
|
||||
config: region.region_type === 'api' ? getCurrentApiConfig(region) : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : getCurrentRssConfig(region),
|
||||
config: region.region_type === 'api' ? getCurrentApiConfig(region) : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : region.region_type === 'weather' ? { weather_location_id: (existingContent[region.region_key] || {}).weather_location_id || '' } : getCurrentRssConfig(region),
|
||||
style: getCurrentTextStyle(region),
|
||||
fontSize: getCurrentTextStyle(region).font_size,
|
||||
feedOptions: rssFeeds.map(function (feed) {
|
||||
@@ -568,8 +569,9 @@ export function createSlideFormRegionHelpers(options) {
|
||||
? getRssFieldList(getCurrentRssConfig(region).feed_id)
|
||||
: [],
|
||||
sampleDataPreview: buildApiSampleDataMarkup(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).item_number, getCurrentApiConfig(region).items_path),
|
||||
timetableGroups: timetableGroups
|
||||
}, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : apiSources);
|
||||
timetableGroups: timetableGroups,
|
||||
weatherLocations: weatherLocations
|
||||
}, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : region.region_type === 'weather' ? weatherLocations : apiSources);
|
||||
}
|
||||
|
||||
var textStyle = getCurrentTextStyle(region);
|
||||
@@ -580,7 +582,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
|
||||
return {
|
||||
region: region,
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' ? currentContent : getCurrentRegionValue(region),
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'weather' ? currentContent : getCurrentRegionValue(region),
|
||||
currentDuration: getCurrentRegionVideoDuration(region),
|
||||
regionRatio: reduceAspectRatio(region.width, region.height),
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
@@ -589,7 +591,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
uploadVideoMaxBytes: uploadVideoMaxBytes,
|
||||
uploadMimeTypes: uploadMimeTypes,
|
||||
disableAudio: currentContent.disable_audio,
|
||||
config: region.region_type === 'api' ? apiConfig : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : rssConfig,
|
||||
config: region.region_type === 'api' ? apiConfig : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : region.region_type === 'weather' ? { weather_location_id: currentContent.weather_location_id || '' } : rssConfig,
|
||||
style: textStyle,
|
||||
fontSize: textStyle.font_size,
|
||||
feedOptions: rssFeeds.map(function (feed) {
|
||||
@@ -609,7 +611,8 @@ export function createSlideFormRegionHelpers(options) {
|
||||
? getRssFieldList(rssConfig.feed_id)
|
||||
: [],
|
||||
sampleDataPreview: buildApiSampleDataMarkup(apiConfig.source_id, apiConfig.item_number, apiItemsPath),
|
||||
timetableGroups: timetableGroups
|
||||
timetableGroups: timetableGroups,
|
||||
weatherLocations: weatherLocations
|
||||
};
|
||||
}
|
||||
|
||||
@@ -745,7 +748,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
function getPreviewRegionContent(card, region) {
|
||||
var module = getRegionTypeModule(region.region_type);
|
||||
if (module && typeof module.buildPreviewRenderContext === 'function') {
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : apiSources);
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : region.region_type === 'weather' ? weatherLocations : apiSources);
|
||||
}
|
||||
|
||||
var current = existingContent[region.region_key];
|
||||
@@ -813,7 +816,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
function buildPreviewRenderContext(region, card) {
|
||||
var module = getRegionTypeModule(region.region_type);
|
||||
if (module && typeof module.buildPreviewRenderContext === 'function') {
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : apiSources);
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : region.region_type === 'weather' ? weatherLocations : apiSources);
|
||||
}
|
||||
|
||||
var textStyle = getCurrentTextStyle(region);
|
||||
|
||||
@@ -30,6 +30,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
||||
var rssFeeds = Array.isArray(slideEditorData.rssFeeds) ? slideEditorData.rssFeeds : [];
|
||||
var apiSources = Array.isArray(slideEditorData.apiSources) ? slideEditorData.apiSources : [];
|
||||
var timetableGroups = Array.isArray(slideEditorData.timetableGroups) ? slideEditorData.timetableGroups : [];
|
||||
var weatherLocations = Array.isArray(slideEditorData.weatherLocations) ? slideEditorData.weatherLocations : [];
|
||||
var fontStylesheetHref = String(slideEditorData.fontStylesheetHref || '').trim();
|
||||
var existingTemplateId = slideEditorData.existingTemplateId !== undefined ? slideEditorData.existingTemplateId : null;
|
||||
var existingContent = slideEditorData.existingContent || {};
|
||||
@@ -139,6 +140,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
||||
rssFeeds: rssFeeds,
|
||||
apiSources: apiSources,
|
||||
timetableGroups: timetableGroups,
|
||||
weatherLocations: weatherLocations,
|
||||
defaultFontSize: DEFAULT_FONT_SIZE,
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
uploadVideoMaxLabel: uploadVideoMaxLabel,
|
||||
|
||||
Reference in New Issue
Block a user