Files
pulse-signage/src/web/public/js/slides/slide-image-cropper.js
T
2026-07-25 17:22:21 +01:00

415 lines
10 KiB
JavaScript

(function () {
var templateFields = document.getElementById('template-fields');
var modal = document.getElementById('slide-image-cropper-modal');
var image = document.getElementById('slide-image-cropper-image');
var status = document.getElementById('slide-image-cropper-status');
if (!templateFields || !modal || !image || !status) {
return;
}
var cropper = null;
var currentInput = null;
var currentFile = null;
var currentObjectUrl = '';
var flipX = 1;
var flipY = 1;
var currentAspectRatio = NaN;
var currentRegionAspectRatio = NaN;
var currentRegionAspectRatioLabel = 'Region';
var listenersAttached = false;
function getRegionId(input) {
var match = String(input && input.name || '').match(/^region_image_(\d+)$/);
return match ? match[1] : '';
}
function getExistingInput(regionId) {
if (!regionId) {
return null;
}
return document.querySelector('input[type="hidden"][name="existing_region_image_' + regionId + '"]');
}
function setStatus(message) {
status.textContent = String(message || '');
}
function showModal() {
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
window.pulseModal.show(modal);
return;
}
if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modal).show();
}
}
function hideModal() {
if (window.pulseModal && typeof window.pulseModal.hide === 'function') {
window.pulseModal.hide(modal);
return;
}
if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modal).hide();
}
}
function destroyCropper() {
if (cropper) {
cropper.destroy();
cropper = null;
}
}
function revokeObjectUrl() {
if (currentObjectUrl) {
URL.revokeObjectURL(currentObjectUrl);
currentObjectUrl = '';
}
}
function resetModalState() {
destroyCropper();
revokeObjectUrl();
currentInput = null;
currentFile = null;
flipX = 1;
flipY = 1;
image.removeAttribute('src');
image.alt = 'Selected image to crop';
setStatus('');
}
function setButtonState(disabled) {
modal.querySelectorAll('[data-slide-image-cropper-action]').forEach(function (button) {
button.disabled = Boolean(disabled) && button.getAttribute('data-slide-image-cropper-action') !== 'reset';
});
}
function ratioLabelForValue(value) {
if (value === 'free') {
return 'free';
}
if (value === 'region') {
return 'region';
}
return String(value || '').trim();
}
function parseAspectRatio(value) {
var parts = String(value || '').trim().split(/[:/]/);
var width = Number(parts[0]);
var height = Number(parts[1]);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return NaN;
}
return width / height;
}
function setActiveRatioButton(value) {
var targetValue = ratioLabelForValue(value);
modal.querySelectorAll('[data-slide-image-cropper-action="ratio"]').forEach(function (button) {
var buttonValue = ratioLabelForValue(button.getAttribute('data-slide-image-cropper-ratio'));
var isActive = buttonValue === targetValue;
button.classList.toggle('active', isActive);
button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
});
}
function applyAspectRatio(value) {
if (!cropper) {
return;
}
currentAspectRatio = value;
if (value === 'free') {
cropper.setAspectRatio(NaN);
setActiveRatioButton('free');
return;
}
if (value === 'region') {
if (!Number.isFinite(currentRegionAspectRatio) || currentRegionAspectRatio <= 0) {
cropper.setAspectRatio(NaN);
setActiveRatioButton('free');
return;
}
cropper.setAspectRatio(currentRegionAspectRatio);
setActiveRatioButton('region');
return;
}
var parts = String(value || '').split(':');
var width = Number(parts[0]);
var height = Number(parts[1]);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return;
}
cropper.setAspectRatio(width / height);
setActiveRatioButton(value);
}
function initCropper() {
if (!currentInput || !currentFile || !window.Cropper) {
return;
}
destroyCropper();
cropper = new window.Cropper(image, {
aspectRatio: NaN,
autoCropArea: 1,
background: false,
dragMode: 'move',
initialAspectRatio: NaN,
movable: true,
responsive: true,
rotatable: true,
scalable: true,
viewMode: 1,
zoomOnTouch: true,
zoomOnWheel: true,
ready: function () {
if (cropper && cropper.container) {
cropper.container.style.width = '100%';
cropper.container.style.height = '560px';
cropper.container.style.maxHeight = '70vh';
}
var containerData = cropper.getContainerData();
var imageData = cropper.getImageData();
var fitRatio = Math.min(
Number(containerData.width || 0) / Number(imageData.naturalWidth || 1),
Number(containerData.height || 0) / Number(imageData.naturalHeight || 1),
1
);
if (Number.isFinite(fitRatio) && fitRatio > 0) {
cropper.zoomTo(fitRatio);
} else {
cropper.reset();
}
applyAspectRatio(currentAspectRatio === undefined ? 'free' : currentAspectRatio);
}
});
setButtonState(false);
setActiveRatioButton(currentAspectRatio === undefined ? 'free' : currentAspectRatio);
setStatus('Use the toolbar to crop, rotate, or flip the image before applying it.');
}
function openEditor(input, file) {
currentInput = input;
currentFile = file;
flipX = 1;
flipY = 1;
currentAspectRatio = 'free';
currentRegionAspectRatio = parseAspectRatio(input && input.dataset && input.dataset.slideImageCropperRegionRatio);
currentRegionAspectRatioLabel = String(input && input.dataset && input.dataset.slideImageCropperRegionRatioLabel || 'Region').trim() || 'Region';
setButtonState(true);
setStatus('Loading image editor...');
modal.querySelectorAll('[data-slide-image-cropper-action="ratio"][data-slide-image-cropper-ratio="region"]').forEach(function (button) {
button.title = currentRegionAspectRatioLabel ? 'Region ratio ' + currentRegionAspectRatioLabel : 'Region ratio';
});
revokeObjectUrl();
currentObjectUrl = URL.createObjectURL(file);
image.alt = file.name || 'Selected image to crop';
image.src = currentObjectUrl;
if (!listenersAttached) {
listenersAttached = true;
modal.addEventListener('shown.bs.modal', function () {
if (currentInput && currentFile) {
initCropper();
}
});
modal.addEventListener('hidden.bs.modal', function () {
resetModalState();
});
}
showModal();
}
function setInputFile(input, file) {
var dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
input.files = dataTransfer.files;
}
function finalizeCropFile(file) {
if (!currentInput) {
return;
}
var input = currentInput;
var existing = getExistingInput(getRegionId(input));
input.dataset.cropperBypass = '1';
setInputFile(input, file);
if (existing) {
existing.value = '';
}
input.dispatchEvent(new Event('change', { bubbles: true }));
window.setTimeout(function () {
delete input.dataset.cropperBypass;
}, 0);
hideModal();
}
function commitCrop() {
if (!currentInput || !currentFile) {
return;
}
if (!cropper) {
finalizeCropFile(currentFile);
return;
}
setButtonState(true);
setStatus('Creating the cropped image...');
var canvas = cropper.getCroppedCanvas({
fillColor: 'transparent',
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high'
});
if (!canvas) {
setStatus('Unable to create the cropped image.');
setButtonState(false);
return;
}
var outputType = /^image\/(jpeg|jpg|webp|png)$/i.test(currentFile.type || '') ? currentFile.type : 'image/png';
canvas.toBlob(function (blob) {
if (!blob) {
setStatus('Unable to create the cropped image.');
setButtonState(false);
return;
}
var finalFile = new File([blob], currentFile.name || 'slide-region-image.png', {
lastModified: Date.now(),
type: blob.type || outputType
});
finalizeCropFile(finalFile);
}, outputType, outputType.indexOf('jpeg') !== -1 ? 0.92 : undefined);
}
function rotateImage(direction) {
if (cropper) {
cropper.rotate(direction);
}
}
function flipImage(axis) {
if (!cropper) {
return;
}
if (axis === 'x') {
flipX *= -1;
cropper.scaleX(flipX);
return;
}
flipY *= -1;
cropper.scaleY(flipY);
}
function resetImage() {
if (!cropper) {
return;
}
cropper.reset();
flipX = 1;
flipY = 1;
}
templateFields.addEventListener('change', function (event) {
var input = event.target;
if (!input || !input.matches || !input.matches('input[type="file"][name^="region_image_"]')) {
return;
}
if (input.dataset.cropperBypass === '1') {
return;
}
var file = input.files && input.files[0];
if (!file) {
return;
}
if (!/^image\//i.test(file.type || '')) {
input.value = '';
return;
}
event.stopImmediatePropagation();
event.stopPropagation();
input.value = '';
if (!window.Cropper) {
currentInput = input;
currentFile = file;
commitCrop();
return;
}
openEditor(input, file);
}, true);
modal.addEventListener('click', function (event) {
var button = event.target && event.target.closest ? event.target.closest('[data-slide-image-cropper-action]') : null;
if (!button) {
return;
}
var action = button.getAttribute('data-slide-image-cropper-action');
if (action === 'apply') {
commitCrop();
return;
}
if (action === 'ratio') {
applyAspectRatio(button.getAttribute('data-slide-image-cropper-ratio') || 'free');
return;
}
if (action === 'rotate-left') {
rotateImage(-90);
return;
}
if (action === 'rotate-right') {
rotateImage(90);
return;
}
if (action === 'flip-horizontal') {
flipImage('x');
return;
}
if (action === 'flip-vertical') {
flipImage('y');
return;
}
if (action === 'reset') {
resetImage();
return;
}
});
}());