diff --git a/assets/cms/components/Image/ThumbnailEditor.vue b/assets/cms/components/Image/ThumbnailEditor.vue index e5745f13..fba754d6 100644 --- a/assets/cms/components/Image/ThumbnailEditor.vue +++ b/assets/cms/components/Image/ThumbnailEditor.vue @@ -3,22 +3,16 @@
- - + +
@@ -42,13 +36,34 @@ export default { type: Number, required: true }, + sizingMode: { + type: String, + required: true, + validator: function(value) { + return ['proportional', 'exact'].indexOf(value) !== -1 + } + }, + fileId: { + type: Number, + required: true + }, + fileVersionId: { + type: Number, + required: true + }, + thumbnailHandle: { + type: String, + required: true + }, accessToken: { type: String, required: true }, lang: { type: Object, - required: true + default: () => ({ + save: 'Save' + }) }, src: { type: String, @@ -64,35 +79,115 @@ export default { saveInProgress: false }), computed: { - stencilSize: function() { - const stencilSize = {} - stencilSize.width = this.width - stencilSize.height = this.height - return stencilSize + aspectRatio: function() { + // Only lock aspect ratio for EXACT mode + // For PROPORTIONAL mode, allow free aspect ratio (let user maintain original image ratio) + if (this.sizingMode === 'exact') { + return this.width / this.height + } + return undefined + }, + stencilProps: function() { + const props = { + // Show corner handlers for resizing + // For EXACT mode: corners maintain locked aspect ratio + // For PROPORTIONAL mode: corners allow free resizing + handlers: { + eastNorth: true, // top-right corner + north: false, // top edge + westNorth: true, // top-left corner + west: false, // left edge + westSouth: true, // bottom-left corner + south: false, // bottom edge + eastSouth: true, // bottom-right corner + east: false // right edge + }, + movable: true, + resizable: true, + // Show border lines on all four edges for visibility + lines: { + north: true, + east: true, + south: true, + west: true + }, + // Style the lines for better visibility + linesClasses: { + default: 'ccm-thumbnail-stencil-line', + north: 'ccm-thumbnail-stencil-line--north', + east: 'ccm-thumbnail-stencil-line--east', + south: 'ccm-thumbnail-stencil-line--south', + west: 'ccm-thumbnail-stencil-line--west' + }, + // Add custom class to preview for border styling + previewClass: 'ccm-thumbnail-stencil-preview' + } + + // Only lock aspect ratio for EXACT mode + if (this.aspectRatio !== undefined) { + props.minAspectRatio = this.aspectRatio + props.maxAspectRatio = this.aspectRatio + } + + return props + }, + resizeImageConfig: function() { + return { + // Don't adjust stencil size when zooming + adjustStencil: false, + // Enable touch zoom + touch: true, + // Enable smooth mouse wheel zoom with fine control (2% per wheel tick) + wheel: { + ratio: 0.02 + } + } }, canvas: function() { - const canvas = {} - canvas.maxWidth = this.width - canvas.maxHeight = this.height - return canvas + // Don't restrict canvas size - let it scale naturally + // The actual output size is controlled by getResult() parameters + return {} } }, methods: { zoomIn() { - this.$refs.cropper.zoom(1.2) + if (this.$refs.cropper) { + // Zoom in by 5% (1.05x) for finer control + this.$refs.cropper.zoom(1.05) + } }, zoomOut() { - this.$refs.cropper.zoom(0.8) + if (this.$refs.cropper) { + // Zoom out by 5% (0.95x) for finer control + this.$refs.cropper.zoom(0.95) + } }, save() { var my = this this.saveInProgress = true - const { canvas } = this.$refs.cropper.getResult() - if (canvas) { + + // Get the cropped result + const result = this.$refs.cropper.getResult() + const sourceCanvas = result.canvas + + if (sourceCanvas) { + // Create a new canvas at the exact thumbnail size + const canvas = document.createElement('canvas') + canvas.width = this.width + canvas.height = this.height + + // Draw the cropped image at the exact size + const ctx = canvas.getContext('2d') + ctx.imageSmoothingEnabled = true + ctx.imageSmoothingQuality = 'high' + ctx.drawImage(sourceCanvas, 0, 0, this.width, this.height) + const form = new FormData() form.append('ccm_token', this.accessToken) + // Convert canvas to data URL for preview update + const imgData = canvas.toDataURL('image/png') canvas.toBlob(blob => { - form.append('file', blob) + form.append('file', blob, 'thumbnail.png') new ConcreteAjaxRequest({ url: my.uploadUrl, processData: false, @@ -100,7 +195,21 @@ export default { dataType: 'json', data: form, success: function(r) { - jQuery.fn.dialog.closeTop() + // Prepare event data + const eventData = { + isThumbnail: true, + handle: my.thumbnailHandle, + fID: my.fileId, + fvID: my.fileVersionId, + imgData: imgData + } + + // Trigger namespaced events for different listeners + // Each dialog/page has its own namespace to avoid unbinding conflicts + Concrete.event.trigger('ImageEditorDidSave.thumbnails.thumbnailsDialog', eventData) + Concrete.event.trigger('ImageEditorDidSave.thumbnails.detailsPage', eventData) + + // Show success notification ConcreteAlert.notify({ message: ccmi18n_filemanager.thumbnailImageSaved, title: ccmi18n_filemanager.thumbnailImages @@ -110,8 +219,64 @@ export default { }) } } - }, - mounted() { } } +