class CropEditor { constructor(opt) { this.opt = opt; this.edit_crop_button = document.createElement("span"); this.edit_crop_button.className = "btn btn-light"; this.edit_crop_button.textContent = "Edit crop"; this.edit_crop_button.addEventListener('click', this.show.bind(this)); this.thumbnail_select = document.getElementById(opt.thumbnail_select_id); this.thumbnail_select.addEventListener('change', this.toggleCropButton.bind(this)); this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling); this.toggleCropButton(); } initDOM() { this.container = document.createElement("div"); this.container.id = "crop_editor"; this.initPositionForm(); this.initImageContainer(); this.parent = document.getElementById(this.opt.editor_container_parent_id); this.parent.appendChild(this.container); } initPositionForm() { this.position = document.createElement("fieldset"); this.position.className = "crop_position"; this.container.appendChild(this.position); const addNumericControl = (label, changeEvent) => { const labelEl = document.createTextNode("div"); this.position.appendChild(labelEl); const control = document.createElement("input"); control.className = 'form-control d-inline'; control.type = 'number'; control.addEventListener("change", changeEvent); control.addEventListener("keyup", changeEvent); this.position.appendChild(control); return control; }; this.source_x = addNumericControl("Source X:", this.positionBoundary); this.source_y = addNumericControl("Source Y:", this.positionBoundary); this.crop_width = addNumericControl("Crop width:", this.positionBoundary); this.crop_height = addNumericControl("Crop height:", this.positionBoundary); this.crop_constrain_label = document.createElement("label"); this.position.appendChild(this.crop_constrain_label); this.crop_constrain = document.createElement("input"); this.crop_constrain.checked = true; this.crop_constrain.type = 'checkbox'; this.crop_constrain_label.appendChild(this.crop_constrain); this.crop_constrain_text = document.createTextNode('Constrain proportions'); this.crop_constrain_label.appendChild(this.crop_constrain_text); this.save_button = document.createElement("span"); this.save_button.className = "btn btn-light"; this.save_button.textContent = "Save"; this.save_button.addEventListener('click', this.save.bind(this)); this.position.appendChild(this.save_button); this.abort_button = document.createElement("span"); this.abort_button.className = "btn btn-danger"; this.abort_button.textContent = "Abort"; this.abort_button.addEventListener('click', this.hide.bind(this)); this.position.appendChild(this.abort_button); } initImageContainer() { this.image_container = document.createElement("div"); this.image_container.className = "crop_image_container"; this.container.appendChild(this.image_container); this.crop_boundary = document.createElement("div"); this.crop_boundary.id = "crop_boundary"; this.image_container.appendChild(this.crop_boundary); this.original_image = document.createElement("img"); this.original_image.draggable = false; this.original_image.id = "original_image"; this.original_image.src = this.opt.original_image_src; this.image_container.appendChild(this.original_image); } setDefaultCrop(cropAspectRatio, cropMethod) { let source = this.original_image; let sourceAspectRatio = source.naturalWidth / source.naturalHeight; // Cropping from the centre? if (cropMethod === "c" || cropMethod === "s") { // Crop vertically from the centre, using the entire width. if (sourceAspectRatio <= cropAspectRatio) { this.crop_width.value = source.naturalWidth; this.crop_height.value = Math.ceil(source.naturalWidth / cropAspectRatio); this.source_x.value = 0; this.source_y.value = Math.ceil((source.naturalHeight - this.crop_height.value) / 2); } // Crop horizontally from the centre, using the entire height. else { this.crop_width.value = Math.ceil(cropAspectRatio * source.naturalHeight); this.crop_height.value = source.naturalHeight; this.source_x.value = Math.ceil((source.naturalWidth - this.crop_width.value) / 2); this.source_y.value = 0; } } // Cropping a top or bottom slice? else { // Can we actually take a top or bottom slice from the original image? if (sourceAspectRatio <= cropAspectRatio) { this.crop_width.value = source.naturalWidth; this.crop_height.value = Math.floor(source.naturalWidth / cropAspectRatio); this.source_x.value = "0"; this.source_y.value = cropMethod.indexOf("t") !== -1 ? "0" : source.naturalHeight - this.crop_height.value; } // Otherwise, take a vertical slice from the centre. else { this.crop_width.value = Math.floor(source.naturalHeight * cropAspectRatio); this.crop_height.value = source.naturalHeight; this.source_x.value = Math.floor((source.naturalWidth - this.crop_width.value) / 2); this.source_y.value = "0"; } } } setPositionFormValues() { let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; if (typeof current.crop_region === "undefined") { let aspectRatio = current.crop_width / current.crop_height; this.setDefaultCrop(aspectRatio, current.crop_method); } else { let region = current.crop_region.split(','); this.crop_width.value = region[0]; this.crop_height.value = region[1]; this.source_x.value = region[2]; this.source_y.value = region[3]; } this.crop_width.min = 1; this.crop_height.min = 1; this.source_x.min = 0; this.source_y.min = 0; let source = this.original_image; this.crop_width.max = source.naturalWidth; this.crop_height.max = source.naturalHeight; this.source_x.max = source.naturalWidth - 1; this.source_y.max = source.naturalHeight - 1; this.crop_constrain_text.textContent = `Constrain proportions (${current.crop_width} × ${current.crop_height})`; } showContainer() { this.container.style.display = ''; this.setPositionFormValues(); this.positionBoundary(); this.addEvents(); } save() { let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; let payload = { thumb_width: current.crop_width, thumb_height: current.crop_height, crop_method: current.crop_method, crop_width: this.crop_width.value, crop_height: this.crop_height.value, source_x: this.source_x.value, source_y: this.source_y.value }; let req = HttpRequest("post", this.opt.submitUrl + "?id=" + this.opt.asset_id + "&updatethumb", "data=" + encodeURIComponent(JSON.stringify(payload)), function(response) { this.opt.after_save(response); this.hide(); }.bind(this)); } show() { if (typeof this.container === "undefined") { this.initDOM(); } // Defer showing and positioning until image is loaded. // !!! TODO: add a spinner in the mean time? if (this.original_image.naturalWidth > 0) { this.showContainer(); } else { this.original_image.addEventListener("load", event => this.showContainer()); } } hide() { this.container.style.display = "none"; } addEvents(event) { let cropTarget = this.image_container; cropTarget.addEventListener('mousedown', this.cropSelectionStart.bind(this)); cropTarget.addEventListener('mousemove', this.cropSelection.bind(this)); cropTarget.addEventListener('mouseup', this.cropSelectionEnd.bind(this)); // cropTarget.addEventListener('mouseout', this.cropSelectionEnd.bind(this)); this.original_image.addEventListener('mousedown', event => {return false}); this.original_image.addEventListener('dragstart', event => {return false}); let moveTarget = this.crop_boundary; moveTarget.addEventListener('mousedown', this.moveSelectionStart.bind(this)); moveTarget.addEventListener('mousemove', this.moveSelection.bind(this)); moveTarget.addEventListener('mouseup', this.moveSelectionEnd.bind(this)); window.addEventListener('resize', this.positionBoundary.bind(this)); } cropSelectionStart(event) { if (this.isMoving) { return false; } let dragStartX = event.x - this.image_container.offsetLeft; let dragStartY = event.y - this.image_container.offsetTop; if (dragStartX > this.original_image.clientWidth || dragStartY > this.original_image.clientHeight) { return; } this.isDragging = true; this.dragStartX = dragStartX; this.dragStartY = dragStartY; } cropSelectionEnd(event) { this.isDragging = false; this.handleCropSelectionEvent(event); } cropSelection(event) { this.handleCropSelectionEvent(event); } getScaleFactor() { return this.original_image.naturalWidth / this.original_image.clientWidth; } handleCropSelectionEvent(event) { if (!this.isDragging) { return; } this.dragEndX = event.x - this.image_container.offsetLeft; this.dragEndY = event.y - this.image_container.offsetTop; let scaleFactor = this.getScaleFactor(); this.source_x.value = Math.ceil(Math.min(this.dragStartX, this.dragEndX) * scaleFactor); this.source_y.value = Math.ceil(Math.min(this.dragStartY, this.dragEndY) * scaleFactor); let width = Math.ceil(Math.abs(this.dragEndX - this.dragStartX) * scaleFactor); this.crop_width.value = Math.min(width, this.original_image.naturalWidth - this.source_x.value); let height = Math.ceil(Math.abs(this.dragEndY - this.dragStartY) * scaleFactor); this.crop_height.value = Math.min(height, this.original_image.naturalHeight - this.source_y.value); if (this.crop_constrain.checked) { let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; let currentAspectRatio = parseInt(this.crop_width.value) / parseInt(this.crop_height.value); let targetAspectRatio = current.crop_width / current.crop_height; if (Math.abs(currentAspectRatio - targetAspectRatio) > 0.001) { // Landscape? if (targetAspectRatio > 1.0) { let height = Math.ceil(this.crop_width.value / targetAspectRatio); if (parseInt(this.source_y.value) + height > this.original_image.naturalHeight) { height = this.original_image.naturalHeight - this.source_y.value; } this.crop_width.value = height * targetAspectRatio; this.crop_height.value = height; } // Portrait? else { let width = Math.ceil(this.crop_height.value * targetAspectRatio); if (parseInt(this.source_x.value) + width > this.original_image.naturalWidth) { width = this.original_image.naturalWidth - this.source_x.value; } this.crop_width.value = width; this.crop_height.value = width / targetAspectRatio; } } } this.positionBoundary(); } handleCropMoveEvent(event) { if (!this.isMoving) { return; } this.dragEndX = event.x - this.crop_boundary.offsetLeft; this.dragEndY = event.y - this.crop_boundary.offsetTop; let scaleFactor = this.getScaleFactor(); let x = parseInt(this.source_x.value) + Math.ceil((this.dragEndX - this.dragStartX) * scaleFactor); if (x + parseInt(this.crop_width.value) > this.original_image.naturalWidth) { x += this.original_image.naturalWidth - (x + parseInt(this.crop_width.value)); } this.source_x.value = Math.max(x, 0); let y = parseInt(this.source_y.value) + Math.ceil((this.dragEndY - this.dragStartY) * scaleFactor); if (y + parseInt(this.crop_height.value) > this.original_image.naturalHeight) { y += this.original_image.naturalHeight - (y + parseInt(this.crop_height.value)); } this.source_y.value = Math.max(y, 0); this.positionBoundary(); } moveSelectionStart(event) { if (this.isDragging) { return false; } this.isMoving = true; this.dragStartX = event.x - this.crop_boundary.offsetLeft; this.dragStartY = event.y - this.crop_boundary.offsetTop; } moveSelectionEnd(event) { this.isMoving = false; this.handleCropMoveEvent(event); } moveSelection(event) { this.handleCropMoveEvent(event); } toggleCropButton() { let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; this.edit_crop_button.style.display = typeof current.crop_method === "undefined" ? "none" : ""; } positionBoundary(event) { let scaleFactor = this.getScaleFactor(); crop_boundary.style.left = parseInt(this.source_x.value) / scaleFactor + "px"; crop_boundary.style.top = parseInt(this.source_y.value) / scaleFactor + "px"; crop_boundary.style.width = parseInt(this.crop_width.value) / scaleFactor + "px"; crop_boundary.style.height = parseInt(this.crop_height.value) / scaleFactor + "px"; } }