Skip to content

Commit

Permalink
Merge pull request #3116 from AlchemyCMS/backport/7.4-stable/pr-3047
Browse files Browse the repository at this point in the history
[7.4-stable] Use cropperjs instead of Jcrop
  • Loading branch information
tvdeyen authored Jan 3, 2025
2 parents c960a43 + dfb492e commit 1328217
Show file tree
Hide file tree
Showing 18 changed files with 123 additions and 94 deletions.
10 changes: 9 additions & 1 deletion app/assets/builds/alchemy/admin.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/assets/builds/alchemy/admin.css.map

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion app/assets/config/alchemy_manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
//= link_tree ../builds/tinymce/
//= link_tree ../images/alchemy/
//= link_tree ../../../vendor/assets/fonts/
//= link_tree ../../../vendor/assets/images/
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js
2 changes: 1 addition & 1 deletion app/assets/stylesheets/alchemy/admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@
@import "alchemy/admin/toolbar";
@import "alchemy/admin/typography";
@import "alchemy/admin/upload";
@import "jquery.Jcrop.min";
@import "cropper.min";
2 changes: 0 additions & 2 deletions app/javascript/alchemy_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Dirty from "alchemy_admin/dirty"
import * as FixedElements from "alchemy_admin/fixed_elements"
import { growl } from "alchemy_admin/growler"
import ImageLoader from "alchemy_admin/image_loader"
import ImageCropper from "alchemy_admin/image_cropper"
import Initializer from "alchemy_admin/initializer"
import { LinkDialog } from "alchemy_admin/link_dialog"
import pictureSelector from "alchemy_admin/picture_selector"
Expand Down Expand Up @@ -46,7 +45,6 @@ Object.assign(Alchemy, {
FixedElements,
growl,
ImageLoader: ImageLoader.init,
ImageCropper,
LinkDialog,
pictureSelector,
pleaseWaitOverlay,
Expand Down
97 changes: 57 additions & 40 deletions app/javascript/alchemy_admin/image_cropper.js
Original file line number Diff line number Diff line change
@@ -1,91 +1,108 @@
import Cropper from "cropperjs"

export default class ImageCropper {
#initialized = false
#cropper = null
#cropFromField = null
#cropSizeField = null

constructor(
image,
minSize,
defaultBox,
aspectRatio,
trueSize,
formFieldIds,
elementId
) {
this.initialized = false

this.image = image
this.minSize = minSize
this.defaultBox = defaultBox
this.aspectRatio = aspectRatio
this.trueSize = trueSize
this.cropFromField = document.getElementById(formFieldIds[0])
this.cropSizeField = document.getElementById(formFieldIds[1])
this.#cropFromField = document.getElementById(formFieldIds[0])
this.#cropSizeField = document.getElementById(formFieldIds[1])
this.elementId = elementId
this.dialog = Alchemy.currentDialog()
this.dialog.options.closed = this.destroy

this.dialog.options.closed = () => this.destroy()
this.init()
this.bind()
}

get jcropOptions() {
get cropperOptions() {
return {
onSelect: this.update.bind(this),
setSelect: this.box,
aspectRatio: this.aspectRatio,
minSize: this.minSize,
boxWidth: 800,
boxHeight: 600,
trueSize: this.trueSize,
closed: this.destroy.bind(this)
viewMode: 1,
zoomable: false,
minCropBoxWidth: this.minSize && this.minSize[0],
minCropBoxHeight: this.minSize && this.minSize[1],
ready: (event) => {
const cropper = event.target.cropper
cropper.setData(this.box)
},
cropend: () => {
const data = this.#cropper.getData(true)
this.update(data)
}
}
}

get cropFrom() {
if (this.cropFromField.value) {
return this.cropFromField.value.split("x").map((v) => parseInt(v))
if (this.#cropFromField?.value) {
return this.#cropFromField.value.split("x").map((v) => parseInt(v))
}
}

get cropSize() {
if (this.cropSizeField.value) {
return this.cropSizeField.value.split("x").map((v) => parseInt(v))
if (this.#cropSizeField?.value) {
return this.#cropSizeField.value.split("x").map((v) => parseInt(v))
}
}

get box() {
if (this.cropFrom && this.cropSize) {
return [
this.cropFrom[0],
this.cropFrom[1],
this.cropFrom[0] + this.cropSize[0],
this.cropFrom[1] + this.cropSize[1]
]
return {
x: this.cropFrom[0],
y: this.cropFrom[1],
width: this.cropSize[0],
height: this.cropSize[1]
}
} else {
return this.defaultBox
return this.defaultBoxSize
}
}

get defaultBoxSize() {
return {
x: this.defaultBox[0],
y: this.defaultBox[1],
width: this.defaultBox[2],
height: this.defaultBox[3]
}
}

init() {
if (!this.initialized) {
this.api = $.Jcrop("#imageToCrop", this.jcropOptions)
this.initialized = true
if (!this.#initialized) {
this.#cropper = new Cropper(this.image, this.cropperOptions)
this.#initialized = true
}
}

update(coords) {
this.cropFromField.value = Math.round(coords.x) + "x" + Math.round(coords.y)
this.cropFromField.dispatchEvent(new Event("change"))
this.cropSizeField.value = Math.round(coords.w) + "x" + Math.round(coords.h)
this.cropFromField.dispatchEvent(new Event("change"))
this.#cropFromField.value = `${coords.x}x${coords.y}`
this.#cropFromField.dispatchEvent(new Event("change"))
this.#cropSizeField.value = `${coords.width}x${coords.height}`
this.#cropSizeField.dispatchEvent(new Event("change"))
}

reset() {
this.api.setSelect(this.defaultBox)
this.cropFromField.value = `${this.box[0]}x${this.box[1]}`
this.cropSizeField.value = `${this.box[2]}x${this.box[3] - this.box[1]}`
this.#cropper.setData(this.defaultBoxSize)
this.update(this.defaultBoxSize)
}

destroy() {
if (this.api) {
this.api.destroy()
if (this.#cropper) {
this.#cropper.destroy()
}
this.initialized = false
this.#initialized = false
return true
}

Expand Down
7 changes: 3 additions & 4 deletions app/models/alchemy/image_cropper_settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ def to_h
{
min_size: large_enough? ? min_size : false,
ratio: ratio,
default_box: default_box,
image_size: [image_width, image_height]
default_box: default_box
}.freeze
end

Expand Down Expand Up @@ -79,8 +78,8 @@ def default_box
[
default_crop_from[0],
default_crop_from[1],
default_crop_from[0] + default_crop_size[0],
default_crop_from[1] + default_crop_size[1]
default_crop_size[0],
default_crop_size[1]
]
end
end
Expand Down
35 changes: 19 additions & 16 deletions app/views/alchemy/admin/crop.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<%= simple_format Alchemy.t(:explain_cropping) %>
<% end %>
<div class="thumbnail_background">
<%= image_tag @picture.thumbnail_url(size: '800x600'), id: 'imageToCrop' %>
<%= image_tag @picture.url(flatten: true), id: 'imageToCrop' %>
</div>
<form>
<%= button_tag Alchemy.t(:apply), type: 'submit' %>
Expand All @@ -17,20 +17,23 @@
</div>
<% end %>
<% if @settings %>
<script type="text/javascript">
Alchemy.ImageLoader('#jscropper .thumbnail_background');
$('#imageToCrop').on("load", function() {
new Alchemy.ImageCropper(
<%= @settings[:min_size].to_json %>,
<%= @settings[:default_box].to_json %>,
<%= @settings[:ratio] %>,
<%= @settings[:image_size].to_json %>,
[
"<%= params[:crop_from_form_field_id] %>",
"<%= params[:crop_size_form_field_id] %>",
],
<%= @element.id %>
);
});
<script type="module">
import ImageCropper from "alchemy_admin/image_cropper";
import ImageLoader from "alchemy_admin/image_loader";

const image = document.getElementById("imageToCrop");

new ImageLoader(image);
new ImageCropper(
image,
<%= @settings[:min_size].to_json %>,
<%= @settings[:default_box].to_json %>,
<%= @settings[:ratio] %>,
[
"<%= params[:crop_from_form_field_id] %>",
"<%= params[:crop_size_form_field_id] %>",
],
<%= @element.id %>
);
</script>
<% end %>
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pin "@ungap/custom-elements", to: "ungap-custom-elements.min.js", preload: true # @1.3.0
pin "clipboard", to: "clipboard.min.js", preload: true
pin "cropperjs", to: "cropperjs.min.js", preload: true
pin "flatpickr", to: "flatpickr.min.js", preload: true # @4.6.13
pin "handlebars", to: "handlebars.min.js", preload: true # @4.7.8
pin "keymaster", to: "keymaster.min.js", preload: true
Expand Down
20 changes: 10 additions & 10 deletions lib/alchemy/test_support/having_picture_thumbnails_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,8 @@
context "size 200x50" do
let(:size) { "200x50" }

it "default box should be [0, 25, 200, 75]" do
expect(subject[:default_box]).to eq([0, 25, 200, 75])
it "default box should be [0, 25, 200, 50]" do
expect(subject[:default_box]).to eq([0, 25, 200, 50])
end
end

Expand All @@ -578,16 +578,16 @@
context "size 50x100" do
let(:size) { "50x100" }

it "the hash should be {x1: 75, y1: 0, x2: 125, y2: 100}" do
expect(subject[:default_box]).to eq([75, 0, 125, 100])
it "the hash should be {x1: 75, y1: 0, x2: 50, y2: 100}" do
expect(subject[:default_box]).to eq([75, 0, 50, 100])
end
end

context "size 50x50" do
let(:size) { "50x50" }

it "the hash should be {x1: 50, y1: 0, x2: 150, y2: 100}" do
expect(subject[:default_box]).to eq([50, 0, 150, 100])
it "the hash should be {x1: 50, y1: 0, x2: 100, y2: 100}" do
expect(subject[:default_box]).to eq([50, 0, 100, 100])
end
end

Expand All @@ -602,16 +602,16 @@
context "size 400x100" do
let(:size) { "400x100" }

it "the hash should be {x1: 0, y1: 25, x2: 200, y2: 75}" do
expect(subject[:default_box]).to eq([0, 25, 200, 75])
it "the hash should be {x1: 0, y1: 25, x2: 200, y2: 50}" do
expect(subject[:default_box]).to eq([0, 25, 200, 50])
end
end

context "size 200x200" do
let(:size) { "200x200" }

it "the hash should be {x1: 50, y1: 0, x2: 150, y2: 100}" do
expect(subject[:default_box]).to eq([50, 0, 150, 100])
it "the hash should be {x1: 50, y1: 0, x2: 100, y2: 100}" do
expect(subject[:default_box]).to eq([50, 0, 100, 100])
end
end
end
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"lint": "prettier --check 'app/javascript/**/*.js'",
"eslint": "eslint app/javascript/**/*.js",
"build:js": "rollup -c",
"build:css": "sass --style=compressed --source-map --load-path app/assets/stylesheets --load-path vendor/assets/stylesheets app/assets/stylesheets/alchemy/admin.scss:app/assets/builds/alchemy/admin.css app/assets/stylesheets/alchemy/admin/print.scss:app/assets/builds/alchemy/admin/print.css app/assets/stylesheets/alchemy/welcome.scss:app/assets/builds/alchemy/welcome.css app/assets/stylesheets/tinymce/skins/content/alchemy/content.scss:app/assets/builds/tinymce/skins/content/alchemy/content.min.css app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.scss:app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css app/assets/stylesheets/alchemy/admin/page-select.scss:app/assets/builds/alchemy/admin/page-select.css app/assets/stylesheets/alchemy/custom-properties.css:app/assets/builds/alchemy/custom-properties.css",
"build:css": "sass --style=compressed --source-map --load-path app/assets/stylesheets --load-path vendor/assets/stylesheets --load-path node_modules/cropperjs/dist app/assets/stylesheets/alchemy/admin.scss:app/assets/builds/alchemy/admin.css app/assets/stylesheets/alchemy/admin/print.scss:app/assets/builds/alchemy/admin/print.css app/assets/stylesheets/alchemy/welcome.scss:app/assets/builds/alchemy/welcome.css app/assets/stylesheets/tinymce/skins/content/alchemy/content.scss:app/assets/builds/tinymce/skins/content/alchemy/content.min.css app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.scss:app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css app/assets/stylesheets/alchemy/admin/page-select.scss:app/assets/builds/alchemy/admin/page-select.css app/assets/stylesheets/alchemy/custom-properties.css:app/assets/builds/alchemy/custom-properties.css",
"handlebars:compile": "handlebars app/javascript/alchemy_admin/templates/*.hbs -f app/javascript/alchemy_admin/templates/compiled.js -o -m",
"build": "bun run --bun build:js && bun run --bun build:css && bun run --bun handlebars:compile"
},
Expand All @@ -17,6 +17,7 @@
"@shoelace-style/shoelace": "^2.16.0",
"@ungap/custom-elements": "^1.3.0",
"clipboard": "^2.0.11",
"cropperjs": "^1.6.2",
"flatpickr": "^4.6.13",
"handlebars": "^4.7.8",
"keymaster": "^1.6.2",
Expand Down
8 changes: 8 additions & 0 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export default [
},
context: "window"
},
{
input: "node_modules/cropperjs/dist/cropper.esm.js",
output: {
file: "vendor/javascript/cropperjs.min.js"
},
plugins: [terser()],
context: "window"
},
{
input: "node_modules/flatpickr/dist/esm/index.js",
output: {
Expand Down
10 changes: 2 additions & 8 deletions spec/models/alchemy/image_cropper_settings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,14 @@
end

it "should return an Array where all values are Integer" do
expect(default_box.all? { |v| v.is_a? Integer }).to be_truthy
expect(default_box.all?(Integer)).to be_truthy
end

context "with crop from and crop size given" do
let(:crop_from) { [0, 25] }
let(:crop_size) { [50, 50] }

it { is_expected.to eq([0, 25, 50, 75]) }
it { is_expected.to eq([0, 25, 50, 50]) }
end
end

Expand Down Expand Up @@ -142,12 +142,6 @@
end
end
end

describe ":image_size" do
it "is an Array of image width and height" do
expect(subject[:image_size]).to eq([300, 250])
end
end
end
end
end
Binary file removed vendor/assets/images/Jcrop.gif
Binary file not shown.
7 changes: 0 additions & 7 deletions vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js

This file was deleted.

2 changes: 0 additions & 2 deletions vendor/assets/stylesheets/jquery.Jcrop.min.css

This file was deleted.

10 changes: 10 additions & 0 deletions vendor/javascript/cropperjs.min.js

Large diffs are not rendered by default.

0 comments on commit 1328217

Please sign in to comment.