Skip to content

Commit

Permalink
Use cropperjs instead of Jcrop
Browse files Browse the repository at this point in the history
JCrop is not maintained anymore and uses jQuery.
Cropperjs is a maintained jQuery-less alternative.
  • Loading branch information
tvdeyen committed Sep 20, 2024
1 parent 8c835bf commit 7233e12
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 @@ -2,6 +2,5 @@
//= link_tree ../builds/alchemy/
//= 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 @@ -40,4 +40,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",
"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",
"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 7233e12

Please sign in to comment.