1867 lines
67 KiB
JavaScript
1867 lines
67 KiB
JavaScript
// Template designer bootstrap and form wiring.
|
|
|
|
(function () {
|
|
var dataElement = document.getElementById('template-editor-data');
|
|
if (!dataElement) {
|
|
return;
|
|
}
|
|
|
|
var utils = window.templateDesignerUtils || {};
|
|
|
|
var templateData = {};
|
|
try {
|
|
templateData = JSON.parse(dataElement.getAttribute('data-json') || dataElement.textContent || '{}') || {};
|
|
} catch (_error) {
|
|
templateData = {};
|
|
}
|
|
|
|
var existingRegions = Array.isArray(templateData.regions)
|
|
? templateData.regions
|
|
: (Array.isArray(templateData) ? templateData : []);
|
|
var stage = document.getElementById('designer-stage');
|
|
var overlay = document.getElementById('designer-overlay');
|
|
var previewCard = document.getElementById('template-preview-card');
|
|
var previewIntroButton = document.getElementById('template-preview-intro');
|
|
var previewOutButton = document.getElementById('template-preview-out');
|
|
var previewContinuousButton = document.getElementById('template-preview-continuous');
|
|
var regionList = document.getElementById('region-list');
|
|
var regionSelect = document.getElementById('region-select');
|
|
var canvasSizeSelect = document.getElementById('canvas-size-select');
|
|
var canvasSizeIdInput = document.getElementById('canvas-size-id');
|
|
var canvasSizeSummary = document.getElementById('canvas-size-summary');
|
|
var canvasWidthInput = document.getElementById('canvas-width');
|
|
var canvasHeightInput = document.getElementById('canvas-height');
|
|
var backgroundInput = document.getElementById('background-image');
|
|
var backgroundColorInput = document.getElementById('background-color');
|
|
var backgroundPreview = document.getElementById('background-preview');
|
|
var backgroundEmpty = document.getElementById('background-empty');
|
|
var removeBackgroundButton = document.getElementById('remove-background-image');
|
|
var removeBackgroundFlag = document.getElementById('remove-background-image-flag');
|
|
var addRegionButton = document.getElementById('add-region-button');
|
|
var regionAddModal = document.getElementById('region-add-modal');
|
|
var regionAddOptions = document.getElementById('region-add-options');
|
|
var animationAdvancedModal = document.getElementById('animation-advanced-modal');
|
|
var regionCardTemplate = document.getElementById('region-card-template');
|
|
var regionsJsonInput = document.getElementById('regions-json');
|
|
var templateForm = document.getElementById('template-form');
|
|
var templateRegionUsageElement = document.getElementById('template-region-usage');
|
|
var regionUsageSet = new Set();
|
|
var draft = null;
|
|
var selectedIndex = -1;
|
|
var activeAnimationCard = null;
|
|
var overlayRenderFrame = 0;
|
|
var stageResizeObserver = null;
|
|
var regionNameValidationToastShown = false;
|
|
var previewState = {
|
|
active: false,
|
|
mode: '',
|
|
stopTimer: 0,
|
|
token: 0,
|
|
animations: []
|
|
};
|
|
var previewButtons = [
|
|
{ button: previewIntroButton, mode: 'intro' },
|
|
{ button: previewOutButton, mode: 'out' },
|
|
{ button: previewContinuousButton, mode: 'continuous' }
|
|
];
|
|
|
|
try {
|
|
var regionUsageData = JSON.parse((templateRegionUsageElement && templateRegionUsageElement.textContent) || '[]');
|
|
if (Array.isArray(regionUsageData)) {
|
|
regionUsageData.forEach(function (regionKey) {
|
|
var normalized = String(regionKey || '').trim();
|
|
if (normalized) {
|
|
regionUsageSet.add(normalized);
|
|
}
|
|
});
|
|
}
|
|
} catch (_error) {
|
|
regionUsageSet = new Set();
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
|
}
|
|
|
|
function valueOrDefault(value, fallback) {
|
|
return utils.valueOrDefault ? utils.valueOrDefault(value, fallback) : (value === undefined || value === null || value === '' ? fallback : value);
|
|
}
|
|
|
|
function getCanvasSize() {
|
|
return {
|
|
width: Math.max(1, Number(canvasWidthInput.value || 1920)),
|
|
height: Math.max(1, Number(canvasHeightInput.value || 1080))
|
|
};
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return utils.clamp ? utils.clamp(value, min, max) : Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
function normalizeLockRatio(value) {
|
|
return utils.normalizeLockRatio ? utils.normalizeLockRatio(value) : String(value || '').trim().replace(/\s+/g, '');
|
|
}
|
|
|
|
function parseLockRatio(value) {
|
|
if (utils.parseLockRatio) {
|
|
return utils.parseLockRatio(value);
|
|
}
|
|
var normalized = normalizeLockRatio(value);
|
|
if (!normalized || !/^\d+:\d+$/.test(normalized)) {
|
|
return null;
|
|
}
|
|
var parts = normalized.split(':');
|
|
var width = Number(parts[0]);
|
|
var height = Number(parts[1]);
|
|
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
return null;
|
|
}
|
|
return { label: normalized, ratio: width / height };
|
|
}
|
|
|
|
function getDefaultRegionSize(regionType, lockRatio) {
|
|
if (utils.getDefaultRegionSize) {
|
|
return utils.getDefaultRegionSize(regionType, lockRatio);
|
|
}
|
|
var ratio = parseLockRatio(lockRatio);
|
|
var locked = false;
|
|
if (ratio) {
|
|
if (ratio.ratio >= 1) {
|
|
var baseWidth = locked ? 420 : 300;
|
|
return { width: Math.max(12, Math.round(baseWidth)), height: Math.max(12, Math.round(baseWidth / ratio.ratio)) };
|
|
}
|
|
var baseHeight = locked ? 300 : 240;
|
|
return { width: Math.max(12, Math.round(baseHeight * ratio.ratio)), height: Math.max(12, Math.round(baseHeight)) };
|
|
}
|
|
return { width: locked ? 420 : 300, height: locked ? 240 : 120 };
|
|
}
|
|
|
|
function getRegionTypeEntries() {
|
|
if (window.pulseRegionTypes && typeof window.pulseRegionTypes.list === 'function') {
|
|
return window.pulseRegionTypes.list();
|
|
}
|
|
|
|
return ['api', 'html', 'image', 'rss', 'rtmp', 'text', 'time-date', 'video', 'webpage'].map(function (type) {
|
|
return { type: type, definition: window.pulseRegionTypes && typeof window.pulseRegionTypes.get === 'function' ? window.pulseRegionTypes.get(type) || {} : {} };
|
|
});
|
|
}
|
|
|
|
function formatRegionTypeLabel(regionType) {
|
|
return String(regionType || '')
|
|
.trim()
|
|
.replace(/(^|[_-])(\w)/g, function (_match, _prefix, letter) {
|
|
return String(letter || '').toUpperCase();
|
|
}) || 'Region';
|
|
}
|
|
|
|
function renderAddRegionOptions() {
|
|
if (!regionAddOptions) {
|
|
return;
|
|
}
|
|
|
|
regionAddOptions.innerHTML = getRegionTypeEntries().map(function (entry) {
|
|
var type = String(entry && entry.type ? entry.type : '').trim();
|
|
if (!type) {
|
|
return '';
|
|
}
|
|
|
|
var definition = entry.definition || {};
|
|
var label = definition.label || formatRegionTypeLabel(type);
|
|
return '<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="' + escapeHtml(type) + '">' + escapeHtml(label) + '</button>';
|
|
}).join('');
|
|
}
|
|
|
|
function getCards() {
|
|
return Array.prototype.slice.call(regionList.querySelectorAll('.region-item'));
|
|
}
|
|
|
|
function cardAt(index) {
|
|
return getCards()[index] || null;
|
|
}
|
|
|
|
function getRegionName(card) {
|
|
return utils.getRegionName ? utils.getRegionName(card) : String(card.querySelector('[name="region_name[]"]').value || '').trim();
|
|
}
|
|
|
|
function syncRegionIdentity(card, value) {
|
|
if (utils.syncRegionIdentity) {
|
|
utils.syncRegionIdentity(card, value);
|
|
return;
|
|
}
|
|
var next = String(value || '').trim();
|
|
card.querySelector('[name="region_name[]"]').value = next;
|
|
card.querySelector('[name="region_key[]"]').value = next;
|
|
card.querySelector('[name="region_label[]"]').value = next;
|
|
}
|
|
|
|
function normalizeRegionNamePrefix(regionType) {
|
|
var prefix = String(regionType || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
return prefix || 'region';
|
|
}
|
|
|
|
function buildDefaultRegionName(regionType, index) {
|
|
return normalizeRegionNamePrefix(regionType) + '_' + index;
|
|
}
|
|
|
|
function getNextDefaultRegionName(regionType) {
|
|
var normalizedType = normalizeRegionNamePrefix(regionType);
|
|
var nextIndex = getCards().reduce(function (count, card) {
|
|
var currentType = normalizeRegionNamePrefix(card.querySelector('[name="region_type[]"]').value || '');
|
|
if (currentType !== normalizedType) {
|
|
return count;
|
|
}
|
|
|
|
var currentName = getRegionName(card);
|
|
var expectedPrefix = normalizedType + '_';
|
|
if (currentName.indexOf(expectedPrefix) !== 0) {
|
|
return count;
|
|
}
|
|
|
|
var suffix = Number(currentName.slice(expectedPrefix.length));
|
|
if (!Number.isFinite(suffix) || suffix < 1) {
|
|
return count;
|
|
}
|
|
|
|
return Math.max(count, suffix);
|
|
}, 0) + 1;
|
|
|
|
return buildDefaultRegionName(normalizedType, nextIndex);
|
|
}
|
|
|
|
function validateRegionNames() {
|
|
var cards = getCards();
|
|
var names = {};
|
|
var hasDuplicate = false;
|
|
|
|
cards.forEach(function (card) {
|
|
var input = card.querySelector('[name="region_name[]"]');
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
var normalized = String(input.value || '').trim().toLowerCase();
|
|
if (!normalized) {
|
|
input.setCustomValidity('Region name is required.');
|
|
return;
|
|
}
|
|
|
|
if (!names[normalized]) {
|
|
names[normalized] = [];
|
|
}
|
|
names[normalized].push(input);
|
|
});
|
|
|
|
Object.keys(names).forEach(function (key) {
|
|
var inputs = names[key];
|
|
if (inputs.length > 1) {
|
|
hasDuplicate = true;
|
|
inputs.forEach(function (input) {
|
|
input.setCustomValidity('Region names must be unique on this template.');
|
|
});
|
|
} else {
|
|
inputs[0].setCustomValidity('');
|
|
}
|
|
});
|
|
|
|
return !hasDuplicate;
|
|
}
|
|
|
|
function notifyRegionNameValidationError(message) {
|
|
if (regionNameValidationToastShown) {
|
|
return;
|
|
}
|
|
|
|
regionNameValidationToastShown = true;
|
|
if (typeof showToast === 'function') {
|
|
showToast(message || 'Region names must be unique on this template.', 'warning');
|
|
}
|
|
}
|
|
|
|
function readCard(card) {
|
|
return utils.readCard ? utils.readCard(card) : {
|
|
region_key: getRegionName(card),
|
|
label: getRegionName(card),
|
|
region_type: card.querySelector('[name="region_type[]"]').value,
|
|
lock_ratio: normalizeLockRatio(card.querySelector('[name="region_lock_ratio[]"]').value),
|
|
animation_json: getAnimationConfig(card),
|
|
x: Number(card.querySelector('[name="region_x[]"]').value || 0),
|
|
y: Number(card.querySelector('[name="region_y[]"]').value || 0),
|
|
width: Number(card.querySelector('[name="region_width[]"]').value || 0),
|
|
height: Number(card.querySelector('[name="region_height[]"]').value || 0),
|
|
z_index: Number(card.querySelector('[name="region_z[]"]').value || 0)
|
|
};
|
|
}
|
|
|
|
function hasAnyAnimationCards() {
|
|
return getCards().some(function (card) {
|
|
var hiddenAnimationInput = card.querySelector('[name="region_animation_json[]"]');
|
|
var animationConfig = normalizeAnimationConfig(hiddenAnimationInput ? hiddenAnimationInput.value || '{}' : '{}');
|
|
return animationConfig && ['intro', 'outro', 'loop'].some(function (stepName) {
|
|
var step = animationConfig[stepName] || {};
|
|
return normalizePreviewAnimation(step.preset) !== 'none';
|
|
});
|
|
});
|
|
}
|
|
|
|
function hasPreviewAnimationForMode(mode) {
|
|
var previewMode = String(mode || '').trim().toLowerCase();
|
|
if (!previewMode) {
|
|
return false;
|
|
}
|
|
|
|
return getCards().some(function (card) {
|
|
var step = getPreviewRegionAnimationStep(readCard(card), previewMode);
|
|
return normalizePreviewAnimation(step && step.preset) !== 'none';
|
|
});
|
|
}
|
|
|
|
function getPreviewRects() {
|
|
if (!overlay) {
|
|
return [];
|
|
}
|
|
|
|
return Array.prototype.slice.call(overlay.querySelectorAll('.designer-rect'));
|
|
}
|
|
|
|
function clearPreviewAnimations() {
|
|
previewState.animations.forEach(function (animation) {
|
|
if (animation && typeof animation.cancel === 'function') {
|
|
animation.cancel();
|
|
}
|
|
});
|
|
previewState.animations = [];
|
|
|
|
getPreviewRects().forEach(function (rect) {
|
|
rect.classList.remove('animate__animated', 'animate__infinite');
|
|
rect.classList.remove('animate__repeat-1', 'animate__repeat-2', 'animate__repeat-3');
|
|
if (window.templateAnimationPresets && Array.isArray(window.templateAnimationPresets.animateCssValues)) {
|
|
window.templateAnimationPresets.animateCssValues.forEach(function (value) {
|
|
if (value && value !== 'none') {
|
|
rect.classList.remove('animate__' + value);
|
|
}
|
|
});
|
|
}
|
|
rect.style.removeProperty('--animate-duration');
|
|
rect.style.removeProperty('--animate-delay');
|
|
rect.style.removeProperty('--animate-repeat');
|
|
rect.style.opacity = '';
|
|
rect.style.transform = '';
|
|
rect.style.filter = '';
|
|
rect.style.willChange = '';
|
|
});
|
|
}
|
|
|
|
function updatePreviewButtonIcon(button, iconClass) {
|
|
if (!button) {
|
|
return;
|
|
}
|
|
|
|
var icon = button.querySelector('[data-preview-icon]');
|
|
if (!icon) {
|
|
return;
|
|
}
|
|
|
|
icon.className = 'bi ' + iconClass + ' me-1';
|
|
}
|
|
|
|
function updatePreviewControlState() {
|
|
var activeMode = previewState.active ? previewState.mode : '';
|
|
|
|
previewButtons.forEach(function (entry) {
|
|
if (!entry.button) {
|
|
return;
|
|
}
|
|
|
|
var available = hasPreviewAnimationForMode(entry.mode);
|
|
var disabled = !available || (previewState.active && activeMode !== entry.mode);
|
|
entry.button.disabled = disabled;
|
|
entry.button.classList.toggle('btn-outline-secondary', disabled);
|
|
entry.button.classList.toggle('btn-info', !disabled);
|
|
updatePreviewButtonIcon(entry.button, previewState.active && activeMode === entry.mode ? 'bi-stop-fill' : 'bi-play-fill');
|
|
entry.button.setAttribute('aria-pressed', previewState.active && activeMode === entry.mode ? 'true' : 'false');
|
|
});
|
|
}
|
|
|
|
function normalizePreviewAnimation(value) {
|
|
if (window.templateAnimationPresets && typeof window.templateAnimationPresets.normalize === 'function') {
|
|
return window.templateAnimationPresets.normalize(value);
|
|
}
|
|
|
|
var allowed = {};
|
|
var presets = window.templateAnimationPresets || {};
|
|
var values = Array.isArray(presets.allValues) && presets.allValues.length ? presets.allValues : [
|
|
'none', 'fadeIn', 'fadeInDown', 'fadeInLeft', 'fadeInRight', 'fadeInUp', 'zoomIn', 'zoomInDown',
|
|
'zoomInLeft', 'zoomInRight', 'zoomInUp', 'slideInDown', 'slideInLeft', 'slideInRight', 'slideInUp',
|
|
'fadeOut', 'fadeOutDown', 'fadeOutLeft', 'fadeOutRight', 'fadeOutUp', 'zoomOut', 'zoomOutDown',
|
|
'zoomOutLeft', 'zoomOutRight', 'zoomOutUp', 'slideOutDown', 'slideOutLeft', 'slideOutRight', 'slideOutUp',
|
|
'backInDown', 'backInLeft', 'backInRight', 'backInUp', 'backOutDown', 'backOutLeft', 'backOutRight', 'backOutUp',
|
|
'bounceIn', 'bounceInDown', 'bounceInLeft', 'bounceInRight', 'bounceInUp', 'bounceOut', 'bounceOutDown', 'bounceOutLeft', 'bounceOutRight', 'bounceOutUp',
|
|
'flip', 'flipInX', 'flipInY', 'flipOutX', 'flipOutY', 'lightSpeedInLeft', 'lightSpeedInRight', 'lightSpeedOutLeft', 'lightSpeedOutRight',
|
|
'rotateIn', 'rotateInDownLeft', 'rotateInDownRight', 'rotateInUpLeft', 'rotateInUpRight', 'rotateOut', 'rotateOutDownLeft', 'rotateOutDownRight', 'rotateOutUpLeft', 'rotateOutUpRight',
|
|
'hinge', 'jackInTheBox', 'rollIn', 'rollOut', 'flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY', 'headShake', 'swing', 'tada', 'wobble', 'jello', 'heartBeat', 'bounce'
|
|
];
|
|
|
|
values.forEach(function (valueName) {
|
|
allowed[valueName] = true;
|
|
});
|
|
|
|
var raw = String(value || '').trim().toLowerCase();
|
|
return allowed[raw] ? raw : 'none';
|
|
}
|
|
|
|
function isAnimateCssPreset(value) {
|
|
var presets = window.templateAnimationPresets || {};
|
|
if (Array.isArray(presets.animateCssValues) && presets.animateCssValues.length) {
|
|
return presets.animateCssValues.indexOf(normalizePreviewAnimation(value)) !== -1;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function applyAnimateCssPreview(rect, preset, step, mode) {
|
|
var animateClass = 'animate__' + preset;
|
|
var durationMs = Number(step && step.duration_ms);
|
|
var delayMs = Number(step && step.delay_ms);
|
|
var repeatCount = Number(step && step.iterations);
|
|
var isAttentionSeeker = preset === 'bounce' || preset === 'flash' || preset === 'pulse' || preset === 'rubberBand' || preset === 'shakeX' || preset === 'shakeY' || preset === 'headShake' || preset === 'swing' || preset === 'tada' || preset === 'wobble' || preset === 'jello' || preset === 'heartBeat';
|
|
|
|
rect.classList.remove('animate__animated', 'animate__infinite');
|
|
rect.style.removeProperty('--animate-duration');
|
|
rect.style.removeProperty('--animate-delay');
|
|
rect.style.removeProperty('--animate-repeat');
|
|
rect.style.setProperty('--animate-duration', Math.max(0, Number.isFinite(durationMs) && durationMs > 0 ? durationMs : 1000) + 'ms');
|
|
if (Number.isFinite(delayMs) && delayMs > 0) {
|
|
rect.style.setProperty('--animate-delay', Math.max(0, delayMs) + 'ms');
|
|
}
|
|
rect.classList.add('animate__animated', animateClass);
|
|
if (isAttentionSeeker) {
|
|
var normalizedRepeatCount = Math.max(1, Number.isFinite(repeatCount) && repeatCount > 0 ? repeatCount : 1);
|
|
rect.classList.remove('animate__repeat-1', 'animate__repeat-2', 'animate__repeat-3');
|
|
rect.classList.add('animate__repeat-1');
|
|
rect.style.setProperty('--animate-repeat', String(normalizedRepeatCount));
|
|
}
|
|
}
|
|
|
|
var MAX_ANIMATION_REPEAT_COUNT = 999;
|
|
|
|
function getPreviewAnimationStepTimingMs(step, mode, animateCssMode) {
|
|
var normalizedMode = String(mode || '').trim().toLowerCase();
|
|
var durationMs = Number(step && step.duration_ms);
|
|
var delayMs = Number(step && step.delay_ms);
|
|
var iterations = Number(step && step.iterations);
|
|
|
|
if (!Number.isFinite(durationMs) || durationMs <= 0) {
|
|
durationMs = animateCssMode ? 1000 : 900;
|
|
}
|
|
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
|
delayMs = 0;
|
|
}
|
|
if (!Number.isFinite(iterations) || iterations < 1) {
|
|
iterations = 1;
|
|
}
|
|
|
|
if (normalizedMode === 'continuous') {
|
|
return delayMs + (durationMs * iterations);
|
|
}
|
|
|
|
return delayMs + durationMs;
|
|
}
|
|
|
|
function normalizeAnimationStep(value, fallbackPreset) {
|
|
if (utils.normalizeAnimationStep) {
|
|
return utils.normalizeAnimationStep(value, fallbackPreset);
|
|
}
|
|
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
var normalized = {};
|
|
Object.keys(value).forEach(function (key) {
|
|
normalized[key] = value[key];
|
|
});
|
|
normalized.preset = normalizePreviewAnimation(value.preset !== undefined ? value.preset : fallbackPreset || 'none');
|
|
normalized.duration_ms = Number.isFinite(Number(value.duration_ms)) && Number(value.duration_ms) > 0 ? Math.round(Number(value.duration_ms)) : null;
|
|
normalized.delay_ms = Number.isFinite(Number(value.delay_ms)) && Number(value.delay_ms) >= 0 ? Math.round(Number(value.delay_ms)) : null;
|
|
normalized.iterations = Number.isFinite(Number(value.iterations)) && Number(value.iterations) > 0
|
|
? Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, Math.round(Number(value.iterations))))
|
|
: null;
|
|
normalized.fill_mode = String(value.fill_mode || '').trim();
|
|
return normalized;
|
|
}
|
|
|
|
return {
|
|
preset: normalizePreviewAnimation(typeof value === 'string' ? value : fallbackPreset || 'none'),
|
|
duration_ms: null,
|
|
delay_ms: null,
|
|
iterations: null,
|
|
fill_mode: ''
|
|
};
|
|
}
|
|
|
|
function normalizeAnimationConfig(value) {
|
|
if (utils.normalizeAnimationConfig) {
|
|
return utils.normalizeAnimationConfig(value);
|
|
}
|
|
|
|
var raw = value;
|
|
if (typeof raw === 'string') {
|
|
var text = String(raw || '').trim();
|
|
if (!text) {
|
|
raw = null;
|
|
} else {
|
|
try {
|
|
raw = JSON.parse(text);
|
|
} catch (_error) {
|
|
raw = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
raw = {};
|
|
}
|
|
|
|
return {
|
|
intro: normalizeAnimationStep(raw.intro, 'none'),
|
|
outro: normalizeAnimationStep(raw.outro !== undefined ? raw.outro : raw.out, 'none'),
|
|
loop: normalizeAnimationStep(raw.loop, 'none')
|
|
};
|
|
}
|
|
|
|
function getPreviewRegionAnimationStep(region, mode) {
|
|
if (!region) {
|
|
return normalizeAnimationStep({}, 'none');
|
|
}
|
|
|
|
var animationConfig = normalizeAnimationConfig(region.animation_json !== undefined ? region.animation_json : region);
|
|
|
|
if (mode === 'intro') {
|
|
return animationConfig.intro;
|
|
}
|
|
|
|
if (mode === 'out') {
|
|
return animationConfig.outro;
|
|
}
|
|
|
|
return animationConfig.loop;
|
|
}
|
|
|
|
function getAnimationConfig(card) {
|
|
var hiddenAnimationInput = card.querySelector('[name="region_animation_json[]"]');
|
|
var animationConfig = normalizeAnimationConfig(hiddenAnimationInput ? hiddenAnimationInput.value || '{}' : '{}');
|
|
var introPresetInput = card.querySelector('[data-animation-preset="intro"]');
|
|
var outroPresetInput = card.querySelector('[data-animation-preset="outro"]');
|
|
var loopPresetInput = card.querySelector('[data-animation-preset="loop"]');
|
|
|
|
if (introPresetInput && introPresetInput.dataset.animationPresetCustom !== '1' && introPresetInput.selectedIndex >= 0) {
|
|
animationConfig.intro.preset = normalizePreviewAnimation(introPresetInput.value);
|
|
}
|
|
if (outroPresetInput && outroPresetInput.dataset.animationPresetCustom !== '1' && outroPresetInput.selectedIndex >= 0) {
|
|
animationConfig.outro.preset = normalizePreviewAnimation(outroPresetInput.value);
|
|
}
|
|
if (loopPresetInput && loopPresetInput.dataset.animationPresetCustom !== '1' && loopPresetInput.selectedIndex >= 0) {
|
|
animationConfig.loop.preset = normalizePreviewAnimation(loopPresetInput.value);
|
|
}
|
|
|
|
return animationConfig;
|
|
}
|
|
|
|
function syncAnimationPresetCustomState(selectInput, resetButton, preset) {
|
|
if (!selectInput) {
|
|
return;
|
|
}
|
|
|
|
var normalizedPreset = normalizePreviewAnimation(preset);
|
|
var hasMatchingOption = Array.prototype.some.call(selectInput.options || [], function (option) {
|
|
return option && option.value === normalizedPreset;
|
|
});
|
|
|
|
if (hasMatchingOption) {
|
|
delete selectInput.dataset.animationPresetCustom;
|
|
selectInput.hidden = false;
|
|
if (resetButton) {
|
|
resetButton.hidden = true;
|
|
}
|
|
} else {
|
|
selectInput.dataset.animationPresetCustom = '1';
|
|
selectInput.hidden = true;
|
|
if (resetButton) {
|
|
resetButton.hidden = false;
|
|
resetButton.textContent = 'Reset to basic';
|
|
}
|
|
}
|
|
}
|
|
|
|
function writeAnimationConfig(card, animationConfig) {
|
|
var normalized = normalizeAnimationConfig(animationConfig);
|
|
var hiddenAnimationInput = card.querySelector('[name="region_animation_json[]"]');
|
|
var introPresetInput = card.querySelector('[data-animation-preset="intro"]');
|
|
var outroPresetInput = card.querySelector('[data-animation-preset="outro"]');
|
|
var loopPresetInput = card.querySelector('[data-animation-preset="loop"]');
|
|
var introResetButton = card.querySelector('[data-animation-reset-button="intro"]');
|
|
var outroResetButton = card.querySelector('[data-animation-reset-button="outro"]');
|
|
var loopResetButton = card.querySelector('[data-animation-reset-button="loop"]');
|
|
|
|
if (hiddenAnimationInput) {
|
|
hiddenAnimationInput.value = JSON.stringify(normalized, null, 2);
|
|
}
|
|
if (introPresetInput) {
|
|
introPresetInput.value = normalized.intro.preset;
|
|
syncAnimationPresetCustomState(introPresetInput, introResetButton, normalized.intro.preset);
|
|
}
|
|
if (outroPresetInput) {
|
|
outroPresetInput.value = normalized.outro.preset;
|
|
syncAnimationPresetCustomState(outroPresetInput, outroResetButton, normalized.outro.preset);
|
|
}
|
|
if (loopPresetInput) {
|
|
loopPresetInput.value = normalized.loop.preset;
|
|
syncAnimationPresetCustomState(loopPresetInput, loopResetButton, normalized.loop.preset);
|
|
}
|
|
|
|
updatePreviewCardVisibility();
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function setAnimationPreset(card, stepName, preset) {
|
|
var config = getAnimationConfig(card);
|
|
if (config[stepName]) {
|
|
config[stepName].preset = normalizePreviewAnimation(preset);
|
|
config[stepName].duration_ms = null;
|
|
config[stepName].delay_ms = null;
|
|
config[stepName].iterations = null;
|
|
config[stepName].easing = '';
|
|
config[stepName].fill_mode = '';
|
|
writeAnimationConfig(card, config);
|
|
if (activeAnimationCard === card) {
|
|
populateAnimationModal(card);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getAnimationModalField(stepName, fieldName) {
|
|
if (!animationAdvancedModal) {
|
|
return null;
|
|
}
|
|
|
|
return animationAdvancedModal.querySelector('[data-animation-modal-step="' + stepName + '"][data-animation-modal-field="' + fieldName + '"]');
|
|
}
|
|
|
|
function populateAnimationModal(card) {
|
|
if (!animationAdvancedModal || !card) {
|
|
return;
|
|
}
|
|
|
|
var title = animationAdvancedModal.querySelector('[data-animation-modal-title]');
|
|
var label = card.querySelector('[data-region-title]');
|
|
var config = getAnimationConfig(card);
|
|
|
|
if (title) {
|
|
title.textContent = (label ? label.textContent : 'Region') + ' animation settings';
|
|
}
|
|
|
|
['intro', 'outro', 'loop'].forEach(function (stepName) {
|
|
var step = config[stepName] || normalizeAnimationStep({}, 'none');
|
|
var presetInput = getAnimationModalField(stepName, 'preset');
|
|
var durationInput = getAnimationModalField(stepName, 'duration_ms');
|
|
var delayInput = getAnimationModalField(stepName, 'delay_ms');
|
|
|
|
if (presetInput) { presetInput.value = step.preset; }
|
|
if (durationInput) { durationInput.value = String(getAnimationDurationPercent(step.duration_ms)); }
|
|
if (delayInput) { delayInput.value = step.delay_ms === null || step.delay_ms === undefined ? '0' : String(step.delay_ms); }
|
|
if (stepName === 'loop') {
|
|
var iterationsInput = getAnimationModalField(stepName, 'iterations');
|
|
if (iterationsInput) { iterationsInput.value = step.iterations === null || step.iterations === undefined ? '1' : String(Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, Number(step.iterations) || 1))); }
|
|
}
|
|
});
|
|
}
|
|
|
|
function readNumberField(value) {
|
|
var raw = String(value || '').trim();
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
var next = Number(raw);
|
|
return Number.isFinite(next) ? Math.round(next) : null;
|
|
}
|
|
|
|
function clampAnimationModalField(field) {
|
|
if (!field || !field.matches) {
|
|
return;
|
|
}
|
|
|
|
var isLoopIterationsField = field.matches('[data-animation-modal-step="loop"][data-animation-modal-field="iterations"]');
|
|
var isDurationField = field.matches('[data-animation-modal-field="duration_ms"]');
|
|
if (!isLoopIterationsField && !isDurationField) {
|
|
return;
|
|
}
|
|
|
|
var fieldValue = readNumberField(field.value);
|
|
if (fieldValue === null) {
|
|
return;
|
|
}
|
|
|
|
var normalizedValue = isLoopIterationsField
|
|
? Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, fieldValue))
|
|
: Math.min(200, Math.max(1, fieldValue));
|
|
if (String(normalizedValue) !== String(field.value)) {
|
|
field.value = String(normalizedValue);
|
|
}
|
|
}
|
|
|
|
function getAnimationDurationPercent(durationMs) {
|
|
var value = Number(durationMs);
|
|
if (!Number.isFinite(value) || value <= 0) {
|
|
return 100;
|
|
}
|
|
|
|
return Math.min(200, Math.max(1, Math.round(value / 10)));
|
|
}
|
|
|
|
function getAnimationDurationMs(durationPercent) {
|
|
var percent = Math.min(200, Math.max(1, readNumberField(durationPercent) || 100));
|
|
return percent * 10;
|
|
}
|
|
|
|
function readAnimationModalConfig() {
|
|
if (!animationAdvancedModal) {
|
|
return null;
|
|
}
|
|
|
|
var config = getAnimationConfig(activeAnimationCard) || { intro: {}, outro: {}, loop: {} };
|
|
|
|
['intro', 'outro', 'loop'].forEach(function (stepName) {
|
|
var presetInput = getAnimationModalField(stepName, 'preset');
|
|
var durationInput = getAnimationModalField(stepName, 'duration_ms');
|
|
var delayInput = getAnimationModalField(stepName, 'delay_ms');
|
|
|
|
var nextStep = Object.assign({}, config[stepName] || {});
|
|
nextStep.preset = presetInput ? presetInput.value : nextStep.preset;
|
|
nextStep.duration_ms = getAnimationDurationMs(durationInput ? durationInput.value : '');
|
|
nextStep.delay_ms = readNumberField(delayInput ? delayInput.value : '') || 0;
|
|
if (stepName === 'loop') {
|
|
var iterationsInput = getAnimationModalField(stepName, 'iterations');
|
|
nextStep.iterations = Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, readNumberField(iterationsInput ? iterationsInput.value : '') || 1));
|
|
}
|
|
|
|
config[stepName] = normalizeAnimationStep(nextStep, 'none');
|
|
});
|
|
|
|
return config;
|
|
}
|
|
|
|
function saveAnimationModal() {
|
|
if (!activeAnimationCard) {
|
|
return;
|
|
}
|
|
|
|
var config = readAnimationModalConfig();
|
|
if (!config) {
|
|
return;
|
|
}
|
|
|
|
writeAnimationConfig(activeAnimationCard, config);
|
|
renderOverlay();
|
|
}
|
|
|
|
function setPreviewControlState(active) {
|
|
updatePreviewControlState();
|
|
}
|
|
|
|
function updatePreviewCardVisibility() {
|
|
if (!previewCard) {
|
|
return;
|
|
}
|
|
|
|
previewCard.hidden = !hasAnyAnimationCards();
|
|
}
|
|
|
|
function setTemplateEditorLocked(locked) {
|
|
if (templateForm) {
|
|
templateForm.classList.toggle('template-designer-form--previewing', locked);
|
|
}
|
|
if (stage) {
|
|
stage.classList.toggle('is-previewing', locked);
|
|
}
|
|
if (previewCard) {
|
|
previewCard.classList.toggle('is-previewing', locked);
|
|
}
|
|
if (overlay) {
|
|
overlay.style.pointerEvents = locked ? 'none' : '';
|
|
}
|
|
|
|
var controls = templateForm ? templateForm.querySelectorAll('input, select, textarea, button') : [];
|
|
Array.prototype.forEach.call(controls, function (control) {
|
|
if (!control) {
|
|
return;
|
|
}
|
|
if (control.matches && control.matches('[data-preview-control]')) {
|
|
return;
|
|
}
|
|
|
|
if (locked) {
|
|
if (!control.hasAttribute('data-preview-disabled')) {
|
|
control.setAttribute('data-preview-disabled', control.disabled ? '1' : '0');
|
|
}
|
|
control.disabled = true;
|
|
} else if (control.hasAttribute('data-preview-disabled')) {
|
|
control.disabled = control.getAttribute('data-preview-disabled') === '1';
|
|
control.removeAttribute('data-preview-disabled');
|
|
}
|
|
});
|
|
|
|
setPreviewControlState(locked);
|
|
}
|
|
|
|
function stopTemplatePreview(skipRender) {
|
|
if (previewState.stopTimer) {
|
|
window.clearTimeout(previewState.stopTimer);
|
|
previewState.stopTimer = 0;
|
|
}
|
|
|
|
previewState.token += 1;
|
|
previewState.active = false;
|
|
previewState.mode = '';
|
|
clearPreviewAnimations();
|
|
setTemplateEditorLocked(false);
|
|
|
|
if (!skipRender) {
|
|
renderOverlay();
|
|
}
|
|
}
|
|
|
|
function schedulePreviewStop(delayMs, token) {
|
|
if (previewState.stopTimer) {
|
|
window.clearTimeout(previewState.stopTimer);
|
|
}
|
|
|
|
previewState.stopTimer = window.setTimeout(function () {
|
|
if (token !== previewState.token) {
|
|
return;
|
|
}
|
|
stopTemplatePreview(false);
|
|
}, Math.max(0, Number(delayMs || 0)));
|
|
}
|
|
|
|
function animatePreviewRects(mode, regions) {
|
|
var rects = getPreviewRects();
|
|
var token = previewState.token;
|
|
var stagger = 90;
|
|
var duration = 900;
|
|
var overlayRect = getOverlayRect();
|
|
var canvasMovement = Math.max(12, Math.round(overlayRect.height * 0.02));
|
|
var longestTimingMs = 0;
|
|
|
|
previewState.animations = [];
|
|
|
|
rects.forEach(function (rect, index) {
|
|
var region = regions && regions[index] ? regions[index] : null;
|
|
var step = getPreviewRegionAnimationStep(region, mode);
|
|
var preset = normalizePreviewAnimation(step && step.preset);
|
|
var delay = index * stagger;
|
|
var animation = null;
|
|
var animateCssMode = isAnimateCssPreset(preset);
|
|
var timingMs = delay + getPreviewAnimationStepTimingMs(step, mode, animateCssMode);
|
|
|
|
rect.style.willChange = 'transform, opacity, filter';
|
|
|
|
if (timingMs > longestTimingMs) {
|
|
longestTimingMs = timingMs;
|
|
}
|
|
|
|
if (preset === 'none') {
|
|
return;
|
|
}
|
|
|
|
if (animateCssMode) {
|
|
applyAnimateCssPreview(rect, preset, step, mode);
|
|
return;
|
|
}
|
|
if (mode === 'intro') {
|
|
animation = rect.animate([
|
|
{ opacity: 0, transform: 'translate3d(0, ' + canvasMovement + 'px, 0) scale(0.96)', filter: 'blur(1px)' },
|
|
{ opacity: 1, transform: 'translate3d(0, 0, 0) scale(1)', filter: 'blur(0)' }
|
|
], {
|
|
duration: duration,
|
|
delay: delay,
|
|
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
fill: 'both'
|
|
});
|
|
} else if (mode === 'out') {
|
|
animation = rect.animate([
|
|
{ opacity: 1, transform: 'translate3d(0, 0, 0) scale(1)', filter: 'blur(0)' },
|
|
{ opacity: 0, transform: 'translate3d(0, -' + canvasMovement + 'px, 0) scale(0.96)', filter: 'blur(1px)' }
|
|
], {
|
|
duration: duration,
|
|
delay: delay,
|
|
easing: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
fill: 'both'
|
|
});
|
|
}
|
|
|
|
if (animation) {
|
|
previewState.animations.push(animation);
|
|
}
|
|
});
|
|
|
|
schedulePreviewStop(longestTimingMs + 140, token);
|
|
}
|
|
|
|
function startTemplatePreview(mode) {
|
|
var previewMode = String(mode || '').trim().toLowerCase();
|
|
if (!previewMode) {
|
|
return;
|
|
}
|
|
|
|
var previewRegions = getCards().map(readCard);
|
|
stopTemplatePreview(true);
|
|
previewState.active = true;
|
|
previewState.mode = previewMode;
|
|
previewState.token += 1;
|
|
setTemplateEditorLocked(true);
|
|
|
|
if (!getPreviewRects().length) {
|
|
schedulePreviewStop(120, previewState.token);
|
|
return;
|
|
}
|
|
animatePreviewRects(previewMode, previewRegions);
|
|
}
|
|
|
|
function writeCard(card, values) {
|
|
if (utils.writeCard) {
|
|
utils.writeCard(card, values);
|
|
return;
|
|
}
|
|
if (values.region_name !== undefined) {
|
|
syncRegionIdentity(card, values.region_name);
|
|
} else if (values.region_key !== undefined) {
|
|
syncRegionIdentity(card, values.region_key);
|
|
} else if (values.label !== undefined) {
|
|
syncRegionIdentity(card, values.label);
|
|
}
|
|
if (values.region_type !== undefined) { card.querySelector('[name="region_type[]"]').value = values.region_type; }
|
|
if (values.lock_ratio !== undefined) { card.querySelector('[name="region_lock_ratio[]"]').value = normalizeLockRatio(values.lock_ratio); }
|
|
if (values.animation_json !== undefined) { writeAnimationConfig(card, values.animation_json); }
|
|
if (values.x !== undefined) { card.querySelector('[name="region_x[]"]').value = Math.round(values.x); }
|
|
if (values.y !== undefined) { card.querySelector('[name="region_y[]"]').value = Math.round(values.y); }
|
|
if (values.width !== undefined) { card.querySelector('[name="region_width[]"]').value = Math.round(values.width); }
|
|
if (values.height !== undefined) { card.querySelector('[name="region_height[]"]').value = Math.round(values.height); }
|
|
if (values.z_index !== undefined) { card.querySelector('[name="region_z[]"]').value = Math.round(values.z_index); }
|
|
}
|
|
|
|
function getOverlayRect() {
|
|
return utils.getOverlayRect ? utils.getOverlayRect(overlay) : overlay.getBoundingClientRect();
|
|
}
|
|
|
|
function toCanvasPoint(event) {
|
|
return utils.toCanvasPoint ? utils.toCanvasPoint(event, overlay, getCanvasSize()) : {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
}
|
|
|
|
function canvasRectToPixels(region) {
|
|
return utils.canvasRectToPixels ? utils.canvasRectToPixels(region, overlay, getCanvasSize()) : {
|
|
left: 0,
|
|
top: 0,
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
}
|
|
|
|
function updateAspectRatio() {
|
|
var size = getCanvasSize();
|
|
stage.style.aspectRatio = size.width + ' / ' + size.height;
|
|
}
|
|
|
|
function updateCanvasSizeSummary() {
|
|
var option = canvasSizeSelect.options[canvasSizeSelect.selectedIndex];
|
|
canvasSizeSummary.textContent = option ? option.textContent : '';
|
|
}
|
|
|
|
function updateCanvasSizeLock() {
|
|
var lockOnExistingTemplate = canvasSizeSelect.dataset.lockOnExistingTemplate === 'true';
|
|
var locked = lockOnExistingTemplate && getCards().length > 0;
|
|
canvasSizeSelect.disabled = locked;
|
|
canvasSizeSummary.classList.toggle('is-locked', locked);
|
|
}
|
|
|
|
function syncCanvasSizeSelection() {
|
|
var option = canvasSizeSelect.options[canvasSizeSelect.selectedIndex];
|
|
if (!option) {
|
|
return;
|
|
}
|
|
if (canvasSizeIdInput) {
|
|
canvasSizeIdInput.value = option.value;
|
|
}
|
|
canvasWidthInput.value = Math.max(1, Number(option.dataset.width || canvasWidthInput.value || 1920));
|
|
canvasHeightInput.value = Math.max(1, Number(option.dataset.height || canvasHeightInput.value || 1080));
|
|
updateAspectRatio();
|
|
updateCanvasSizeSummary();
|
|
getCards().forEach(function (card) {
|
|
normalizeRegionCard(card, 'width');
|
|
});
|
|
}
|
|
|
|
function fitRegionToCanvas(region, aspect) {
|
|
var size = getCanvasSize();
|
|
var minSize = 12;
|
|
var next = {
|
|
x: Math.round(Number(region && region.x !== undefined ? region.x : 0)),
|
|
y: Math.round(Number(region && region.y !== undefined ? region.y : 0)),
|
|
width: Math.max(minSize, Math.round(Number(region && region.width !== undefined ? region.width : minSize))),
|
|
height: Math.max(minSize, Math.round(Number(region && region.height !== undefined ? region.height : minSize)))
|
|
};
|
|
|
|
if (aspect && Number.isFinite(aspect.ratio) && aspect.ratio > 0) {
|
|
if (next.width > size.width || next.height > size.height) {
|
|
var canvasRatio = size.width / Math.max(1, size.height);
|
|
if (canvasRatio >= aspect.ratio) {
|
|
next.height = Math.max(minSize, Math.min(size.height, next.height, Math.round(size.height)));
|
|
next.width = Math.max(minSize, Math.round(next.height * aspect.ratio));
|
|
if (next.width > size.width) {
|
|
next.width = size.width;
|
|
next.height = Math.max(minSize, Math.round(next.width / aspect.ratio));
|
|
}
|
|
} else {
|
|
next.width = Math.max(minSize, Math.min(size.width, next.width, Math.round(size.width)));
|
|
next.height = Math.max(minSize, Math.round(next.width / aspect.ratio));
|
|
if (next.height > size.height) {
|
|
next.height = size.height;
|
|
next.width = Math.max(minSize, Math.round(next.height * aspect.ratio));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (next.width > size.width) {
|
|
next.width = size.width;
|
|
next.x = 0;
|
|
}
|
|
if (next.height > size.height) {
|
|
next.height = size.height;
|
|
next.y = 0;
|
|
}
|
|
}
|
|
|
|
if (next.x + next.width > size.width) {
|
|
next.x = size.width - next.width;
|
|
}
|
|
if (next.y + next.height > size.height) {
|
|
next.y = size.height - next.height;
|
|
}
|
|
|
|
next.x = clamp(next.x, 0, Math.max(0, size.width - next.width));
|
|
next.y = clamp(next.y, 0, Math.max(0, size.height - next.height));
|
|
|
|
return next;
|
|
}
|
|
|
|
function normalizeRegionCard(card, changedField) {
|
|
if (!card) {
|
|
return null;
|
|
}
|
|
|
|
var current = readCard(card);
|
|
var next = {
|
|
x: current.x,
|
|
y: current.y,
|
|
width: Math.max(12, Math.round(current.width || 0)),
|
|
height: Math.max(12, Math.round(current.height || 0))
|
|
};
|
|
var lockRatio = normalizeLockRatio(current.lock_ratio);
|
|
var aspect = lockRatio ? parseLockRatio(lockRatio) : null;
|
|
var size = getCanvasSize();
|
|
|
|
if (aspect) {
|
|
if (changedField === 'height') {
|
|
next.width = Math.max(12, Math.round(next.height * aspect.ratio));
|
|
} else {
|
|
next.height = Math.max(12, Math.round(next.width / aspect.ratio));
|
|
}
|
|
|
|
if (next.width > size.width || next.height > size.height) {
|
|
var scale = Math.min(size.width / Math.max(1, next.width), size.height / Math.max(1, next.height), 1);
|
|
if (scale < 1) {
|
|
next.width = Math.max(12, Math.round(next.width * scale));
|
|
next.height = Math.max(12, Math.round(next.height * scale));
|
|
if (changedField === 'height') {
|
|
next.width = Math.max(12, Math.round(next.height * aspect.ratio));
|
|
} else {
|
|
next.height = Math.max(12, Math.round(next.width / aspect.ratio));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
next = fitRegionToCanvas(next, aspect);
|
|
writeCard(card, next);
|
|
return next;
|
|
}
|
|
|
|
function updateBackgroundPreview(file) {
|
|
if (!file) {
|
|
return;
|
|
}
|
|
if (removeBackgroundFlag) {
|
|
removeBackgroundFlag.checked = false;
|
|
}
|
|
var reader = new FileReader();
|
|
reader.onload = function () {
|
|
backgroundPreview.src = reader.result;
|
|
backgroundPreview.style.display = 'block';
|
|
backgroundEmpty.style.display = 'none';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
function updateStageBackgroundColor() {
|
|
if (!stage) {
|
|
return;
|
|
}
|
|
stage.style.backgroundColor = backgroundColorInput && backgroundColorInput.value ? backgroundColorInput.value : '#111111';
|
|
}
|
|
|
|
function updateRegionLockBadge(card) {
|
|
var lockChip = card.querySelector('[data-region-lock-chip]');
|
|
if (!lockChip) {
|
|
return;
|
|
}
|
|
var lockRatio = normalizeLockRatio(card.querySelector('[name="region_lock_ratio[]"]').value);
|
|
if (!lockRatio) {
|
|
lockChip.hidden = true;
|
|
lockChip.textContent = '';
|
|
return;
|
|
}
|
|
lockChip.hidden = false;
|
|
lockChip.textContent = 'Locked ' + lockRatio;
|
|
}
|
|
|
|
function updateRegionLockRatioState(card) {
|
|
var lockRatioInput = card.querySelector('[name="region_lock_ratio[]"]');
|
|
var regionTypeInput = card.querySelector('[name="region_type[]"]');
|
|
if (!lockRatioInput || !regionTypeInput) {
|
|
return;
|
|
}
|
|
|
|
var isQrCode = String(regionTypeInput.value || '').trim() === 'qr-code';
|
|
lockRatioInput.disabled = isQrCode;
|
|
lockRatioInput.setAttribute('aria-disabled', isQrCode ? 'true' : 'false');
|
|
|
|
if (isQrCode) {
|
|
lockRatioInput.value = '1:1';
|
|
lockRatioInput.title = 'QR code regions are fixed to 1:1.';
|
|
} else {
|
|
lockRatioInput.removeAttribute('title');
|
|
}
|
|
}
|
|
|
|
function getRegionLockRatio(card) {
|
|
return normalizeLockRatio(card.querySelector('[name="region_lock_ratio[]"]').value);
|
|
}
|
|
|
|
function isRegionLocked(card) {
|
|
return Boolean(getRegionLockRatio(card));
|
|
}
|
|
|
|
function syncLockedDimensions(card, changedField) {
|
|
normalizeRegionCard(card, changedField);
|
|
updateRegionLockBadge(card);
|
|
markTemplateFormDirty();
|
|
requestOverlayRender();
|
|
}
|
|
|
|
function getRegionChipLabel(regionType) {
|
|
if (window.pulseRegionTypes && typeof window.pulseRegionTypes.getRegionChipLabel === 'function') {
|
|
return window.pulseRegionTypes.getRegionChipLabel(regionType);
|
|
}
|
|
|
|
return 'Text';
|
|
}
|
|
|
|
function populateRegionCard(card, region) {
|
|
var chip = card.querySelector('[data-region-chip]');
|
|
var title = card.querySelector('[data-region-title]');
|
|
var nameInput = card.querySelector('[name="region_name[]"]');
|
|
var regionTypeInput = card.querySelector('[name="region_type[]"]');
|
|
var regionKeyInput = card.querySelector('[name="region_key[]"]');
|
|
var regionLabelInput = card.querySelector('[name="region_label[]"]');
|
|
|
|
if (title) {
|
|
title.textContent = region.label || region.region_key || 'Region';
|
|
}
|
|
if (chip) {
|
|
chip.textContent = getRegionChipLabel(region.region_type);
|
|
}
|
|
if (nameInput) {
|
|
nameInput.value = region.region_key || region.label || '';
|
|
}
|
|
if (regionTypeInput) {
|
|
regionTypeInput.value = region.region_type || 'text';
|
|
}
|
|
if (regionKeyInput) {
|
|
regionKeyInput.value = region.region_key || region.label || '';
|
|
}
|
|
if (regionLabelInput) {
|
|
regionLabelInput.value = region.label || region.region_key || '';
|
|
}
|
|
var regionLockRatioInput = card.querySelector('[name="region_lock_ratio[]"]');
|
|
if (regionLockRatioInput) {
|
|
regionLockRatioInput.value = normalizeLockRatio(region.lock_ratio || (String(region.region_type || '').trim() === 'qr-code' ? '1:1' : ''));
|
|
}
|
|
writeAnimationConfig(card, region.animation_json);
|
|
card.querySelector('[name="region_x[]"]').value = valueOrDefault(region.x, 80);
|
|
card.querySelector('[name="region_y[]"]').value = valueOrDefault(region.y, 80);
|
|
card.querySelector('[name="region_z[]"]').value = valueOrDefault(region.z_index, 1);
|
|
card.querySelector('[name="region_width[]"]').value = valueOrDefault(region.width, 300);
|
|
card.querySelector('[name="region_height[]"]').value = valueOrDefault(region.height, 120);
|
|
updateRegionLockRatioState(card);
|
|
updateRegionLockBadge(card);
|
|
}
|
|
|
|
function setRegionRemovalState(card) {
|
|
var removeButton = card.querySelector('[data-region-remove-button]');
|
|
if (!removeButton) {
|
|
return;
|
|
}
|
|
var regionKey = String(card.dataset.regionKey || getRegionName(card) || '').trim();
|
|
var regionDeletionLocked = regionUsageSet.has(regionKey);
|
|
removeButton.disabled = regionDeletionLocked;
|
|
if (regionDeletionLocked) {
|
|
removeButton.title = 'Regions cannot be deleted while this template is used by slides.';
|
|
} else {
|
|
removeButton.removeAttribute('title');
|
|
}
|
|
}
|
|
|
|
function updateRegionLabel(card) {
|
|
var label = getRegionName(card) || 'Region';
|
|
var cards = getCards();
|
|
var index = cards.indexOf(card);
|
|
var title = card.querySelector('.template-field-head strong');
|
|
if (title) {
|
|
title.textContent = label;
|
|
}
|
|
if (index >= 0 && regionSelect.options[index]) {
|
|
regionSelect.options[index].textContent = label;
|
|
}
|
|
}
|
|
|
|
function makeRegionCard(region) {
|
|
var card;
|
|
if (regionCardTemplate && regionCardTemplate.content) {
|
|
card = regionCardTemplate.content.firstElementChild.cloneNode(true);
|
|
} else {
|
|
card = document.createElement('div');
|
|
card.className = 'card card-outline card-secondary admin-form-card region-item mb-3';
|
|
}
|
|
populateRegionCard(card, region);
|
|
card.dataset.regionKey = String(region.region_key || region.label || getRegionName(card) || '').trim();
|
|
setRegionRemovalState(card);
|
|
var nameInput = card.querySelector('[name="region_name[]"]');
|
|
var lockRatioInput = card.querySelector('[name="region_lock_ratio[]"]');
|
|
var widthInput = card.querySelector('[name="region_width[]"]');
|
|
var heightInput = card.querySelector('[name="region_height[]"]');
|
|
var animationPresetInputs = card.querySelectorAll('[data-animation-preset]');
|
|
var animationResetButtons = card.querySelectorAll('[data-animation-reset-button]');
|
|
var animationAdvancedButton = card.querySelector('[data-animation-advanced-button]');
|
|
nameInput.addEventListener('input', function () {
|
|
regionNameValidationToastShown = false;
|
|
syncRegionIdentity(card, nameInput.value);
|
|
updateRegionLabel(card);
|
|
validateRegionNames();
|
|
renderRegionSidebar();
|
|
renderOverlay();
|
|
});
|
|
Array.prototype.forEach.call(animationPresetInputs, function (input) {
|
|
input.addEventListener('change', function () {
|
|
setAnimationPreset(card, input.getAttribute('data-animation-preset'), input.value);
|
|
markTemplateFormDirty();
|
|
requestOverlayRender();
|
|
});
|
|
});
|
|
Array.prototype.forEach.call(animationResetButtons, function (button) {
|
|
button.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
setAnimationPreset(card, button.getAttribute('data-animation-reset-button'), 'none');
|
|
markTemplateFormDirty();
|
|
requestOverlayRender();
|
|
});
|
|
});
|
|
if (animationAdvancedButton && animationAdvancedModal) {
|
|
animationAdvancedButton.addEventListener('click', function () {
|
|
activeAnimationCard = card;
|
|
populateAnimationModal(card);
|
|
if (window.pulseModal) {
|
|
window.pulseModal.show(animationAdvancedModal);
|
|
}
|
|
});
|
|
}
|
|
if (lockRatioInput) {
|
|
lockRatioInput.addEventListener('input', function () {
|
|
syncLockedDimensions(card, 'width');
|
|
});
|
|
}
|
|
if (widthInput) {
|
|
widthInput.addEventListener('input', function () {
|
|
syncLockedDimensions(card, 'width');
|
|
});
|
|
}
|
|
if (heightInput) {
|
|
heightInput.addEventListener('input', function () {
|
|
syncLockedDimensions(card, 'height');
|
|
});
|
|
}
|
|
card.addEventListener('click', function (event) {
|
|
if (event.target && event.target.closest && event.target.closest('[data-region-remove-button]')) {
|
|
return;
|
|
}
|
|
setSelected(getCards().indexOf(card));
|
|
});
|
|
card.querySelector('[data-region-remove-button]').addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
card.remove();
|
|
markTemplateFormDirty();
|
|
if (!getCards().length) {
|
|
selectedIndex = -1;
|
|
} else if (selectedIndex >= getCards().length) {
|
|
selectedIndex = getCards().length - 1;
|
|
}
|
|
renderRegionSidebar();
|
|
renderOverlay();
|
|
});
|
|
return card;
|
|
}
|
|
|
|
function renderRegionList(initialRegions) {
|
|
regionList.innerHTML = '';
|
|
initialRegions.forEach(function (region) {
|
|
regionList.appendChild(makeRegionCard(region));
|
|
});
|
|
}
|
|
|
|
function renderRegionSidebar() {
|
|
var cards = getCards();
|
|
regionSelect.innerHTML = '';
|
|
if (!cards.length) {
|
|
regionSelect.disabled = true;
|
|
regionList.innerHTML = '';
|
|
updateCanvasSizeLock();
|
|
return;
|
|
}
|
|
regionSelect.disabled = false;
|
|
if (selectedIndex < 0 || selectedIndex >= cards.length) {
|
|
selectedIndex = 0;
|
|
}
|
|
cards.forEach(function (card, index) {
|
|
var option = document.createElement('option');
|
|
option.value = String(index);
|
|
option.textContent = getRegionName(card) || ('Region ' + (index + 1));
|
|
if (index === selectedIndex) {
|
|
option.selected = true;
|
|
}
|
|
regionSelect.appendChild(option);
|
|
card.hidden = index !== selectedIndex;
|
|
});
|
|
regionSelect.value = String(selectedIndex);
|
|
updateCanvasSizeLock();
|
|
validateRegionNames();
|
|
updatePreviewCardVisibility();
|
|
}
|
|
|
|
function renderOverlay() {
|
|
if (previewState.active) {
|
|
return;
|
|
}
|
|
|
|
var cards = getCards();
|
|
var selectedCard = selectedIndex >= 0 ? cards[selectedIndex] : null;
|
|
var selectedNow = selectedCard ? cards.indexOf(selectedCard) : -1;
|
|
var regions = cards.map(readCard);
|
|
var canvasSize = getCanvasSize();
|
|
var overlayRect = getOverlayRect();
|
|
var overlayScale = overlayRect.width / Math.max(1, canvasSize.width);
|
|
overlay.style.setProperty('--designer-overlay-scale', String(overlayScale));
|
|
regionsJsonInput.value = JSON.stringify(regions);
|
|
overlay.innerHTML = regions.map(function (region, index) {
|
|
var box = canvasRectToPixels(region);
|
|
var renderedBox = {
|
|
left: Math.round(box.left),
|
|
top: Math.round(box.top),
|
|
width: Math.round(box.width),
|
|
height: Math.round(box.height)
|
|
};
|
|
var selected = index === selectedNow ? ' selected' : '';
|
|
return '<div class="designer-rect' + selected + '" data-index="' + index + '" style="--designer-overlay-scale:1;left:' + renderedBox.left + 'px;top:' + renderedBox.top + 'px;width:' + renderedBox.width + 'px;height:' + renderedBox.height + 'px;"><div class="designer-rect-label">' + escapeHtml(region.label || region.region_key || 'Region') + '</div><span class="resize-handle nw" data-dir="nw"></span><span class="resize-handle ne" data-dir="ne"></span><span class="resize-handle sw" data-dir="sw"></span><span class="resize-handle se" data-dir="se"></span></div>';
|
|
}).join('');
|
|
if (draft) {
|
|
var rect = overlayRect;
|
|
var size = canvasSize;
|
|
var draftBox = { x: Math.min(draft.start.x, draft.end.x), y: Math.min(draft.start.y, draft.end.y), width: Math.abs(draft.end.x - draft.start.x), height: Math.abs(draft.end.y - draft.start.y) };
|
|
overlay.innerHTML += '<div class="designer-rect designer-draft" style="left:' + Math.round((draftBox.x / size.width) * rect.width) + 'px;top:' + Math.round((draftBox.y / size.height) * rect.height) + 'px;width:' + Math.round((draftBox.width / size.width) * rect.width) + 'px;height:' + Math.round((draftBox.height / size.height) * rect.height) + 'px;"></div>';
|
|
}
|
|
|
|
updatePreviewControlState();
|
|
}
|
|
|
|
function requestOverlayRender() {
|
|
if (previewState.active) {
|
|
return;
|
|
}
|
|
|
|
if (overlayRenderFrame) {
|
|
return;
|
|
}
|
|
|
|
overlayRenderFrame = window.requestAnimationFrame(function () {
|
|
overlayRenderFrame = 0;
|
|
renderOverlay();
|
|
});
|
|
}
|
|
|
|
function render() {
|
|
updateAspectRatio();
|
|
renderRegionSidebar();
|
|
renderOverlay();
|
|
}
|
|
|
|
function observeStageResize() {
|
|
if (!stage) {
|
|
return;
|
|
}
|
|
|
|
if (window.ResizeObserver) {
|
|
stageResizeObserver = new window.ResizeObserver(function () {
|
|
requestOverlayRender();
|
|
});
|
|
stageResizeObserver.observe(stage);
|
|
return;
|
|
}
|
|
|
|
window.addEventListener('resize', requestOverlayRender);
|
|
}
|
|
|
|
function markTemplateFormDirty() {
|
|
if (!templateForm) {
|
|
return;
|
|
}
|
|
templateForm.dataset.dirty = 'true';
|
|
}
|
|
|
|
function setSelected(index) {
|
|
if (previewState.active) {
|
|
return;
|
|
}
|
|
|
|
var cards = getCards();
|
|
if (!cards.length) {
|
|
selectedIndex = -1;
|
|
} else if (index < 0) {
|
|
selectedIndex = 0;
|
|
} else {
|
|
selectedIndex = clamp(index, 0, cards.length - 1);
|
|
}
|
|
renderRegionSidebar();
|
|
renderOverlay();
|
|
}
|
|
|
|
function addRegion(region) {
|
|
if (previewState.active) {
|
|
return;
|
|
}
|
|
|
|
var hint = regionList.querySelector('.muted');
|
|
if (hint) {
|
|
hint.remove();
|
|
}
|
|
var card = makeRegionCard(region);
|
|
setRegionRemovalState(card);
|
|
regionList.appendChild(card);
|
|
markTemplateFormDirty();
|
|
setSelected(getCards().length - 1);
|
|
}
|
|
|
|
function openAddRegionModal() {
|
|
if (!window.pulseModal) {
|
|
return;
|
|
}
|
|
window.pulseModal.show(regionAddModal);
|
|
}
|
|
|
|
function createDefaultRegion(type) {
|
|
var name = getNextDefaultRegionName(type);
|
|
var lockRatio = String(type || '').trim() === 'qr-code' ? '1:1' : '';
|
|
var size = getDefaultRegionSize(type, lockRatio);
|
|
return {
|
|
region_key: name,
|
|
label: name,
|
|
region_type: type,
|
|
animation_json: normalizeAnimationConfig('{}'),
|
|
x: 80,
|
|
y: 80,
|
|
width: size.width,
|
|
height: size.height,
|
|
lock_ratio: lockRatio,
|
|
z_index: 1
|
|
};
|
|
}
|
|
|
|
function clampRegion(region) {
|
|
var size = getCanvasSize();
|
|
var minSize = 12;
|
|
var x = clamp(region.x, 0, size.width - minSize);
|
|
var y = clamp(region.y, 0, size.height - minSize);
|
|
var width = Math.max(minSize, region.width);
|
|
var height = Math.max(minSize, region.height);
|
|
if (x + width > size.width) {
|
|
width = size.width - x;
|
|
}
|
|
if (y + height > size.height) {
|
|
height = size.height - y;
|
|
}
|
|
return { x: Math.round(x), y: Math.round(y), width: Math.round(Math.max(minSize, width)), height: Math.round(Math.max(minSize, height)) };
|
|
}
|
|
|
|
function startDraw(event) {
|
|
if (previewState.active) {
|
|
return;
|
|
}
|
|
|
|
var start = toCanvasPoint(event);
|
|
draft = { start: start, end: start };
|
|
renderOverlay();
|
|
function moveHandler(moveEvent) {
|
|
draft.end = toCanvasPoint(moveEvent);
|
|
requestOverlayRender();
|
|
}
|
|
function upHandler(upEvent) {
|
|
draft.end = toCanvasPoint(upEvent);
|
|
var width = Math.abs(draft.end.x - draft.start.x);
|
|
var height = Math.abs(draft.end.y - draft.start.y);
|
|
if (width >= 8 && height >= 8) {
|
|
var region = fitRegionToCanvas({ x: Math.min(draft.start.x, draft.end.x), y: Math.min(draft.start.y, draft.end.y), width: width, height: height });
|
|
var name = getNextDefaultRegionName('text');
|
|
addRegion({ region_key: name, label: name, region_type: 'text', x: region.x, y: region.y, width: region.width, height: region.height, z_index: 1 });
|
|
}
|
|
draft = null;
|
|
requestOverlayRender();
|
|
document.removeEventListener('mousemove', moveHandler);
|
|
document.removeEventListener('mouseup', upHandler);
|
|
}
|
|
document.addEventListener('mousemove', moveHandler);
|
|
document.addEventListener('mouseup', upHandler);
|
|
}
|
|
|
|
function startMove(index, event) {
|
|
if (previewState.active) {
|
|
return;
|
|
}
|
|
|
|
var startPoint = toCanvasPoint(event);
|
|
var startRegion = readCard(cardAt(index));
|
|
function moveHandler(moveEvent) {
|
|
var currentPoint = toCanvasPoint(moveEvent);
|
|
var dx = currentPoint.x - startPoint.x;
|
|
var dy = currentPoint.y - startPoint.y;
|
|
var size = getCanvasSize();
|
|
var maxX = Math.max(0, size.width - startRegion.width);
|
|
var maxY = Math.max(0, size.height - startRegion.height);
|
|
var nextX = clamp(startRegion.x + dx, 0, maxX);
|
|
var nextY = clamp(startRegion.y + dy, 0, maxY);
|
|
writeCard(cardAt(index), { x: nextX, y: nextY, width: startRegion.width, height: startRegion.height });
|
|
markTemplateFormDirty();
|
|
requestOverlayRender();
|
|
}
|
|
function upHandler() {
|
|
document.removeEventListener('mousemove', moveHandler);
|
|
document.removeEventListener('mouseup', upHandler);
|
|
}
|
|
document.addEventListener('mousemove', moveHandler);
|
|
document.addEventListener('mouseup', upHandler);
|
|
}
|
|
|
|
function resizeFromHandle(index, dir, event) {
|
|
if (previewState.active) {
|
|
return;
|
|
}
|
|
|
|
var startPoint = toCanvasPoint(event);
|
|
var startRegion = readCard(cardAt(index));
|
|
var lockRatio = normalizeLockRatio(startRegion.lock_ratio);
|
|
var aspect = lockRatio ? parseLockRatio(lockRatio) : null;
|
|
var canvasSize = getCanvasSize();
|
|
var minSize = 12;
|
|
|
|
function clampResizeDelta(dx, dy) {
|
|
if (dir.indexOf('w') !== -1) {
|
|
dx = clamp(dx, -startRegion.x, startRegion.width - minSize);
|
|
}
|
|
if (dir.indexOf('e') !== -1) {
|
|
dx = clamp(dx, minSize - startRegion.width, canvasSize.width - startRegion.x - startRegion.width);
|
|
}
|
|
if (dir.indexOf('n') !== -1) {
|
|
dy = clamp(dy, -startRegion.y, startRegion.height - minSize);
|
|
}
|
|
if (dir.indexOf('s') !== -1) {
|
|
dy = clamp(dy, minSize - startRegion.height, canvasSize.height - startRegion.y - startRegion.height);
|
|
}
|
|
return { dx: dx, dy: dy };
|
|
}
|
|
|
|
function fitFromWidth(width) {
|
|
var nextWidth = Math.max(12, Math.round(width));
|
|
return {
|
|
width: nextWidth,
|
|
height: aspect ? Math.max(12, Math.round(nextWidth / aspect.ratio)) : startRegion.height
|
|
};
|
|
}
|
|
|
|
function fitFromHeight(height) {
|
|
var nextHeight = Math.max(12, Math.round(height));
|
|
return {
|
|
width: aspect ? Math.max(12, Math.round(nextHeight * aspect.ratio)) : startRegion.width,
|
|
height: nextHeight
|
|
};
|
|
}
|
|
|
|
function moveHandler(moveEvent) {
|
|
var currentPoint = toCanvasPoint(moveEvent);
|
|
var dx = currentPoint.x - startPoint.x;
|
|
var dy = currentPoint.y - startPoint.y;
|
|
var constrainedDelta = clampResizeDelta(dx, dy);
|
|
dx = constrainedDelta.dx;
|
|
dy = constrainedDelta.dy;
|
|
var next = { x: startRegion.x, y: startRegion.y, width: startRegion.width, height: startRegion.height };
|
|
if (aspect) {
|
|
if (dir === 'e') {
|
|
var eastSize = fitFromWidth(startRegion.width + dx);
|
|
next.width = eastSize.width;
|
|
next.height = eastSize.height;
|
|
next.y = startRegion.y + Math.round((startRegion.height - next.height) / 2);
|
|
} else if (dir === 'w') {
|
|
var westSize = fitFromWidth(startRegion.width - dx);
|
|
next.width = westSize.width;
|
|
next.height = westSize.height;
|
|
next.x = startRegion.x + startRegion.width - next.width;
|
|
next.y = startRegion.y + Math.round((startRegion.height - next.height) / 2);
|
|
} else if (dir === 'n') {
|
|
var northSize = fitFromHeight(startRegion.height - dy);
|
|
next.width = northSize.width;
|
|
next.height = northSize.height;
|
|
next.x = startRegion.x + Math.round((startRegion.width - next.width) / 2);
|
|
next.y = startRegion.y + startRegion.height - next.height;
|
|
} else if (dir === 's') {
|
|
var southSize = fitFromHeight(startRegion.height + dy);
|
|
next.width = southSize.width;
|
|
next.height = southSize.height;
|
|
next.x = startRegion.x + Math.round((startRegion.width - next.width) / 2);
|
|
} else {
|
|
var useWidth = Math.abs(dx) >= Math.abs(dy * aspect.ratio);
|
|
var cornerSize = useWidth ? fitFromWidth(dir.indexOf('w') !== -1 ? startRegion.width - dx : startRegion.width + dx) : fitFromHeight(dir.indexOf('n') !== -1 ? startRegion.height - dy : startRegion.height + dy);
|
|
next.width = cornerSize.width;
|
|
next.height = cornerSize.height;
|
|
if (dir.indexOf('w') !== -1) { next.x = startRegion.x + startRegion.width - next.width; }
|
|
if (dir.indexOf('n') !== -1) { next.y = startRegion.y + startRegion.height - next.height; }
|
|
if (dir.indexOf('e') !== -1) { next.x = startRegion.x; }
|
|
if (dir.indexOf('s') !== -1) { next.y = startRegion.y; }
|
|
}
|
|
} else {
|
|
if (dir.indexOf('w') !== -1) { next.x = startRegion.x + dx; next.width = startRegion.width - dx; }
|
|
if (dir.indexOf('e') !== -1) { next.width = startRegion.width + dx; }
|
|
if (dir.indexOf('n') !== -1) { next.y = startRegion.y + dy; next.height = startRegion.height - dy; }
|
|
if (dir.indexOf('s') !== -1) { next.height = startRegion.height + dy; }
|
|
}
|
|
if (next.width < 12) { if (dir.indexOf('w') !== -1) { next.x -= 12 - next.width; } next.width = 12; }
|
|
if (next.height < 12) { if (dir.indexOf('n') !== -1) { next.y -= 12 - next.height; } next.height = 12; }
|
|
next = fitRegionToCanvas(next, aspect);
|
|
writeCard(cardAt(index), { x: next.x, y: next.y, width: next.width, height: next.height });
|
|
markTemplateFormDirty();
|
|
requestOverlayRender();
|
|
}
|
|
function upHandler() {
|
|
document.removeEventListener('mousemove', moveHandler);
|
|
document.removeEventListener('mouseup', upHandler);
|
|
}
|
|
document.addEventListener('mousemove', moveHandler);
|
|
document.addEventListener('mouseup', upHandler);
|
|
}
|
|
|
|
if (addRegionButton && regionAddModal) {
|
|
addRegionButton.addEventListener('click', function () {
|
|
renderAddRegionOptions();
|
|
openAddRegionModal();
|
|
});
|
|
if (regionAddOptions) {
|
|
regionAddOptions.addEventListener('click', function (event) {
|
|
var button = event.target && event.target.closest ? event.target.closest('[data-add-region-type]') : null;
|
|
if (!button) {
|
|
return;
|
|
}
|
|
|
|
var regionType = button.getAttribute('data-add-region-type');
|
|
addRegion(createDefaultRegion(regionType));
|
|
if (window.pulseModal) {
|
|
window.pulseModal.hide(regionAddModal);
|
|
}
|
|
});
|
|
}
|
|
|
|
renderAddRegionOptions();
|
|
}
|
|
backgroundInput.addEventListener('change', function () {
|
|
var file = backgroundInput.files && backgroundInput.files[0];
|
|
if (file) {
|
|
updateBackgroundPreview(file);
|
|
}
|
|
});
|
|
if (removeBackgroundButton && removeBackgroundFlag) {
|
|
removeBackgroundButton.addEventListener('click', function () {
|
|
removeBackgroundFlag.checked = true;
|
|
backgroundInput.value = '';
|
|
backgroundPreview.removeAttribute('src');
|
|
backgroundPreview.style.display = 'none';
|
|
backgroundEmpty.style.display = 'block';
|
|
});
|
|
}
|
|
if (animationAdvancedModal) {
|
|
animationAdvancedModal.addEventListener('hidden.bs.modal', function () {
|
|
activeAnimationCard = null;
|
|
});
|
|
animationAdvancedModal.addEventListener('input', function (event) {
|
|
clampAnimationModalField(event.target);
|
|
markTemplateFormDirty();
|
|
});
|
|
animationAdvancedModal.addEventListener('change', function () {
|
|
markTemplateFormDirty();
|
|
});
|
|
var animationModalApplyButton = animationAdvancedModal.querySelector('[data-animation-modal-apply]');
|
|
var animationModalResetButton = animationAdvancedModal.querySelector('[data-animation-modal-reset]');
|
|
if (animationModalApplyButton) {
|
|
animationModalApplyButton.addEventListener('click', function () {
|
|
saveAnimationModal();
|
|
markTemplateFormDirty();
|
|
if (window.pulseModal) {
|
|
window.pulseModal.hide(animationAdvancedModal);
|
|
}
|
|
});
|
|
}
|
|
if (animationModalResetButton) {
|
|
animationModalResetButton.addEventListener('click', function () {
|
|
if (!activeAnimationCard) {
|
|
return;
|
|
}
|
|
writeAnimationConfig(activeAnimationCard, normalizeAnimationConfig('{}'));
|
|
markTemplateFormDirty();
|
|
populateAnimationModal(activeAnimationCard);
|
|
requestOverlayRender();
|
|
});
|
|
}
|
|
}
|
|
if (backgroundColorInput) {
|
|
backgroundColorInput.addEventListener('input', updateStageBackgroundColor);
|
|
}
|
|
canvasSizeSelect.addEventListener('change', function () { syncCanvasSizeSelection(); render(); });
|
|
canvasWidthInput.addEventListener('input', render);
|
|
canvasHeightInput.addEventListener('input', render);
|
|
regionSelect.addEventListener('change', function () { setSelected(Number(regionSelect.value || 0)); });
|
|
overlay.addEventListener('mousedown', function (event) {
|
|
var rect = event.target.closest('.designer-rect');
|
|
if (rect) {
|
|
var index = Number(rect.getAttribute('data-index'));
|
|
var handle = event.target.closest('.resize-handle');
|
|
event.preventDefault();
|
|
setSelected(index);
|
|
if (handle) {
|
|
resizeFromHandle(index, handle.getAttribute('data-dir'), event);
|
|
} else {
|
|
startMove(index, event);
|
|
}
|
|
return;
|
|
}
|
|
if (event.target !== overlay && !event.target.classList.contains('designer-overlay')) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
setSelected(-1);
|
|
});
|
|
if (templateForm) {
|
|
templateForm.addEventListener('invalid', function (event) {
|
|
var target = event.target;
|
|
if (!target || !target.validationMessage || !target.validationMessage.match(/Region names must be unique on this template\.|Region name is required\./)) {
|
|
return;
|
|
}
|
|
|
|
if (target.closest && !target.closest('.region-item')) {
|
|
return;
|
|
}
|
|
|
|
notifyRegionNameValidationError(target.validationMessage);
|
|
}, true);
|
|
|
|
templateForm.addEventListener('formdata', function (event) {
|
|
if (previewState.active) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (!validateRegionNames()) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
getCards().forEach(function (card) {
|
|
normalizeRegionCard(card, 'width');
|
|
});
|
|
syncCanvasSizeSelection();
|
|
event.formData.set('regions_json', JSON.stringify(getCards().map(readCard)));
|
|
});
|
|
|
|
templateForm.addEventListener('submit', function (event) {
|
|
if (previewState.active) {
|
|
return;
|
|
}
|
|
|
|
if (!validateRegionNames()) {
|
|
return;
|
|
}
|
|
getCards().forEach(function (card) {
|
|
normalizeRegionCard(card, 'width');
|
|
});
|
|
syncCanvasSizeSelection();
|
|
regionsJsonInput.value = JSON.stringify(getCards().map(readCard));
|
|
});
|
|
}
|
|
|
|
if (previewIntroButton) {
|
|
previewIntroButton.addEventListener('click', function () {
|
|
if (previewState.active && previewState.mode === 'intro') {
|
|
stopTemplatePreview(false);
|
|
return;
|
|
}
|
|
startTemplatePreview('intro');
|
|
});
|
|
}
|
|
|
|
if (previewOutButton) {
|
|
previewOutButton.addEventListener('click', function () {
|
|
if (previewState.active && previewState.mode === 'out') {
|
|
stopTemplatePreview(false);
|
|
return;
|
|
}
|
|
startTemplatePreview('out');
|
|
});
|
|
}
|
|
|
|
if (previewContinuousButton) {
|
|
previewContinuousButton.addEventListener('click', function () {
|
|
if (previewState.active && previewState.mode === 'continuous') {
|
|
stopTemplatePreview(false);
|
|
return;
|
|
}
|
|
startTemplatePreview('continuous');
|
|
});
|
|
}
|
|
|
|
renderRegionList(existingRegions);
|
|
syncCanvasSizeSelection();
|
|
updateStageBackgroundColor();
|
|
observeStageResize();
|
|
render();
|
|
})();
|