diff --git a/app/javascript/retrospring/controllers/cropper_controller.ts b/app/javascript/retrospring/controllers/cropper_controller.ts new file mode 100644 index 00000000..921e328c --- /dev/null +++ b/app/javascript/retrospring/controllers/cropper_controller.ts @@ -0,0 +1,49 @@ +import { Controller } from '@hotwired/stimulus'; +import Croppr from 'croppr'; + +export default class extends Controller { + static targets = ['input', 'controls', 'cropper', 'x', 'y', 'w', 'h']; + + declare readonly inputTarget: HTMLInputElement; + declare readonly controlsTarget: HTMLElement; + declare readonly cropperTarget: HTMLImageElement; + declare readonly xTarget: HTMLInputElement; + declare readonly yTarget: HTMLInputElement; + declare readonly wTarget: HTMLInputElement; + declare readonly hTarget: HTMLInputElement; + + static values = { + aspectRatio: String + }; + + declare readonly aspectRatioValue: string; + + readImage(file: File, callback: (string) => void): void { + callback((window.URL || window.webkitURL).createObjectURL(file)); + } + + updateValues(data: Record): void { + this.xTarget.value = data.x; + this.yTarget.value = data.y; + this.wTarget.value = data.width; + this.hTarget.value = data.height; + } + + change(): void { + this.controlsTarget.classList.toggle('d-none'); + + if (this.inputTarget.files && this.inputTarget.files[0]) { + this.readImage(this.inputTarget.files[0], (src) => { + this.cropperTarget.src = src; + + new Croppr(this.cropperTarget, { + aspectRatio: parseFloat(this.aspectRatioValue), + startSize: [100, 100, '%'], + onCropStart: this.updateValues.bind(this), + onCropMove: this.updateValues.bind(this), + onCropEnd: this.updateValues.bind(this) + }); + }); + } + } +} diff --git a/app/javascript/retrospring/features/settings/crop.ts b/app/javascript/retrospring/features/settings/crop.ts deleted file mode 100644 index c30e3e5f..00000000 --- a/app/javascript/retrospring/features/settings/crop.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Croppr from 'croppr'; - -const readImage = (file, callback) => callback((window.URL || window.webkitURL).createObjectURL(file)); - -export function profilePictureChangeHandler(event: Event): void { - const input = event.target as HTMLInputElement; - - const cropControls = document.querySelector('#profile-picture-crop-controls'); - cropControls.classList.toggle('d-none'); - - if (input.files && input.files[0]) { - readImage(input.files[0], (src) => { - const updateValues = (data) => { - document.querySelector('#profile_picture_x').value = data.x; - document.querySelector('#profile_picture_y').value = data.y; - document.querySelector('#profile_picture_w').value = data.width; - document.querySelector('#profile_picture_h').value = data.height; - } - - const cropper = document.querySelector('#profile-picture-cropper'); - cropper.src = src; - - new Croppr(cropper, { - aspectRatio: 1, - startSize: [100, 100, '%'], - onCropStart: updateValues, - onCropMove: updateValues, - onCropEnd: updateValues - }); - }); - } -} - -export function profileHeaderChangeHandler(event: Event): void { - const input = event.target as HTMLInputElement; - - const cropControls = document.querySelector('#profile-header-crop-controls'); - cropControls.classList.toggle('d-none'); - - if (input.files && input.files[0]) { - readImage(input.files[0], (src) => { - const updateValues = (data) => { - document.querySelector('#profile_header_x').value = data.x; - document.querySelector('#profile_header_y').value = data.y; - document.querySelector('#profile_header_w').value = data.width; - document.querySelector('#profile_header_h').value = data.height; - } - - const cropper = document.querySelector('#profile-header-cropper'); - cropper.src = src; - - new Croppr(cropper, { - aspectRatio: 7/30, - startSize: [100, 100, '%'], - onCropStart: updateValues, - onCropMove: updateValues, - onCropEnd: updateValues - }); - }); - } -} \ No newline at end of file diff --git a/app/javascript/retrospring/features/settings/index.ts b/app/javascript/retrospring/features/settings/index.ts index ad6b214e..5ea4bb2d 100644 --- a/app/javascript/retrospring/features/settings/index.ts +++ b/app/javascript/retrospring/features/settings/index.ts @@ -1,11 +1,8 @@ import registerEvents from "utilities/registerEvents"; -import { profileHeaderChangeHandler, profilePictureChangeHandler } from "./crop"; import { userSubmitHandler } from "./password"; export default (): void => { registerEvents([ - { type: 'submit', target: document.querySelector('#edit_user'), handler: userSubmitHandler }, - { type: 'change', target: document.querySelector('#user_profile_picture[type=file]'), handler: profilePictureChangeHandler }, - { type: 'change', target: document.querySelector('#user_profile_header[type=file]'), handler: profileHeaderChangeHandler } + { type: 'submit', target: document.querySelector('#edit_user'), handler: userSubmitHandler } ]); } diff --git a/app/javascript/retrospring/initializers/stimulus.ts b/app/javascript/retrospring/initializers/stimulus.ts index 3af6306c..64ba1a50 100644 --- a/app/javascript/retrospring/initializers/stimulus.ts +++ b/app/javascript/retrospring/initializers/stimulus.ts @@ -7,6 +7,7 @@ import FormatPopupController from "retrospring/controllers/format_popup_controll import CollapseController from "retrospring/controllers/collapse_controller"; import ThemeController from "retrospring/controllers/theme_controller"; import CapabilitiesController from "retrospring/controllers/capabilities_controller"; +import CropperController from "retrospring/controllers/cropper_controller"; /** * This module sets up Stimulus and our controllers @@ -23,6 +24,7 @@ export default function (): void { window['Stimulus'].register('character-count', CharacterCountController); window['Stimulus'].register('character-count-warning', CharacterCountWarningController); window['Stimulus'].register('collapse', CollapseController); + window['Stimulus'].register('cropper', CropperController); window['Stimulus'].register('format-popup', FormatPopupController); window['Stimulus'].register('theme', ThemeController); } diff --git a/app/views/settings/profile/edit.html.haml b/app/views/settings/profile/edit.html.haml index 7cd29926..32721590 100644 --- a/app/views/settings/profile/edit.html.haml +++ b/app/views/settings/profile/edit.html.haml @@ -2,36 +2,38 @@ .card-body = bootstrap_form_for(current_user, url: settings_profile_picture_path, html: { multipart: true }, method: :patch, data: { turbo: false }) do |f| - .d-flex#profile-picture-media - .flex-shrink-0 - %img.avatar-lg.me-3{ src: current_user.profile_picture.url(:medium) } - .flex-grow-1 - = f.file_field :profile_picture, accept: APP_CONFIG[:accepted_image_formats].join(",") + %div{ data: { controller: "cropper", cropper_aspect_ratio_value: "1" } } + .d-flex + .flex-shrink-0 + %img.avatar-lg.me-3{ src: current_user.profile_picture.url(:medium) } + .flex-grow-1 + = f.file_field :profile_picture, accept: APP_CONFIG[:accepted_image_formats].join(","), data: { cropper_target: "input", action: "cropper#change" } - .row.d-none#profile-picture-crop-controls - .col-sm-10.col-md-8 - %strong= t(".adjust.profile_picture") - %img#profile-picture-cropper{ src: current_user.profile_picture.url(:medium) } + .row.d-none{ data: { cropper_target: "controls" } } + .col-sm-10.col-md-8 + %strong= t(".adjust.profile_picture") + %img{ src: current_user.profile_picture.url(:medium), data: { cropper_target: "cropper" } } - .row.mb-2#profile-header-media - .col-xs-12.col-md-6 - %img.mw-100.me-3{ src: current_user.profile_header.url(:mobile) } - .col-xs-12.col-md-6.mt-3.mt-sm-0.ps-3.pe-3 - = f.file_field :profile_header, accept: APP_CONFIG[:accepted_image_formats].join(",") + - %i[profile_picture_x profile_picture_y profile_picture_w profile_picture_h].each do |attrib| + = f.hidden_field attrib, id: attrib, data: { cropper_target: attrib.to_s.split("_").last } - .row.d-none#profile-header-crop-controls - .col-sm-10.col-md-8 - %strong= t(".adjust.profile_header") - %img#profile-header-cropper{ src: current_user.profile_header.url(:web) } + %div{ data: { controller: "cropper", cropper_aspect_ratio_value: "0.23" } } + .row.mb-2 + .col-xs-12.col-md-6 + %img.mw-100.me-3{ src: current_user.profile_header.url(:mobile) } + .col-xs-12.col-md-6.mt-3.mt-sm-0.ps-3.pe-3 + = f.file_field :profile_header, accept: APP_CONFIG[:accepted_image_formats].join(","), data: { cropper_target: "input", action: "cropper#change" } + + .row.d-none{ data: { cropper_target: "controls" } } + .col-sm-10.col-md-8 + %strong= t(".adjust.profile_header") + %img{ src: current_user.profile_header.url(:web), data: { cropper_target: "cropper" } } + + - %i[profile_header_x profile_header_y profile_header_w profile_header_h].each do |attrib| + = f.hidden_field attrib, id: attrib, data: { cropper_target: attrib.to_s.split("_").last } = f.check_box :show_foreign_themes - - %i[profile_picture_x profile_picture_y profile_picture_w profile_picture_h].each do |attrib| - = f.hidden_field attrib, id: attrib - - - %i[profile_header_x profile_header_y profile_header_w profile_header_h].each do |attrib| - = f.hidden_field attrib, id: attrib - = f.primary t(".submit_picture") .card .card-body