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 b30080f
Show file tree
Hide file tree
Showing 17 changed files with 113 additions and 84 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
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 b30080f

Please sign in to comment.