468 lines
20 KiB
JavaScript
468 lines
20 KiB
JavaScript
(function () {
|
|
var dataElement = document.getElementById('template-editor-data');
|
|
if (!dataElement) {
|
|
return;
|
|
}
|
|
|
|
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 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 backgroundPreview = document.getElementById('background-preview');
|
|
var backgroundEmpty = document.getElementById('background-empty');
|
|
var addTextRegionButton = document.getElementById('add-text-region');
|
|
var addImageRegionButton = document.getElementById('add-image-region');
|
|
var regionsJsonInput = document.getElementById('regions-json');
|
|
var draft = null;
|
|
var selectedIndex = -1;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function valueOrDefault(value, fallback) {
|
|
return 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 Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
function getCards() {
|
|
return Array.prototype.slice.call(regionList.querySelectorAll('.region-item'));
|
|
}
|
|
|
|
function cardAt(index) {
|
|
return getCards()[index] || null;
|
|
}
|
|
|
|
function getRegionName(card) {
|
|
return String(card.querySelector('[name="region_name[]"]').value || '').trim();
|
|
}
|
|
|
|
function syncRegionIdentity(card, value) {
|
|
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 readCard(card) {
|
|
var name = getRegionName(card);
|
|
return {
|
|
region_key: name,
|
|
label: name,
|
|
region_type: card.querySelector('[name="region_type[]"]').value,
|
|
font_family: card.querySelector('[name="font_family[]"]').value,
|
|
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 writeCard(card, values) {
|
|
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.font_family !== undefined) { card.querySelector('[name="font_family[]"]').value = values.font_family; updateRegionFieldVisibility(card); }
|
|
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 overlay.getBoundingClientRect();
|
|
}
|
|
|
|
function toCanvasPoint(event) {
|
|
var rect = getOverlayRect();
|
|
var size = getCanvasSize();
|
|
var x = clamp(event.clientX - rect.left, 0, rect.width);
|
|
var y = clamp(event.clientY - rect.top, 0, rect.height);
|
|
return {
|
|
x: Math.round((x / Math.max(rect.width, 1)) * size.width),
|
|
y: Math.round((y / Math.max(rect.height, 1)) * size.height)
|
|
};
|
|
}
|
|
|
|
function canvasRectToPixels(region) {
|
|
var rect = getOverlayRect();
|
|
var size = getCanvasSize();
|
|
return {
|
|
left: (region.x / size.width) * rect.width,
|
|
top: (region.y / size.height) * rect.height,
|
|
width: (region.width / size.width) * rect.width,
|
|
height: (region.height / size.height) * rect.height
|
|
};
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
function updateBackgroundPreview(file) {
|
|
if (!file) {
|
|
return;
|
|
}
|
|
var reader = new FileReader();
|
|
reader.onload = function () {
|
|
backgroundPreview.src = reader.result;
|
|
backgroundPreview.style.display = 'block';
|
|
backgroundEmpty.style.display = 'none';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
function updateRegionFieldVisibility(card) {
|
|
var typeSelect = card.querySelector('[name="region_type[]"]');
|
|
var fontField = card.querySelector('[name="font_family[]"]');
|
|
if (!typeSelect || !fontField) {
|
|
return;
|
|
}
|
|
fontField.value = typeSelect.value === 'text' || typeSelect.value === 'html' ? 'Arial' : '';
|
|
}
|
|
|
|
function makeRegionCard(region) {
|
|
var card = document.createElement('div');
|
|
card.className = 'region-item';
|
|
var chipLabel = region.region_type === 'image' ? 'Image' : region.region_type === 'webpage' ? 'Webpage' : region.region_type === 'html' ? 'HTML' : 'Text';
|
|
card.innerHTML = '' +
|
|
'<label>Region<input name="region_name[]" value="' + escapeHtml(region.region_key || region.label || '') + '" placeholder="region_1" required /></label>' +
|
|
'<label>Type<select name="region_type[]"><option value="text"' + (region.region_type === 'image' || region.region_type === 'webpage' || region.region_type === 'html' ? '' : ' selected') + '>Text</option><option value="html"' + (region.region_type === 'html' ? ' selected' : '') + '>HTML</option><option value="image"' + (region.region_type === 'image' ? ' selected' : '') + '>Image</option><option value="webpage"' + (region.region_type === 'webpage' ? ' selected' : '') + '>Webpage</option></select></label>' +
|
|
'<input type="hidden" name="font_family[]" value="' + escapeHtml(region.region_type === 'image' ? '' : (region.font_family || 'Arial')) + '" />' +
|
|
'<input type="hidden" name="region_key[]" value="' + escapeHtml(region.region_key || region.label || '') + '" />' +
|
|
'<input type="hidden" name="region_label[]" value="' + escapeHtml(region.label || region.region_key || '') + '" />' +
|
|
'<div class="template-field-head"><strong>' + escapeHtml(region.label || region.region_key || '') + '</strong><span class="chip">' + escapeHtml(chipLabel) + '</span></div>' +
|
|
'<div class="row"><label style="flex:1">X<input type="number" name="region_x[]" value="' + escapeHtml(valueOrDefault(region.x, 80)) + '" required /></label><label style="flex:1">Y<input type="number" name="region_y[]" value="' + escapeHtml(valueOrDefault(region.y, 80)) + '" required /></label></div>' +
|
|
'<div class="row"><label style="flex:1">Width<input type="number" name="region_width[]" value="' + escapeHtml(valueOrDefault(region.width, 300)) + '" required /></label><label style="flex:1">Height<input type="number" name="region_height[]" value="' + escapeHtml(valueOrDefault(region.height, 120)) + '" required /></label><label style="flex:1">Z-Index<input type="number" name="region_z[]" value="' + escapeHtml(valueOrDefault(region.z_index, 1)) + '" required /></label></div>' +
|
|
'<div class="row"><button type="button" class="danger remove-region">Remove</button></div>';
|
|
var nameInput = card.querySelector('[name="region_name[]"]');
|
|
var typeSelect = card.querySelector('[name="region_type[]"]');
|
|
nameInput.addEventListener('input', function () {
|
|
syncRegionIdentity(card, nameInput.value);
|
|
renderRegionSidebar();
|
|
renderOverlay();
|
|
});
|
|
typeSelect.addEventListener('change', function () {
|
|
updateRegionFieldVisibility(card);
|
|
renderOverlay();
|
|
});
|
|
card.addEventListener('click', function (event) {
|
|
if (event.target && event.target.classList && event.target.classList.contains('remove-region')) {
|
|
return;
|
|
}
|
|
setSelected(getCards().indexOf(card));
|
|
});
|
|
card.querySelector('.remove-region').addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
card.remove();
|
|
if (!getCards().length) {
|
|
selectedIndex = -1;
|
|
} else if (selectedIndex >= getCards().length) {
|
|
selectedIndex = getCards().length - 1;
|
|
}
|
|
renderRegionSidebar();
|
|
renderOverlay();
|
|
});
|
|
updateRegionFieldVisibility(card);
|
|
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();
|
|
}
|
|
|
|
function renderOverlay() {
|
|
var cards = getCards();
|
|
var selectedCard = selectedIndex >= 0 ? cards[selectedIndex] : null;
|
|
var selectedNow = selectedCard ? cards.indexOf(selectedCard) : -1;
|
|
var regions = cards.map(readCard);
|
|
regionsJsonInput.value = JSON.stringify(regions);
|
|
overlay.innerHTML = regions.map(function (region, index) {
|
|
var box = canvasRectToPixels(region);
|
|
var selected = index === selectedNow ? ' selected' : '';
|
|
return '<div class="designer-rect' + selected + '" data-index="' + index + '" style="left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.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 = getOverlayRect();
|
|
var size = getCanvasSize();
|
|
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:' + ((draftBox.x / size.width) * rect.width) + 'px;top:' + ((draftBox.y / size.height) * rect.height) + 'px;width:' + ((draftBox.width / size.width) * rect.width) + 'px;height:' + ((draftBox.height / size.height) * rect.height) + 'px;"></div>';
|
|
}
|
|
}
|
|
|
|
function render() {
|
|
updateAspectRatio();
|
|
renderRegionSidebar();
|
|
renderOverlay();
|
|
}
|
|
|
|
function setSelected(index) {
|
|
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) {
|
|
var hint = regionList.querySelector('.muted');
|
|
if (hint) {
|
|
hint.remove();
|
|
}
|
|
regionList.appendChild(makeRegionCard(region));
|
|
setSelected(getCards().length - 1);
|
|
}
|
|
|
|
function createDefaultRegion(type) {
|
|
var count = getCards().length + 1;
|
|
var name = 'region_' + count;
|
|
return {
|
|
region_key: name,
|
|
label: name,
|
|
region_type: type,
|
|
font_family: type === 'text' || type === 'html' ? 'Arial' : '',
|
|
x: 80,
|
|
y: 80,
|
|
width: type === 'image' || type === 'webpage' || type === 'html' ? 420 : 300,
|
|
height: type === 'image' || type === 'webpage' || type === 'html' ? 240 : 120,
|
|
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) {
|
|
var start = toCanvasPoint(event);
|
|
draft = { start: start, end: start };
|
|
renderOverlay();
|
|
function moveHandler(moveEvent) {
|
|
draft.end = toCanvasPoint(moveEvent);
|
|
renderOverlay();
|
|
}
|
|
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 = clampRegion({ x: Math.min(draft.start.x, draft.end.x), y: Math.min(draft.start.y, draft.end.y), width: width, height: height });
|
|
addRegion({ region_key: 'region_' + (getCards().length + 1), label: 'Region ' + (getCards().length + 1), region_type: 'text', x: region.x, y: region.y, width: region.width, height: region.height, z_index: 1 });
|
|
}
|
|
draft = null;
|
|
renderOverlay();
|
|
document.removeEventListener('mousemove', moveHandler);
|
|
document.removeEventListener('mouseup', upHandler);
|
|
}
|
|
document.addEventListener('mousemove', moveHandler);
|
|
document.addEventListener('mouseup', upHandler);
|
|
}
|
|
|
|
function startMove(index, event) {
|
|
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 next = clampRegion({ x: startRegion.x + dx, y: startRegion.y + dy, width: startRegion.width, height: startRegion.height });
|
|
writeCard(cardAt(index), { x: next.x, y: next.y });
|
|
renderOverlay();
|
|
}
|
|
function upHandler() {
|
|
document.removeEventListener('mousemove', moveHandler);
|
|
document.removeEventListener('mouseup', upHandler);
|
|
}
|
|
document.addEventListener('mousemove', moveHandler);
|
|
document.addEventListener('mouseup', upHandler);
|
|
}
|
|
|
|
function resizeFromHandle(index, dir, event) {
|
|
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 next = { x: startRegion.x, y: startRegion.y, width: startRegion.width, height: startRegion.height };
|
|
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 = clampRegion(next);
|
|
writeCard(cardAt(index), { x: next.x, y: next.y, width: next.width, height: next.height });
|
|
renderOverlay();
|
|
}
|
|
function upHandler() {
|
|
document.removeEventListener('mousemove', moveHandler);
|
|
document.removeEventListener('mouseup', upHandler);
|
|
}
|
|
document.addEventListener('mousemove', moveHandler);
|
|
document.addEventListener('mouseup', upHandler);
|
|
}
|
|
|
|
addTextRegionButton.addEventListener('click', function () { addRegion(createDefaultRegion('text')); });
|
|
var addHtmlRegionButton = document.getElementById('add-html-region');
|
|
if (addHtmlRegionButton) {
|
|
addHtmlRegionButton.addEventListener('click', function () { addRegion(createDefaultRegion('html')); });
|
|
}
|
|
addImageRegionButton.addEventListener('click', function () { addRegion(createDefaultRegion('image')); });
|
|
var addWebpageRegionButton = document.getElementById('add-webpage-region');
|
|
if (addWebpageRegionButton) {
|
|
addWebpageRegionButton.addEventListener('click', function () { addRegion(createDefaultRegion('webpage')); });
|
|
}
|
|
backgroundInput.addEventListener('change', function () {
|
|
var file = backgroundInput.files && backgroundInput.files[0];
|
|
if (file) {
|
|
updateBackgroundPreview(file);
|
|
}
|
|
});
|
|
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);
|
|
startDraw(event);
|
|
});
|
|
document.getElementById('template-form').addEventListener('submit', function () {
|
|
syncCanvasSizeSelection();
|
|
regionsJsonInput.value = JSON.stringify(getCards().map(readCard));
|
|
});
|
|
|
|
renderRegionList(existingRegions);
|
|
syncCanvasSizeSelection();
|
|
render();
|
|
})();
|