Release 1.5.4
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
(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;
|
||||
}
|
||||
});
|
||||
}());
|
||||
Reference in New Issue
Block a user