���� ������������������������������������ 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
Image Preview `; $('#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 = `
px
×
px
`; // 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')) { $('