����
������������������������������������
let globalImageValidationPassed = true;
function validateImageTableRows() {
const urlParams = new URLSearchParams(window.location.search);
const shouldValidate = urlParams.get('validation') === 'true';
if (!shouldValidate) return Promise.resolve(true); // Skip validation, assume it's passed
globalImageValidationPassed = true;
const validations = [];
$('#imageTableBody tr').each(function () {
const row = $(this);
const imageSize = row.find('.image-size').val();
const fileInput = row.find('input[type="file"]')[0];
row.css('border', '');
row.find('td.label-td span.image-error').remove(); // Remove previous errors if any
if (!fileInput || !fileInput.files || fileInput.files.length === 0) return;
const file = fileInput.files[0];
if (!file.type.startsWith('image/')) return;
const promise = new Promise((resolve) => {
const reader = new FileReader();
reader.onload = function (e) {
const img = new Image();
img.onload = function () {
const [wRaw, hRaw, type] = imageSize ? imageSize.split(',').map(val => val.trim()) : [];
const requiredWidth = parseInt(wRaw, 10);
const requiredHeight = parseInt(hRaw, 10);
const t = (type || '').toLowerCase();
let isValid = true;
let errorMessage = '';
if (!isNaN(requiredWidth) && !isNaN(requiredHeight)) {
switch (t) {
case 'width':
isValid = img.width === requiredWidth;
if (!isValid) errorMessage = `Image width must be ${requiredWidth}px. Yours is ${img.width}px.`;
break;
case 'height':
isValid = img.height === requiredHeight;
if (!isValid) errorMessage = `Image height must be ${requiredHeight}px. Yours is ${img.height}px.`;
break;
default:
isValid = img.width === requiredWidth && img.height === requiredHeight;
if (!isValid) errorMessage = `Image size must be ${requiredWidth}x${requiredHeight}px. Yours is ${img.width}x${img.height}px.`;
}
}
if (!isValid) {
row.css('border', '2px solid red');
const firstTd = row.find('td.label-td');
firstTd.append(`
${errorMessage}`);
globalImageValidationPassed = false;
toastr.error(errorMessage);
}
resolve();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
validations.push(promise);
});
return Promise.all(validations).then(() => globalImageValidationPassed);
}
function saveUpdatedImages() {
validateImageTableRows().then(valid => {
if (!valid) return;
});
const saveButton = $('#saveImagesButton');
saveButton.prop('disabled', true);
saveButton.text('Submitting...');
let formData = new FormData();
let pid = $('#pid').val();
let rid = $('#rid').val();
$('#imageTableBody tr').each(function (index) {
const fileInput = $(this).find('.image-file')[0].files[0];
const caption = $(this).find('input[type="text"]').val();
const pins = $(this).data('pins') || [];
const id = (pid == rid) ? $(this).data('image-id') : undefined;
const imageOrder = $(this).index();
const imageSize = $(this).find('.image-size').val();
const imageCleared = $(this).attr('data-image-cleared') === 'true';
let fileName;
formData.append('captions[]', caption || '');
formData.append('imageOrder[]', imageOrder);
formData.append('imageSizes[]', imageSize);
formData.append('image_cleared[]', imageCleared ? 'true' : 'false');
if (typeof pins === 'string') {
try {
JSON.parse(pins);
formData.append('pins[]', pins);
} catch {
formData.append('pins[]', JSON.stringify(pins));
}
} else {
formData.append('pins[]', JSON.stringify(pins));
}
formData.append('ids[]', id);
if (fileInput) {
fileName = (typeof id === 'undefined' || id === '') ? 'image_' + index : 'image_' + id;
formData.append('image_files[' + fileName + ']', fileInput);
if (pid != rid) {
formData.append('images[]', fileInput);
}
} else if (!imageCleared) {
const originalPath = $(this).find('.original-image-path').val();
formData.append('image_paths[]', originalPath || '');
} else {
formData.append('image_paths[]', '');
}
formData.append('ret_id', rid);
});
formData.append('pid', pid);
$.ajax({
url: 'update_images.php',
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (response) {
saveButton.text('Images Saved');
toastr.success('Images saved successfully!');
window.parent.postMessage({ action: 'submitForm' }, '*');
},
error: function (xhr, status, error) {
saveButton.prop('disabled', false);
saveButton.text('Save All Images');
toastr.error('Error saving images: ' + error);
console.log(xhr.responseText);
}
});
}
function saveImages() {
validateImageTableRows().then(valid => {
if (!valid) return;
});
$(document).find("button.btn-secondary").removeClass("currentActive");
const saveButton = $('#saveImagesButton');
saveButton.prop('disabled', true);
saveButton.text('Submitting...');
let formData = new FormData();
let pid = $('#pid').val();
const whiteImageBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=';
const byteString = atob(whiteImageBase64.split(',')[1]);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const defaultFile = new File([ab], 'White-Background-PNG.png', { type: 'image/png' });
$('#imageTableBody tr').each(function () {
const fileInput = $(this).find('.image-file')[0].files[0];
const caption = $(this).find('input[type="text"]').val();
const imageSize = $(this).find('.image-size').val();
const pins = $(this).data('pins') || [];
const id = $(this).data('image-id');
const imageOrder = $(this).index();
formData.append('images[]', fileInput || defaultFile);
formData.append('captions[]', caption);
formData.append('pins[]', JSON.stringify(pins));
formData.append('ids[]', id);
formData.append('pid', pid);
formData.append('imageOrder[]', imageOrder);
formData.append('imageSizes[]', imageSize);
});
$.ajax({
url: 'save_images.php',
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (response) {
saveButton.text('Images Saved');
toastr.success('Images saved successfully!');
window.parent.postMessage({ action: 'submitForm' }, '*');
},
error: function (xhr, status, error) {
saveButton.prop('disabled', false);
saveButton.text('Save All Images');
toastr.error('Error saving images: ' + error);
}
});
isLineDrawing = false;
isDrawing = false;
cropFrom = false;
disableAllDrawingModes();
canvas.isDrawingMode = false;
}
function previewImage(event, input) {
const container = $(input).closest('td');
const imgPreview = container.find('.image-preview');
const clearButton = container.find('.clear-image-btn');
const dragDropArea = container.find('.drag-drop-area');
if (input.files && input.files[0]) {
const fileType = input.files[0].type;
const supportedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
if (!supportedTypes.includes(fileType)) {
toastr.error("We're sorry, you have uploaded an image format that is not supported. Please upload an image in the following formats: png, jpeg, jpg. Thank you.");
input.value = '';
imgPreview.attr('src', '#').hide();
clearButton.hide();
dragDropArea.removeClass('highlight').show();
return;
}
const reader = new FileReader();
reader.onload = function (e) {
imgPreview.attr('src', e.target.result).show();
clearButton.show();
dragDropArea.removeClass('highlight');
// Trigger validation after image preview is updated
validateImageTableRows();
};
reader.readAsDataURL(input.files[0]);
}
}
function saveImageChanges() {
// Reset zoom before saving
if (currentZoom !== 1) {
currentZoom = 1;
applyZoom();
}
cropFrom = false;
// Store all objects including pins
const objects = canvas.getObjects();
const pins = objects.filter(obj => obj.type === 'pin').map(pin => ({
left: (pin.left / canvas.width) * 100,
top: (pin.top / canvas.height) * 100
}));
const dataURL = canvas.toDataURL();
currentRow.find('.image-preview').attr('src', dataURL);
const fileInput = currentRow.find('input[type="file"]')[0];
const blob = dataURLtoBlob(dataURL);
const newFile = new File([blob], 'edited-image.png', { type: 'image/png' });
const dataTransfer = new DataTransfer();
dataTransfer.items.add(newFile);
fileInput.files = dataTransfer.files;
// Save pins data to the row
currentRow.data('pins', pins);
$('#editImageModal').modal('hide');
validateImageTableRows().then((isValid) => {
if (isValid) {
toastr.success('Image and pins saved successfully');
return; // stop save if validation fails
}
});
}
/////////////// Tool Tip //////////////
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
let currentRow;
let undoStack = [];
let hasUndone = false;
let cropFrom = false;
let initialCanvasState = null;
let isSelectMode = false;
let isShapeMode = true; // Default mode when modal opens
let currentZoom = 1;
const ZOOM_STEP = 0.1;
const MAX_ZOOM = 3;
const MIN_ZOOM = 0.5;
function triggerFileInput(dropArea) {
const fileInput = $(dropArea).siblings('input[type="file"]')[0];
fileInput.click();
}
function addRow() {
const row = `
|
|
Drag & Drop Image Here
Press Ctrl + V to paste images into the highlighted area.
|
|
`;
$('#imageTableBody').append(row);
}
function removeRow(button) {
const rows = $('#imageTableBody tr');
if (rows.length > 1) {
$(button).closest('tr').remove();
} else {
toastr.warning("The last row cannot be removed!");
}
}
function moveRow(button, direction) {
const row = $(button).closest('tr');
if (direction === 'up' && row.index() > 0) {
row.insertBefore(row.prev());
} else if (direction === 'down') {
row.insertAfter(row.next());
}
}
function clearImage(button) {
const container = $(button).closest('td');
const fileInput = container.find('input[type="file"]');
const imgPreview = container.find('.image-preview');
const clearButton = container.find('.clear-image-btn');
const dragDropArea = container.find('.drag-drop-area');
// Clear the file input value
fileInput.val('');
// Create an empty DataTransfer object to properly clear the files
const emptyDataTransfer = new DataTransfer();
fileInput[0].files = emptyDataTransfer.files;
// Hide the image preview and clear button
imgPreview.attr('src', '#').hide();
clearButton.hide();
// Make sure drag-drop area is visible and not highlighted
dragDropArea.removeClass('highlight').show();
// Show success message
toastr.success('Image cleared successfully');
}
function highlightDropArea(dropArea) {
$(dropArea).addClass('highlight');
}
function removeHighlight(dropArea) {
$(dropArea).removeClass('highlight');
}
$(document).ready(function () {
////////////// If any button is active and any mode is enable then disable it //////////
$(document).find("button.btn-secondary").each(function(index){
// console.log("%cREADY ABC", "color:green;font-size:25px;");
// console.log("This is button Index "+index);
if($(this).hasClass("currentActive")){
$(this).removeClass("currentActive");
}
});
isLineDrawing = false;
isDrawing = false;
cropFrom = false;
disableAllDrawingModes();
canvas.isDrawingMode = false;
cropFrom = false;
$(document).on('dragover', '.drag-drop-area', function (event) {
event.preventDefault();
highlightDropArea(this);
});
$(document).on('dragleave', '.drag-drop-area', function () {
removeHighlight(this);
});
$(document).on('drop', '.drag-drop-area', function (event) {
handleDrop(event, this);
});
$(document).on('paste', '.drag-drop-area', function (event) {
handlePaste(event, this);
});
});
function highlightDropArea(dropArea) {
$(dropArea).addClass('highlight');
}
function removeHighlight(dropArea) {
$(dropArea).removeClass('highlight');
}
function handleDrop(event, dropArea) {
event.preventDefault();
removeHighlight(dropArea);
const files = event.originalEvent.dataTransfer.files;
if (files.length > 0) {
const input = $(dropArea).siblings('input[type="file"]')[0];
const dataTransfer = new DataTransfer();
dataTransfer.items.add(files[0]); // Simulate file input
input.files = dataTransfer.files;
previewImage(event, input);
}
}
function handlePaste(event, dropArea) {
const items = event.originalEvent.clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith('image/')) {
event.preventDefault();
highlightDropArea(dropArea); // Highlight the area
const file = item.getAsFile();
const input = $(dropArea).siblings('input[type="file"]')[0];
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
input.files = dataTransfer.files; // Simulate file input
previewImage(event, input);
}
}
}
let canvas = new fabric.Canvas('canvas');
let fabricImage = null;
let originalImageSrc = null;
function editImage(icon) {
////////////// If any button is active and any mode is enable then disable it //////////
$(document).find("button.btn-secondary").each(function(index){
if($(this).hasClass("currentActive")){
$(this).removeClass("currentActive");
}
});
isLineDrawing = false;
isDrawing = false;
cropFrom = false;
disableAllDrawingModes();
canvas.isDrawingMode = false;
cropFrom = false;
currentRow = $(icon).closest('tr');
const imageSrc = currentRow.find('.image-preview').attr('src');
// Get the description cell and extract preferred size
const descriptionCell = currentRow.find('td').filter(function() {
return $(this).text().includes('Preferred Image Size:');
});
// Extract preferred size
let preferredSize = '';
if (descriptionCell.length > 0) {
const sizeMatch = descriptionCell.text().match(/Preferred Image Size: \((.*?)\)/);
if (sizeMatch && sizeMatch[1]) {
preferredSize = sizeMatch[1];
}
}
if (imageSrc) {
originalImageSrc = imageSrc;
$('#editImageModal').modal({
backdrop: 'static',
keyboard: false
});
// Update modal title and preferred size display
if (preferredSize) {
$('#preferredSizeDisplay').html(`
Preferred Size: ${preferredSize}`);
} else {
$('#preferredSizeDisplay').html('');
}
loadFabricImage(imageSrc);
}
}
function loadFabricImage(src) {
fabric.Image.fromURL(src, function(img) {
// Reset zoom level
currentZoom = 1;
const originalWidth = img.width;
const originalHeight = img.height;
const maxWidth = 1000;
if (originalWidth > maxWidth) {
const aspectRatio = originalHeight / originalWidth;
canvas.setWidth(maxWidth);
canvas.setHeight(maxWidth * aspectRatio);
} else {
canvas.setWidth(originalWidth);
canvas.setHeight(originalHeight);
}
canvas.clear();
fabricImage = img;
// Set enhanced quality properties
img.set({
left: 0,
top: 0,
scaleX: canvas.width / originalWidth,
scaleY: canvas.height / originalHeight,
selectable: false,
evented: false,
objectCaching: false
});
// Set canvas quality settings
canvas.set({
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high'
});
canvas.add(fabricImage);
fabricImage.sendToBack();
canvas.renderAll();
// Force a high-quality render
setTimeout(() => {
canvas.requestRenderAll();
}, 100);
initialCanvasState = {
state: JSON.stringify(canvas.toJSON()),
width: canvas.getWidth(),
height: canvas.getHeight(),
zoom: 1
};
// Update dimension display
updateDimensionDisplay();
}, {
crossOrigin: 'anonymous',
quality: 1.0,
enableRetinaScaling: true
});
}
function updateModalImage(event) {
const input = event.target;
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
loadFabricImage(e.target.result);
};
reader.readAsDataURL(input.files[0]);
}
}
let cropMode = false; // Tracks whether crop mode is active
let cropRect;
function cropImage() {
disableShapeMode();
if (!cropMode) {
// First click - initialize crop mode and create crop rectangle
disableAllDrawingModes();
disableAllButtonsExcept('cropButton');
pushUndoState();
// Reset zoom level before cropping if needed
if (currentZoom !== 1) {
currentZoom = 1;
applyZoom();
}
// Get image bounds correctly considering its positioning mode
let imgBounds;
if (fabricImage.originX === 'center' && fabricImage.originY === 'center') {
// For center-origin images (after resize)
const width = fabricImage.getScaledWidth();
const height = fabricImage.getScaledHeight();
imgBounds = {
left: fabricImage.left - width/2,
top: fabricImage.top - height/2,
width: width,
height: height
};
} else {
// For top-left origin images (default)
imgBounds = {
left: fabricImage.left || 0,
top: fabricImage.top || 0,
width: fabricImage.getScaledWidth(),
height: fabricImage.getScaledHeight()
};
}
// Create crop rectangle proportional to image size
const initialSize = Math.min(imgBounds.width, imgBounds.height) * 0.5;
cropRect = new fabric.Rect({
left: imgBounds.left + imgBounds.width/4,
top: imgBounds.top + imgBounds.height/4,
width: initialSize,
height: initialSize,
fill: 'transparent',
stroke: 'red',
strokeWidth: 2,
selectable: true,
});
canvas.add(cropRect);
canvas.setActiveObject(cropRect);
canvas.renderAll();
cropMode = true;
$('#cropButton').html(' Apply Crop');
if (!$('#cancelCropButton').length) {
$('#cropButton').after(`
`);
}
toastr.info('Select area to crop. Click Apply Crop when ready, or Cancel to abort.');
} else {
// Second click - perform the crop
if (fabricImage && cropRect) {
const rect = cropRect.getBoundingRect();
// Store all objects except the background image and crop rectangle
const objectsToKeep = canvas.getObjects().filter(obj =>
obj !== fabricImage && obj !== cropRect
);
// Get the original image source
const originalSrc = fabricImage.getSrc();
// Create a new image to get the original dimensions
const img = new Image();
img.onload = function() {
const originalWidth = img.width;
const originalHeight = img.height;
// Calculate crop coordinates based on image position mode
let cropX, cropY;
if (fabricImage.originX === 'center' && fabricImage.originY === 'center') {
// For center-positioned images (after resize)
const imgWidth = fabricImage.getScaledWidth();
const imgHeight = fabricImage.getScaledHeight();
const imgLeft = fabricImage.left - imgWidth/2;
const imgTop = fabricImage.top - imgHeight/2;
// Calculate relative position within the image
cropX = (rect.left - imgLeft) / imgWidth * originalWidth;
cropY = (rect.top - imgTop) / imgHeight * originalHeight;
} else {
// For top-left positioned images (default)
cropX = (rect.left - fabricImage.left) / fabricImage.scaleX;
cropY = (rect.top - fabricImage.top) / fabricImage.scaleY;
}
// Calculate crop dimensions
const cropWidth = (rect.width / fabricImage.scaleX) * (originalWidth / fabricImage.width);
const cropHeight = (rect.height / fabricImage.scaleY) * (originalHeight / fabricImage.height);
// Create a temporary canvas for cropping
const tempCanvas = document.createElement('canvas');
tempCanvas.width = originalWidth;
tempCanvas.height = originalHeight;
const tempCtx = tempCanvas.getContext('2d');
// Draw the original image at full size
tempCtx.drawImage(img, 0, 0, originalWidth, originalHeight);
// Create the final cropped canvas
const croppedCanvas = document.createElement('canvas');
croppedCanvas.width = cropWidth;
croppedCanvas.height = cropHeight;
const ctx = croppedCanvas.getContext('2d');
// Draw the cropped portion
ctx.drawImage(
tempCanvas,
cropX, cropY, cropWidth, cropHeight,
0, 0, cropWidth, cropHeight
);
const croppedImageDataURL = croppedCanvas.toDataURL();
fabric.Image.fromURL(croppedImageDataURL, function (croppedImage) {
canvas.clear();
let finalWidth = croppedImage.width;
let finalHeight = croppedImage.height;
const maxWidth = 1000;
if (finalWidth > maxWidth) {
const scaleFactor = maxWidth / finalWidth;
finalWidth = maxWidth;
finalHeight *= scaleFactor;
}
canvas.setWidth(finalWidth);
canvas.setHeight(finalHeight);
// Add the cropped background image - use standard positioning
croppedImage.set({
left: 0,
top: 0,
originX: 'left',
originY: 'top',
scaleX: canvas.width / croppedImage.width,
scaleY: canvas.height / croppedImage.height,
selectable: false,
evented: false
});
canvas.add(croppedImage);
fabricImage = croppedImage;
// Restore all other objects with adjusted positions
objectsToKeep.forEach(obj => {
// Calculate new positions relative to crop area
const newLeft = ((obj.left - rect.left) / rect.width) * canvas.width;
const newTop = ((obj.top - rect.top) / rect.height) * canvas.height;
// Scale the object proportionally
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
obj.set({
left: newLeft,
top: newTop,
scaleX: obj.scaleX * scaleX,
scaleY: obj.scaleY * scaleY
});
canvas.add(obj);
});
canvas.renderAll();
pushUndoState();
// Update dimension display after cropping
updateDimensionDisplay();
// After successful crop, restore the button
$('#cropButton').html(' Crop');
$('#cancelCropButton').remove();
cropMode = false;
cropRect = null;
enableAllButtons();
toastr.success('Image cropped successfully');
});
};
img.src = originalSrc;
}
}
}
// Add new function to handle crop cancellation
function cancelCrop() {
if (cropRect) {
canvas.remove(cropRect);
canvas.renderAll();
}
// Reset the crop button
$('#cropButton').html(' Crop');
$('#cancelCropButton').remove();
cropMode = false;
cropRect = null;
enableAllButtons();
toastr.info('Crop operation cancelled');
}
// Update the disableAllButtonsExcept function to handle the cancel button
function disableAllButtonsExcept(buttonId) {
document.querySelectorAll('.btn-secondary').forEach(button => {
if (button.id !== buttonId && button.id !== 'cancelCropButton') {
button.disabled = true;
}
});
document.getElementById(buttonId).disabled = false;
}
function enableAllButtons() {
document.querySelectorAll('.btn-secondary').forEach(button => {
button.disabled = false;
});
}
function flipImage() {
disableAllDrawingModes()
cropFrom = false;
if (fabricImage) {
// console.log("Before flip: ", fabricImage);
// console.log("Before flip: ", fabricImage.flipX);
pushUndoState();
fabricImage.flipX = !fabricImage.flipX;
// console.log("After flip: ", fabricImage);
// console.log("After flip: ", fabricImage.flipX);
fabricImage.dirty = true;
canvas.renderAll();
}
cropFrom = true;
}
function rotateImage() {
disableAllDrawingModes();
if (fabricImage) {
pushUndoState();
fabricImage.rotate((fabricImage.angle + 90) % 360);
canvas.renderAll();
}
cropFrom = true;
}
// Helper function to convert base64 to Blob
function dataURLtoBlob(dataURL) {
const byteString = atob(dataURL.split(',')[1]);
const mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mimeString });
}
// Add this after the canvas initialization
let selectedColor = '#000000'; // Default color
// Update the color selection handler
$(document).ready(function() {
// Add click handler for color boxes
$('.color-box').click(function() {
// Remove selected class from all boxes
$('.color-box').removeClass('selected');
// Add selected class to clicked box
$(this).addClass('selected');
// Update the selected color
selectedColor = $(this).data('color');
// Update active object if exists
if (canvas.getActiveObject()) {
const activeObject = canvas.getActiveObject();
if (activeObject.type === 'text') {
activeObject.set('fill', selectedColor);
} else {
activeObject.set('stroke', selectedColor);
}
canvas.renderAll();
}
// Update free drawing brush if active
if (canvas.isDrawingMode) {
canvas.freeDrawingBrush.color = selectedColor;
}
});
});
// Update the toggleFreeDrawing function
function toggleFreeDrawing() {
disableAllDrawingModes();
canvas.isDrawingMode = !canvas.isDrawingMode;
if (canvas.isDrawingMode) {
$('#freeDrawButton').addClass('currentActive');
canvas.freeDrawingBrush.color = selectedColor;
canvas.freeDrawingBrush.width = 5;
// Make existing paths selectable
canvas.getObjects('path').forEach(path => {
path.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
});
toastr.success('Free Drawing Mode Enabled');
} else {
$('#freeDrawButton').removeClass('currentActive');
toastr.success('Free Drawing Mode Disabled');
}
canvas.renderAll();
}
// Update canvas event listener for newly added free drawing paths
canvas.on('path:created', function(e) {
const path = e.path;
// Ensure path properties are properly set for Safari
path.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
objectCaching: false, // Disable object caching for Safari
strokeUniform: true // Ensure stroke rendering is uniform
});
// Force path to render properly in Safari
path.dirty = true;
canvas.requestRenderAll();
// Add to undo stack after path is created
setTimeout(function() {
pushUndoState();
}, 100);
});
// Add Safari-specific handling
$(document).ready(function() {
// Detect Safari
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari) {
// Initialize canvas with Safari-specific settings
canvas.set({
enableRetinaScaling: true,
renderOnAddRemove: true,
skipTargetFind: false,
imageSmoothingEnabled: true
});
// Remove the Safari drawing controls since they're no longer needed
// We've improved the drawing functionality to work without manual saving
// Replace the toggleFreeDrawing function for Safari
window.originalToggleFreeDrawing = window.toggleFreeDrawing;
window.toggleFreeDrawing = function() {
disableAllDrawingModes();
// Toggle drawing mode
canvas.isDrawingMode = !canvas.isDrawingMode;
if (canvas.isDrawingMode) {
$('#freeDrawButton').addClass('currentActive');
canvas.freeDrawingBrush.color = selectedColor;
canvas.freeDrawingBrush.width = 5;
// Make existing paths selectable
canvas.getObjects('path').forEach(path => {
path.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
});
// Use a flag to properly track drawing state
let isReallyDrawing = false;
let currentPath = null;
// Remove any existing handlers
$(canvas.upperCanvasEl).off('.safariDrawing');
$(document).off('.safariDrawing');
// Completely override fabric.js's drawing behavior for Safari
// First, disable fabric's built-in drawing mode but keep our state
const wasDrawingMode = canvas.isDrawingMode;
canvas.isDrawingMode = false;
// Disable canvas selection to prevent selection box
canvas.selection = false;
canvas.defaultCursor = 'crosshair';
// Disable text selection on the page while drawing
$('body').css({
'-webkit-user-select': 'none',
'-moz-user-select': 'none',
'-ms-user-select': 'none',
'user-select': 'none'
});
// Override the path created event to ensure paths are permanent
canvas.off('path:created');
canvas.on('path:created', function(e) {
const path = e.path;
currentPath = path;
// Ensure path is visible and permanent with NO FILL
path.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
objectCaching: false,
strokeUniform: true,
erasable: false,
fill: 'transparent' // Ensure there is no fill
});
// Force the path to be permanent
path.dirty = true;
// Make sure the path stays in the canvas
if (!canvas.contains(path)) {
canvas.add(path);
}
canvas.requestRenderAll();
// Add to undo stack immediately
setTimeout(function() {
if (currentPath && canvas.contains(currentPath)) {
pushUndoState();
}
}, 200);
});
// Create our own drawing implementation
let currentLine = null;
let startPoint = null;
let pathPoints = []; // Track points to prevent Safari's line drop issue
// Mouse down - only now start drawing
$(canvas.upperCanvasEl).on('mousedown.safariDrawing', function(e) {
if (!wasDrawingMode) return;
// Prevent default browser selection behavior
e.preventDefault();
e.stopPropagation();
isReallyDrawing = true;
const pointer = canvas.getPointer(e);
startPoint = pointer;
pathPoints = []; // Reset path points
pathPoints.push({ x: pointer.x, y: pointer.y });
// Create a new line with the selected color
currentLine = new fabric.Path(`M ${pointer.x} ${pointer.y}`, {
stroke: selectedColor,
strokeWidth: 5,
fill: 'transparent',
selectable: false,
evented: false,
objectCaching: false, // Disable caching to improve rendering in Safari
strokeLineCap: 'round', // Round the line ends to help hide gaps
strokeLineJoin: 'round' // Round the line corners
});
canvas.add(currentLine);
canvas.renderAll();
});
// Mouse move - only draw if mouse button is pressed
$(canvas.upperCanvasEl).on('mousemove.safariDrawing', function(e) {
if (!isReallyDrawing || !wasDrawingMode) return;
// Prevent default to avoid selection box
e.preventDefault();
e.stopPropagation();
const pointer = canvas.getPointer(e);
// Add point to tracking array
pathPoints.push({ x: pointer.x, y: pointer.y });
// Make sure currentLine exists (create it if it doesn't)
if (!currentLine) {
// Create a new line if one doesn't exist yet
currentLine = new fabric.Path(`M ${pointer.x} ${pointer.y}`, {
stroke: selectedColor,
strokeWidth: 5,
fill: 'transparent',
selectable: false,
evented: false,
objectCaching: false,
strokeLineCap: 'round', // Round the line ends to help hide gaps
strokeLineJoin: 'round' // Round the line corners
});
canvas.add(currentLine);
} else {
// Append to the current path
currentLine.path.push(['L', pointer.x, pointer.y]);
currentLine.dirty = true;
}
// Safari line fix: Instead of trying to maintain one long path,
// create a new path segment when the current one gets too long
if (pathPoints.length > 25) { // Slightly higher threshold for fewer segments
// Finalize current line (make it permanent)
currentLine.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
// Ensure the line stays visible
dirty: true,
strokeUniform: true,
objectCaching: false
});
// Force the canvas to recognize the current line is complete
canvas.renderAll();
// Get the last few points for overlap to ensure continuity
const overlapPoints = pathPoints.slice(-3); // Use last 3 points for overlap
// Start a new line segment from slightly before the last point for overlap
// This creates a small overlap between segments to avoid gaps
const startPoint = overlapPoints[0];
// Create a new line starting from the overlap point
currentLine = new fabric.Path(`M ${startPoint.x} ${startPoint.y}`, {
stroke: selectedColor,
strokeWidth: 5,
fill: 'transparent',
selectable: false,
evented: false,
objectCaching: false,
strokeLineCap: 'round', // Round the line ends
strokeLineJoin: 'round' // Round the line corners
});
// Add the overlap points to ensure continuity between segments
for (let i = 1; i < overlapPoints.length; i++) {
currentLine.path.push(['L', overlapPoints[i].x, overlapPoints[i].y]);
}
canvas.add(currentLine);
// Reset path points but keep the overlap points for continuity
pathPoints = overlapPoints;
}
canvas.renderAll();
});
// Mouse up - finish drawing
$(document).on('mouseup.safariDrawing', function(e) {
if (!isReallyDrawing || !wasDrawingMode) return;
// Prevent default to avoid selection box
e.preventDefault();
e.stopPropagation();
isReallyDrawing = false;
if (currentLine) {
// Make the line selectable now
currentLine.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
// Ensure the line stays visible
dirty: true,
strokeUniform: true,
objectCaching: false
});
canvas.renderAll();
// Only push to undo stack once at the end of the drawing operation
pushUndoState();
currentLine = null;
startPoint = null;
pathPoints = [];
}
});
// Mouse leave - stop drawing if cursor leaves canvas
$(canvas.upperCanvasEl).on('mouseleave.safariDrawing', function(e) {
if (!isReallyDrawing || !wasDrawingMode) return;
isReallyDrawing = false;
if (currentLine) {
// Make the final line segment selectable
currentLine.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
dirty: true
});
canvas.renderAll();
pushUndoState();
currentLine = null;
startPoint = null;
pathPoints = [];
}
});
toastr.success('Free Drawing Mode Enabled');
} else {
// Clean up all event handlers when disabling
$(canvas.upperCanvasEl).off('.safariDrawing');
$(document).off('.safariDrawing');
$('#freeDrawButton').removeClass('currentActive');
// Re-enable all buttons
$('.btn-secondary').prop('disabled', false);
// Save the current drawing
saveSafariDrawing();
// Restore text selection and selection mode
$('body').css({
'-webkit-user-select': 'text',
'-moz-user-select': 'text',
'-ms-user-select': 'text',
'user-select': 'text'
});
canvas.selection = true;
canvas.defaultCursor = 'default';
toastr.success('Free Drawing Mode Disabled');
}
canvas.renderAll();
};
// Also update disableAllDrawingModes to remove Safari events and hide controls
const originalDisableAllDrawingModes = window.disableAllDrawingModes;
window.disableAllDrawingModes = function() {
originalDisableAllDrawingModes();
// Remove Safari-specific event handlers
$(canvas.upperCanvasEl).off('.safariDrawing');
$(document).off('.safariDrawing');
// Make sure we save any drawings
if (canvas.isDrawingMode) {
saveSafariDrawing();
canvas.isDrawingMode = false;
}
};
// Make sure drawing controls are hidden when modal is closed
$('#editImageModal').on('hidden.bs.modal', function() {
// Ensure we save any drawings
if (canvas.isDrawingMode) {
saveSafariDrawing();
canvas.isDrawingMode = false;
}
});
}
});
// Update addRectangle function
function addRectangle() {
disableAllDrawingModes();
isShapeMode = true; // Enable shape mode
$('#rectangleButton').addClass('currentActive');
const canvasCenter = {
x: canvas.width / 2,
y: canvas.height / 2
};
const offsetX = Math.floor(Math.random() * 60) - 30;
const offsetY = Math.floor(Math.random() * 60) - 30;
const rect = new fabric.Rect({
left: canvasCenter.x - 40 + offsetX,
top: canvasCenter.y - 30 + offsetY,
fill: 'transparent',
width: 80,
height: 60,
stroke: selectedColor,
strokeWidth: 2,
selectable: true, // Always selectable when added
evented: true,
hasControls: true,
hasBorders: true
});
canvas.add(rect);
canvas.setActiveObject(rect);
canvas.renderAll();
}
// Update addCircle function
function addCircle() {
disableAllDrawingModes();
isShapeMode = true; // Enable shape mode
$('#circleButton').addClass('currentActive');
const canvasCenter = {
x: canvas.width / 2,
y: canvas.height / 2
};
const offsetX = Math.floor(Math.random() * 60) - 30;
const offsetY = Math.floor(Math.random() * 60) - 30;
const circle = new fabric.Circle({
left: canvasCenter.x - 30 + offsetX,
top: canvasCenter.y - 30 + offsetY,
radius: 30,
fill: 'transparent',
stroke: selectedColor,
strokeWidth: 2,
selectable: true, // Always selectable when added
evented: true,
hasControls: true,
hasBorders: true
});
canvas.add(circle);
canvas.setActiveObject(circle);
canvas.renderAll();
}
// Update addText function
function addText() {
disableAllDrawingModes();
$('#textButton').addClass('currentActive');
const text = prompt('Enter the text you want to add:');
if (text) {
const offsetX = Math.floor(Math.random() * 60) - 30;
const offsetY = Math.floor(Math.random() * 60) - 30;
const textObj = new fabric.Text(text, {
left: canvas.width / 2 + offsetX,
top: canvas.height / 2 + offsetY,
fill: selectedColor,
fontSize: 24,
fontFamily: 'Arial',
originX: 'center',
originY: 'center',
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
canvas.add(textObj);
canvas.setActiveObject(textObj);
canvas.renderAll();
}
}
let isLineDrawing = false;
let isDrawing = false;
let line = null;
function toggleLineDrawing() {
disableAllDrawingModes();
isLineDrawing = !isLineDrawing;
if (isLineDrawing) {
$('#lineButton').addClass('currentActive');
canvas.defaultCursor = 'crosshair';
canvas.selection = false; // Disable canvas selection when in line drawing mode
// Add drawing event listeners
canvas.on('mouse:down', startDrawingLine);
canvas.on('mouse:move', continueDrawingLine);
canvas.on('mouse:up', finishDrawingLine);
// Add object:moving event listener to disable line drawing mode
canvas.on('object:moving', function() {
if (isLineDrawing) {
disableLineDrawing();
$('#lineButton').removeClass('currentActive');
canvas.selection = true;
toastr.info('Line Drawing Mode disabled due to object movement');
}
});
toastr.success('Line Drawing Mode Enabled - Click and drag to draw lines. Click the Line button again to exit.');
} else {
disableLineDrawing();
canvas.selection = true; // Re-enable canvas selection when exiting line drawing mode
toastr.success('Line Drawing Mode Disabled');
}
canvas.renderAll();
}
function startDrawingLine(o) {
if (!isLineDrawing || isDrawing) return;
isDrawing = true;
const pointer = canvas.getPointer(o.e);
line = new fabric.Line([pointer.x, pointer.y, pointer.x, pointer.y], {
strokeWidth: 2,
stroke: selectedColor,
selectable: false,
evented: false,
hasControls: false,
hasBorders: false,
hoverCursor: 'pointer',
perPixelTargetFind: false,
strokeLineCap: 'round',
strokeLineJoin: 'round',
targetFindTolerance: 10,
padding: 5
});
canvas.add(line);
canvas.selection = false;
}
function continueDrawingLine(o) {
if (!isDrawing || !line) return;
const pointer = canvas.getPointer(o.e);
line.set({
x2: pointer.x,
y2: pointer.y
});
canvas.renderAll();
}
function finishDrawingLine() {
if (!isDrawing || !line) return;
isDrawing = false;
line.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
perPixelTargetFind: false,
targetFindTolerance: 10,
padding: 5,
hoverCursor: 'pointer'
});
// Add hover effect
line.on('mouseover', function() {
this.set({
strokeWidth: 4
});
canvas.renderAll();
});
line.on('mouseout', function() {
this.set({
strokeWidth: 2
});
canvas.renderAll();
});
canvas.selection = true;
line.setCoords();
canvas.discardActiveObject();
canvas.renderAll();
pushUndoState();
line = null;
}
function disableLineDrawing() {
$('#lineButton').removeClass('currentActive');
// Remove drawing event listeners
canvas.off('mouse:down', startDrawingLine);
canvas.off('mouse:move', continueDrawingLine);
canvas.off('mouse:up', finishDrawingLine);
canvas.off('object:moving'); // Remove the moving event listener
// Ensure all objects remain selectable
canvas.getObjects().forEach(obj => {
if (obj !== fabricImage) {
obj.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
}
});
canvas.defaultCursor = 'default';
isLineDrawing = false;
isDrawing = false;
line = null;
}
// Update disableAllDrawingModes to keep objects selectable
function disableAllDrawingModes() {
canvas.isDrawingMode = false;
if (isLineDrawing) {
disableLineDrawing();
}
isLineDrawing = false;
isDrawing = false;
cropFrom = false;
// Remove active class from all buttons
$('.btn-secondary').removeClass('currentActive');
// Keep all objects selectable
canvas.getObjects().forEach(obj => {
if (obj !== fabricImage) {
obj.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
}
});
canvas.selection = true;
canvas.defaultCursor = 'default';
canvas.renderAll();
}
// Add this to handle switching between tools
$('.btn-secondary').click(function() {
if (this.id !== 'lineButton') {
isLineDrawing = false;
canvas.off('mouse:down', startDrawingLine);
canvas.off('mouse:move', continueDrawingLine);
canvas.off('mouse:up', finishDrawingLine);
}
});
let pinsData = [];
function addPin() {
disableAllDrawingModes();
$('#pinButton').addClass('currentActive');
// Remove URL prompt and directly add the pin
fabric.Image.fromURL('location-pin.png', function(pinIcon) {
pinIcon.scale(0.06);
const offsetX = Math.floor(Math.random() * 60) - 30;
const offsetY = Math.floor(Math.random() * 60) - 30;
pinIcon.set({
left: canvas.width / 2 + offsetX,
top: canvas.height / 2 + offsetY,
originX: 'center',
originY: 'bottom',
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
type: 'pin' // Add type identifier
});
canvas.add(pinIcon);
canvas.setActiveObject(pinIcon);
canvas.renderAll();
pushUndoState(); // Add to undo stack
});
}
function disableAllDrawingModes() {
canvas.isDrawingMode = false;
isLineDrawing = false;
isDrawing = false;
cropFrom = false;
// Remove active class from all buttons
$('.btn-secondary').removeClass('currentActive');
// Make all objects (except background image) selectable
canvas.getObjects().forEach(obj => {
if (obj !== fabricImage) {
obj.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
}
});
canvas.selection = true;
canvas.defaultCursor = 'default';
canvas.renderAll();
}
let isDropAreaSelected = false; // Track the selection state
$(document).on('click', '.select-area-btn', function() {
var currentRow = $(this).closest('tr');
var dropArea = currentRow.find('.drag-drop-area')[0];
var instructions = currentRow.find('.paste-instructions');
highlightDropArea(dropArea);
instructions.show();
$(document).off('paste');
$(document).on('paste', function(event) {
handlePaste(event, dropArea);
});
});
function pushUndoState() {
const currentState = JSON.stringify(canvas.toJSON());
if (undoStack.length > 0) {
const lastState = undoStack[undoStack.length - 1];
if (lastState.state === currentState) {
return;
}
}
if (hasUndone) {
undoStack = undoStack.slice(0, undoStack.length);
hasUndone = false;
}
undoStack.push({
state: currentState,
width: canvas.getWidth(),
height: canvas.getHeight(),
});
console.log('Undo state saved:', undoStack);
}
function undo() {
disableAllDrawingModes();
if (undoStack.length > 0) {
const lastState = undoStack.pop(); // Remove and retrieve the last state
const previousState = undoStack[undoStack.length - 1]; // Peek the next state
if (previousState) {
canvas.clear();
canvas.setWidth(previousState.width);
canvas.setHeight(previousState.height);
canvas.loadFromJSON(previousState.state, () => {
canvas.getObjects().forEach(obj => {
obj.set({
selectable: false, // Ensure objects are not selectable
evented: false, // Disable event handlers
});
// Set fabricImage if the object is an image
if (obj.type === 'image') {
fabricImage = obj;
}
});
canvas.renderAll();
toastr.success('Undo successful.');
});
} else if (initialCanvasState) {
// Restore the initial canvas state
canvas.clear();
canvas.setWidth(initialCanvasState.width);
canvas.setHeight(initialCanvasState.height);
canvas.loadFromJSON(initialCanvasState.state, () => {
canvas.getObjects().forEach(obj => {
obj.set({
selectable: false,
evented: false,
});
if (obj.type === 'image') {
fabricImage = obj;
}
});
canvas.renderAll();
toastr.warning('No actions to undo.');
});
} else {
// If no initial state, just clear the canvas
canvas.clear();
toastr.warning('No actions to undo.');
}
} else {
if (initialCanvasState) {
// Restore to initial state if undo stack is empty
canvas.clear();
canvas.setWidth(initialCanvasState.width);
canvas.setHeight(initialCanvasState.height);
canvas.loadFromJSON(initialCanvasState.state, () => {
canvas.getObjects().forEach(obj => {
obj.set({
selectable: false,
evented: false,
});
if (obj.type === 'image') {
fabricImage = obj;
}
});
canvas.renderAll();
toastr.warning('No actions to undo.');
});
} else {
toastr.warning('No actions to undo.');
}
}
}
$(document).ready(function () {
$(document).on('click', 'button.close', function() {
$(document).find("button.btn-secondary").each(function(index){
// console.log("%cREADY ABC", "color:green;font-size:25px;");
// console.log("This is button Index "+index);
if($(this).hasClass("currentActive")){
$(this).removeClass("currentActive");
}
});
isLineDrawing = false;
isDrawing = false;
cropFrom = false;
disableAllDrawingModes();
canvas.isDrawingMode = false;
cropFrom = false;
});
/////// Remove All Active Class On Load /////
$(document).on('click', 'button.btn-secondary', function() {
// console.log("%cREADY XYZ", "color:yellow;font-size:25px;");
// console.log(cropFrom);
if(!cropFrom){
$(document).find("button.btn-secondary").each(function(index){
// console.log("%cREADY ABC", "color:green;font-size:25px;");
// console.log("This is button Index "+index);
if($(this).hasClass("currentActive")){
$(this).removeClass("currentActive");
}
});
$(this).addClass("currentActive");
}
else{
// console.log("%cREADY BBBB", "color:blue;font-size:25px;");
// console.log(cropFrom);
$(document).find("button.btn-secondary").each(function(index){
// console.log("%cREADY ABC", "color:green;font-size:25px;");
// console.log("This is button Index "+index);
if($(this).hasClass("currentActive")){
$(this).removeClass("currentActive");
}
});
cropFrom = false;
}
});
});
// Add this function to handle selection mode
function toggleSelectMode() {
disableAllDrawingModes();
isSelectMode = true;
// Enable selection mode
canvas.selection = true;
canvas.discardActiveObject(); // Clear any active selection
// Make all objects except background image selectable
canvas.getObjects().forEach(obj => {
if (obj !== fabricImage) {
obj.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
}
});
// Enable canvas interactivity
canvas.interactive = true;
canvas.defaultCursor = 'pointer';
$('#selectButton').addClass('currentActive');
$('#deselectButton').removeClass('currentActive');
toastr.success('Select Mode Enabled - Click on shapes to select them');
canvas.renderAll();
}
function deselectAll() {
disableAllDrawingModes();
isSelectMode = false;
// Disable selection mode
canvas.selection = false;
canvas.discardActiveObject();
// Make all objects non-selectable
canvas.getObjects().forEach(obj => {
if (obj !== fabricImage) {
obj.set({
selectable: false,
evented: false,
hasControls: false,
hasBorders: false
});
}
});
canvas.defaultCursor = 'default';
$('#selectButton').removeClass('currentActive');
$('#deselectButton').addClass('currentActive');
toastr.info('All objects deselected');
canvas.renderAll();
}
function deleteSelected() {
const activeObject = canvas.getActiveObject();
if (activeObject) {
if (activeObject !== fabricImage) {
// Disable all drawing modes first
canvas.isDrawingMode = false;
isLineDrawing = false;
isDrawing = false;
// Remove all active classes from buttons
$('.btn-secondary').removeClass('currentActive');
// Remove drawing event listeners
canvas.off('mouse:down', startDrawingLine);
canvas.off('mouse:move', continueDrawingLine);
canvas.off('mouse:up', finishDrawingLine);
// Delete the object
canvas.remove(activeObject);
canvas.discardActiveObject();
// Keep all remaining objects selectable
canvas.getObjects().forEach(obj => {
if (obj !== fabricImage) {
obj.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
}
});
canvas.renderAll();
pushUndoState(); // Save state for undo
toastr.success('Object deleted successfully');
} else {
toastr.warning('Cannot delete the background image');
}
} else {
toastr.warning('Please select an object to delete');
}
}
// Add this to handle object selection events
canvas.on('selection:created', function(options) {
if (isSelectMode && options.target) {
options.target.set({
hasControls: true,
hasBorders: true
});
}
});
canvas.off('selection:cleared');
// Add this function to disable shape mode when switching to other tools
function disableShapeMode() {
isShapeMode = false;
canvas.getObjects().forEach(obj => {
if (obj !== fabricImage) {
obj.set({
selectable: false,
evented: false,
hasControls: false,
hasBorders: false
});
}
});
canvas.renderAll();
}
// When the modal opens, set shape mode as default
$('#editImageModal').on('shown.bs.modal', function () {
isShapeMode = true;
disableAllDrawingModes();
});
function zoomIn() {
disableAllDrawingModes();
if (currentZoom < MAX_ZOOM) {
currentZoom += ZOOM_STEP;
applyZoom();
// Disable resize button and update its tooltip
const resizeBtn = $('.dimension-display .btn');
resizeBtn.prop('disabled', true);
resizeBtn.attr('data-original-title', 'Resize is disabled while zooming. Reset zoom to enable resize.');
$('[data-toggle="tooltip"]').tooltip('dispose').tooltip();
}
}
function zoomOut() {
disableAllDrawingModes();
if (currentZoom > MIN_ZOOM) {
currentZoom -= ZOOM_STEP;
applyZoom();
// Disable resize button and update its tooltip
const resizeBtn = $('.dimension-display .btn');
resizeBtn.prop('disabled', true);
resizeBtn.attr('data-original-title', 'Resize is disabled while zooming. Reset zoom to enable resize.');
$('[data-toggle="tooltip"]').tooltip('dispose').tooltip();
}
}
function resetZoom() {
const resizeBtn = $('.dimension-display .btn');
resizeBtn.prop('disabled', false);
resizeBtn.attr('data-original-title', 'Image resize cannot be undone');
$('[data-toggle="tooltip"]').tooltip('dispose').tooltip();
disableAllDrawingModes();
if (currentZoom !== 1) {
// Store current annotations
const currentObjects = canvas.getObjects().filter(obj => obj !== fabricImage);
currentZoom = 1;
applyZoom();
// Restore annotations with adjusted positions
currentObjects.forEach(obj => {
// Calculate position ratios
const leftRatio = obj.left / canvas.width;
const topRatio = obj.top / canvas.height;
// Set new position based on ratios
obj.set({
left: leftRatio * canvas.width,
top: topRatio * canvas.height,
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
// Add back to canvas
canvas.add(obj);
});
canvas.renderAll();
}
}
function applyZoom() {
// Store current dimensions
const currentWidth = canvas.getWidth();
const currentHeight = canvas.getHeight();
// Calculate new dimensions
const scaledWidth = initialCanvasState.width * currentZoom;
const scaledHeight = initialCanvasState.height * currentZoom;
// Set new dimensions
canvas.setWidth(scaledWidth);
canvas.setHeight(scaledHeight);
// Scale all objects
canvas.getObjects().forEach(obj => {
const originalScaleX = obj.scaleX;
const originalScaleY = obj.scaleY;
const originalLeft = obj.left;
const originalTop = obj.top;
obj.set({
scaleX: originalScaleX * (scaledWidth / currentWidth),
scaleY: originalScaleY * (scaledHeight / currentHeight),
left: originalLeft * (scaledWidth / currentWidth),
top: originalTop * (scaledHeight / currentHeight)
});
});
canvas.renderAll();
updateDimensionDisplay();
}
// Add this function to reset the image to original state
function resetImage() {
disableAllDrawingModes();
if (initialCanvasState) {
// Reset zoom level
currentZoom = 1;
// Store current annotations before reset
const currentObjects = canvas.getObjects().filter(obj => obj !== fabricImage);
// Reset canvas dimensions to initial state
canvas.setWidth(initialCanvasState.width);
canvas.setHeight(initialCanvasState.height);
// Load the initial state
canvas.loadFromJSON(initialCanvasState.state, () => {
// Ensure the background image is not selectable
canvas.getObjects().forEach(obj => {
if (obj.type === 'image') {
fabricImage = obj;
obj.set({
selectable: false,
evented: false
});
}
});
// Restore current annotations with adjusted positions
currentObjects.forEach(obj => {
// Calculate position ratios
const leftRatio = obj.left / canvas.width;
const topRatio = obj.top / canvas.height;
// Set new position based on ratios
obj.set({
left: leftRatio * initialCanvasState.width,
top: topRatio * initialCanvasState.height,
selectable: true,
evented: true,
hasControls: true,
hasBorders: true
});
// Add back to canvas
canvas.add(obj);
});
canvas.renderAll();
});
}
}
// Update dimension display
function updateDimensionDisplay() {
const dimensionHtml = `
`;
// Replace the existing dimension display with the new one
$('.dimension-display').replaceWith(dimensionHtml);
// Initialize tooltips
$('[data-toggle="tooltip"]').tooltip();
// Update the values
$('#imageWidth').val(Math.round(canvas.width));
$('#imageHeight').val(Math.round(canvas.height));
// Store original aspect ratio
const originalAspectRatio = canvas.width / canvas.height;
// Add event listeners for dimension inputs
$('#imageWidth').on('input', function() {
if ($('#useExactDimensions').is(':checked')) {
const newWidth = parseInt($(this).val());
if (newWidth && newWidth > 0) {
const newHeight = Math.round(newWidth / originalAspectRatio);
$('#imageHeight').val(newHeight);
}
}
});
$('#imageHeight').on('input', function() {
if ($('#useExactDimensions').is(':checked')) {
const newHeight = parseInt($(this).val());
if (newHeight && newHeight > 0) {
const newWidth = Math.round(newHeight * originalAspectRatio);
$('#imageWidth').val(newWidth);
}
}
});
// Add event listener for the aspect ratio checkbox
$('#useExactDimensions').on('change', function() {
if ($(this).is(':checked')) {
const width = parseInt($('#imageWidth').val());
const height = parseInt($('#imageHeight').val());
if (width && height) {
const widthRatio = width / originalAspectRatio;
const heightRatio = height * originalAspectRatio;
if (Math.abs(widthRatio - height) < Math.abs(heightRatio - width)) {
$('#imageHeight').val(Math.round(width / originalAspectRatio));
} else {
$('#imageWidth').val(Math.round(height * originalAspectRatio));
}
}
}
});
}
// Add CSS for the input groups
const styles = `
.input-group-text {
background-color: #e9ecef;
border: 1px solid #ced4da;
padding: 0.375rem 0.75rem;
font-size: 0.9rem;
color: #495057;
}
.input-group input {
border-right: none;
}
.input-group-append {
margin-left: -1px;
}
.dimension-display .input-group {
display: inline-flex;
vertical-align: middle;
}
@media (max-width: 576px) {
.dimension-display {
flex-direction: column;
align-items: stretch !important;
gap: 1rem !important;
}
.dimension-display .input-group {
width: 100% !important;
margin-bottom: 0.5rem;
}
.dimension-display .btn {
width: 100% !important;
margin-top: 0.5rem;
}
.custom-control {
margin: 0.5rem 0;
}
}
`;
// Add these additional styles
const additionalStyles = `
.dimension-display {
padding: 10px;
border-radius: 6px;
margin-bottom: 10px;
background-color: #f8f9fa;
}
.input-group {
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border-radius: 4px;
overflow: hidden;
}
.input-group-text {
background-color: #e9ecef;
border: 1px solid #ced4da;
padding: 0.375rem 0.75rem;
font-size: 0.9rem;
color: #495057;
min-width: 35px;
justify-content: center;
}
.input-group .form-control {
border: 1px solid #ced4da;
border-left: none;
border-right: none;
padding: 0.375rem 0.75rem;
}
.input-group .form-control:focus {
box-shadow: none;
border-color: #ced4da;
}
.custom-checkbox {
padding-left: 1.5rem;
margin: 0 0.5rem;
}
.custom-control-label {
font-size: 0.9rem;
color: #495057;
cursor: pointer;
white-space: nowrap;
}
.custom-control-input:checked ~ .custom-control-label::before {
background-color: #712C63;
border-color: #712C63;
}
.btn-primary {
padding: 0.375rem 0.75rem;
font-size: 0.9rem;
white-space: nowrap;
height: 38px;
line-height: 1;
}
`;
// Update the styles in the document
if (!document.getElementById('dimension-display-styles')) {
$('