Merge pull request #984 from Retrospring/feature/stimulus-cropper
Refactor image cropping into Stimulus controller
This commit is contained in:
commit
2ecfe38e5e
|
@ -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)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue