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 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 }
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue