Merge pull request #984 from Retrospring/feature/stimulus-cropper

Refactor image cropping into Stimulus controller
This commit is contained in:
Karina Kwiatek 2023-01-27 17:22:58 +01:00 committed by GitHub
commit 2ecfe38e5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 78 additions and 89 deletions

View File

@ -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<string, string>): 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)
});
});
}
}
}

View File

@ -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<HTMLInputElement>('#profile_picture_x').value = data.x;
document.querySelector<HTMLInputElement>('#profile_picture_y').value = data.y;
document.querySelector<HTMLInputElement>('#profile_picture_w').value = data.width;
document.querySelector<HTMLInputElement>('#profile_picture_h').value = data.height;
}
const cropper = document.querySelector<HTMLImageElement>('#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<HTMLInputElement>('#profile_header_x').value = data.x;
document.querySelector<HTMLInputElement>('#profile_header_y').value = data.y;
document.querySelector<HTMLInputElement>('#profile_header_w').value = data.width;
document.querySelector<HTMLInputElement>('#profile_header_h').value = data.height;
}
const cropper = document.querySelector<HTMLImageElement>('#profile-header-cropper');
cropper.src = src;
new Croppr(cropper, {
aspectRatio: 7/30,
startSize: [100, 100, '%'],
onCropStart: updateValues,
onCropMove: updateValues,
onCropEnd: updateValues
});
});
}
}

View File

@ -1,11 +1,8 @@
import registerEvents from "utilities/registerEvents"; import registerEvents from "utilities/registerEvents";
import { profileHeaderChangeHandler, profilePictureChangeHandler } from "./crop";
import { userSubmitHandler } from "./password"; import { userSubmitHandler } from "./password";
export default (): void => { export default (): void => {
registerEvents([ registerEvents([
{ type: 'submit', target: document.querySelector('#edit_user'), handler: userSubmitHandler }, { 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 }
]); ]);
} }

View File

@ -7,6 +7,7 @@ import FormatPopupController from "retrospring/controllers/format_popup_controll
import CollapseController from "retrospring/controllers/collapse_controller"; import CollapseController from "retrospring/controllers/collapse_controller";
import ThemeController from "retrospring/controllers/theme_controller"; import ThemeController from "retrospring/controllers/theme_controller";
import CapabilitiesController from "retrospring/controllers/capabilities_controller"; import CapabilitiesController from "retrospring/controllers/capabilities_controller";
import CropperController from "retrospring/controllers/cropper_controller";
/** /**
* This module sets up Stimulus and our controllers * 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', CharacterCountController);
window['Stimulus'].register('character-count-warning', CharacterCountWarningController); window['Stimulus'].register('character-count-warning', CharacterCountWarningController);
window['Stimulus'].register('collapse', CollapseController); window['Stimulus'].register('collapse', CollapseController);
window['Stimulus'].register('cropper', CropperController);
window['Stimulus'].register('format-popup', FormatPopupController); window['Stimulus'].register('format-popup', FormatPopupController);
window['Stimulus'].register('theme', ThemeController); window['Stimulus'].register('theme', ThemeController);
} }

View File

@ -2,35 +2,37 @@
.card-body .card-body
= bootstrap_form_for(current_user, url: settings_profile_picture_path, html: { multipart: true }, method: :patch, data: { turbo: false }) do |f| = 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 %div{ data: { controller: "cropper", cropper_aspect_ratio_value: "1" } }
.d-flex
.flex-shrink-0 .flex-shrink-0
%img.avatar-lg.me-3{ src: current_user.profile_picture.url(:medium) } %img.avatar-lg.me-3{ src: current_user.profile_picture.url(:medium) }
.flex-grow-1 .flex-grow-1
= f.file_field :profile_picture, accept: APP_CONFIG[:accepted_image_formats].join(",") = 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 .row.d-none{ data: { cropper_target: "controls" } }
.col-sm-10.col-md-8 .col-sm-10.col-md-8
%strong= t(".adjust.profile_picture") %strong= t(".adjust.profile_picture")
%img#profile-picture-cropper{ src: current_user.profile_picture.url(:medium) } %img{ src: current_user.profile_picture.url(:medium), data: { cropper_target: "cropper" } }
.row.mb-2#profile-header-media - %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 }
%div{ data: { controller: "cropper", cropper_aspect_ratio_value: "0.23" } }
.row.mb-2
.col-xs-12.col-md-6 .col-xs-12.col-md-6
%img.mw-100.me-3{ src: current_user.profile_header.url(:mobile) } %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 .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(",") = f.file_field :profile_header, accept: APP_CONFIG[:accepted_image_formats].join(","), data: { cropper_target: "input", action: "cropper#change" }
.row.d-none#profile-header-crop-controls .row.d-none{ data: { cropper_target: "controls" } }
.col-sm-10.col-md-8 .col-sm-10.col-md-8
%strong= t(".adjust.profile_header") %strong= t(".adjust.profile_header")
%img#profile-header-cropper{ src: current_user.profile_header.url(:web) } %img{ src: current_user.profile_header.url(:web), data: { cropper_target: "cropper" } }
= 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| - %i[profile_header_x profile_header_y profile_header_w profile_header_h].each do |attrib|
= f.hidden_field attrib, id: attrib = f.hidden_field attrib, id: attrib, data: { cropper_target: attrib.to_s.split("_").last }
= f.check_box :show_foreign_themes
= f.primary t(".submit_picture") = f.primary t(".submit_picture")
.card .card