415 lines
10 KiB
JavaScript
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;
|
|
}
|
|
});
|
|
}()); |