alchemy_cms 5.2.0 → 6.0.0.b3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -14
- data/.gitignore +0 -1
- data/.hound.yml +1 -1
- data/.rubocop.yml +46 -4
- data/CHANGELOG.md +114 -5
- data/Gemfile +8 -1
- data/README.md +5 -2
- data/alchemy_cms.gemspec +78 -65
- data/app/assets/javascripts/alchemy/admin.js +0 -2
- data/app/assets/javascripts/alchemy/alchemy.base.js.coffee +0 -27
- data/app/assets/javascripts/alchemy/alchemy.confirm_dialog.js.coffee +2 -1
- data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +1 -1
- data/app/assets/javascripts/alchemy/alchemy.dragndrop.js.coffee +0 -25
- data/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee +1 -1
- data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +2 -0
- data/app/assets/javascripts/alchemy/alchemy.fixed_elements.js +1 -1
- data/app/assets/javascripts/alchemy/alchemy.gui.js.coffee +3 -1
- data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +1 -1
- data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +40 -27
- data/app/assets/javascripts/alchemy/templates/node_folder.hbs +1 -1
- data/app/assets/stylesheets/alchemy/_extends.scss +15 -2
- data/app/assets/stylesheets/alchemy/admin.scss +1 -1
- data/app/assets/stylesheets/alchemy/archive.scss +20 -5
- data/app/assets/stylesheets/alchemy/buttons.scss +0 -4
- data/app/assets/stylesheets/alchemy/elements.scss +73 -61
- data/app/assets/stylesheets/alchemy/images.scss +8 -0
- data/app/assets/stylesheets/alchemy/node-select.scss +4 -3
- data/app/assets/stylesheets/alchemy/page-select.scss +1 -0
- data/app/controllers/alchemy/admin/attachments_controller.rb +8 -4
- data/app/controllers/alchemy/admin/base_controller.rb +5 -7
- data/app/controllers/alchemy/admin/elements_controller.rb +59 -34
- data/app/controllers/alchemy/admin/essence_audios_controller.rb +30 -0
- data/app/controllers/alchemy/admin/essence_files_controller.rb +0 -14
- data/app/controllers/alchemy/admin/essence_pictures_controller.rb +8 -79
- data/app/controllers/alchemy/admin/essence_videos_controller.rb +33 -0
- data/app/controllers/alchemy/admin/ingredients_controller.rb +30 -0
- data/app/controllers/alchemy/admin/layoutpages_controller.rb +0 -1
- data/app/controllers/alchemy/admin/pages_controller.rb +7 -22
- data/app/controllers/alchemy/admin/pictures_controller.rb +56 -17
- data/app/controllers/alchemy/admin/resources_controller.rb +84 -10
- data/app/controllers/alchemy/api/elements_controller.rb +13 -4
- data/app/controllers/alchemy/api/pages_controller.rb +4 -3
- data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +13 -3
- data/app/controllers/concerns/alchemy/admin/crop_action.rb +26 -0
- data/app/decorators/alchemy/element_editor.rb +26 -1
- data/app/decorators/alchemy/ingredient_editor.rb +158 -0
- data/app/helpers/alchemy/admin/elements_helper.rb +1 -0
- data/app/helpers/alchemy/admin/essences_helper.rb +1 -1
- data/app/helpers/alchemy/admin/ingredients_helper.rb +42 -0
- data/app/helpers/alchemy/elements_block_helper.rb +23 -6
- data/app/helpers/alchemy/elements_helper.rb +12 -5
- data/app/helpers/alchemy/pages_helper.rb +3 -11
- data/app/jobs/alchemy/base_job.rb +11 -0
- data/app/jobs/alchemy/publish_page_job.rb +11 -0
- data/app/models/alchemy/attachment.rb +24 -7
- data/app/models/alchemy/content.rb +1 -6
- data/app/models/alchemy/content/factory.rb +23 -27
- data/app/models/alchemy/element.rb +39 -72
- data/app/models/alchemy/element/definitions.rb +29 -27
- data/app/models/alchemy/element/element_contents.rb +131 -122
- data/app/models/alchemy/element/element_essences.rb +111 -98
- data/app/models/alchemy/element/element_ingredients.rb +184 -0
- data/app/models/alchemy/element/presenters.rb +104 -85
- data/app/models/alchemy/elements_repository.rb +126 -0
- data/app/models/alchemy/essence_audio.rb +12 -0
- data/app/models/alchemy/essence_headline.rb +40 -0
- data/app/models/alchemy/essence_picture.rb +4 -116
- data/app/models/alchemy/essence_richtext.rb +12 -0
- data/app/models/alchemy/essence_video.rb +12 -0
- data/app/models/alchemy/image_cropper_settings.rb +87 -0
- data/app/models/alchemy/ingredient.rb +183 -0
- data/app/models/alchemy/ingredient_validator.rb +97 -0
- data/app/models/alchemy/ingredients/audio.rb +29 -0
- data/app/models/alchemy/ingredients/boolean.rb +21 -0
- data/app/models/alchemy/ingredients/datetime.rb +20 -0
- data/app/models/alchemy/ingredients/file.rb +30 -0
- data/app/models/alchemy/ingredients/headline.rb +42 -0
- data/app/models/alchemy/ingredients/html.rb +19 -0
- data/app/models/alchemy/ingredients/link.rb +16 -0
- data/app/models/alchemy/ingredients/node.rb +23 -0
- data/app/models/alchemy/ingredients/page.rb +23 -0
- data/app/models/alchemy/ingredients/picture.rb +41 -0
- data/app/models/alchemy/ingredients/richtext.rb +57 -0
- data/app/models/alchemy/ingredients/select.rb +10 -0
- data/app/models/alchemy/ingredients/text.rb +17 -0
- data/app/models/alchemy/ingredients/video.rb +33 -0
- data/app/models/alchemy/language.rb +0 -11
- data/app/models/alchemy/page.rb +76 -33
- data/app/models/alchemy/page/fixed_attributes.rb +53 -51
- data/app/models/alchemy/page/page_elements.rb +186 -205
- data/app/models/alchemy/page/page_naming.rb +66 -64
- data/app/models/alchemy/page/page_natures.rb +139 -142
- data/app/models/alchemy/page/page_scopes.rb +117 -102
- data/app/models/alchemy/page/publisher.rb +50 -0
- data/app/models/alchemy/page/url_path.rb +1 -1
- data/app/models/alchemy/page_version.rb +58 -0
- data/app/models/alchemy/picture.rb +18 -40
- data/app/models/alchemy/picture/calculations.rb +2 -8
- data/app/models/alchemy/picture/preprocessor.rb +2 -0
- data/app/models/alchemy/picture/transformations.rb +24 -96
- data/app/models/concerns/alchemy/picture_thumbnails.rb +181 -0
- data/app/models/concerns/alchemy/touch_elements.rb +2 -2
- data/app/presenters/alchemy/picture_view.rb +88 -0
- data/app/serializers/alchemy/element_serializer.rb +5 -0
- data/app/serializers/alchemy/page_tree_serializer.rb +3 -2
- data/app/services/alchemy/delete_elements.rb +44 -0
- data/app/services/alchemy/duplicate_element.rb +56 -0
- data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +2 -3
- data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
- data/app/views/alchemy/admin/attachments/assign.js.erb +11 -0
- data/app/views/alchemy/admin/attachments/index.html.erb +2 -3
- data/app/views/alchemy/admin/crop.html.erb +36 -0
- data/app/views/alchemy/admin/elements/_element.html.erb +14 -10
- data/app/views/alchemy/admin/elements/{_element_footer.html.erb → _footer.html.erb} +0 -0
- data/app/views/alchemy/admin/elements/{_new_element_form.html.erb → _form.html.erb} +1 -1
- data/app/views/alchemy/admin/elements/{_element_header.html.erb → _header.html.erb} +1 -1
- data/app/views/alchemy/admin/elements/{_element_toolbar.html.erb → _toolbar.html.erb} +5 -6
- data/app/views/alchemy/admin/elements/create.js.erb +1 -1
- data/app/views/alchemy/admin/elements/{trash.js.erb → destroy.js.erb} +2 -6
- data/app/views/alchemy/admin/elements/fold.js.erb +2 -2
- data/app/views/alchemy/admin/elements/new.html.erb +3 -3
- data/app/views/alchemy/admin/elements/order.js.erb +0 -17
- data/app/views/alchemy/admin/elements/update.js.erb +3 -2
- data/app/views/alchemy/admin/essence_audios/edit.html.erb +7 -0
- data/app/views/alchemy/admin/essence_pictures/update.js.erb +0 -1
- data/app/views/alchemy/admin/essence_videos/edit.html.erb +11 -0
- data/app/views/alchemy/admin/ingredients/_audio_fields.html.erb +4 -0
- data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +18 -0
- data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +25 -0
- data/app/views/alchemy/admin/ingredients/_video_fields.html.erb +8 -0
- data/app/views/alchemy/admin/ingredients/edit.html.erb +4 -0
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +0 -5
- data/app/views/alchemy/admin/nodes/_node.html.erb +2 -2
- data/app/views/alchemy/admin/pages/_anchor_link.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_external_link.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_file_link.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_form.html.erb +0 -6
- data/app/views/alchemy/admin/pages/_internal_link.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb +5 -2
- data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -1
- data/app/views/alchemy/admin/pages/edit.html.erb +36 -24
- data/app/views/alchemy/admin/pages/index.html.erb +2 -9
- data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +2 -4
- data/app/views/alchemy/admin/partials/_routes.html.erb +7 -11
- data/app/views/alchemy/admin/partials/_search_form.html.erb +9 -0
- data/app/views/alchemy/admin/pictures/_archive.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -7
- data/app/views/alchemy/admin/pictures/_infos.html.erb +0 -1
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +4 -4
- data/app/views/alchemy/admin/pictures/assign.js.erb +10 -0
- data/app/views/alchemy/admin/pictures/index.html.erb +8 -3
- data/app/views/alchemy/admin/resources/_filter.html.erb +12 -0
- data/app/views/alchemy/admin/resources/_filter_bar.html.erb +14 -17
- data/app/views/alchemy/admin/resources/_form.html.erb +3 -0
- data/app/views/alchemy/admin/resources/_table_header.html.erb +15 -0
- data/app/views/alchemy/admin/resources/index.html.erb +3 -11
- data/app/views/alchemy/essences/_essence_audio_editor.html.erb +4 -0
- data/app/views/alchemy/essences/_essence_audio_view.html.erb +15 -0
- data/app/views/alchemy/essences/_essence_file_editor.html.erb +15 -6
- data/app/views/alchemy/essences/_essence_headline_editor.html.erb +36 -0
- data/app/views/alchemy/essences/_essence_headline_view.html.erb +10 -0
- data/app/views/alchemy/essences/_essence_link_editor.html.erb +8 -4
- data/app/views/alchemy/essences/_essence_picture_editor.html.erb +27 -12
- data/app/views/alchemy/essences/_essence_picture_view.html.erb +3 -3
- data/app/views/alchemy/essences/_essence_text_editor.html.erb +12 -4
- data/app/views/alchemy/essences/_essence_video_editor.html.erb +4 -0
- data/app/views/alchemy/essences/_essence_video_view.html.erb +18 -0
- data/app/views/alchemy/essences/shared/_essence_picture_tools.html.erb +21 -16
- data/app/views/alchemy/essences/shared/_linkable_essence_tools.html.erb +2 -2
- data/app/views/alchemy/ingredients/_audio_editor.html.erb +5 -0
- data/app/views/alchemy/ingredients/_audio_view.html.erb +14 -0
- data/app/views/alchemy/ingredients/_boolean_editor.html.erb +11 -0
- data/app/views/alchemy/ingredients/_boolean_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_datetime_editor.html.erb +17 -0
- data/app/views/alchemy/ingredients/_datetime_view.html.erb +9 -0
- data/app/views/alchemy/ingredients/_file_editor.html.erb +52 -0
- data/app/views/alchemy/ingredients/_file_view.html.erb +17 -0
- data/app/views/alchemy/ingredients/_headline_editor.html.erb +30 -0
- data/app/views/alchemy/ingredients/_headline_view.html.erb +9 -0
- data/app/views/alchemy/ingredients/_html_editor.html.erb +8 -0
- data/app/views/alchemy/ingredients/_html_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_link_editor.html.erb +24 -0
- data/app/views/alchemy/ingredients/_link_view.html.erb +9 -0
- data/app/views/alchemy/ingredients/_node_editor.html.erb +26 -0
- data/app/views/alchemy/ingredients/_node_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_page_editor.html.erb +25 -0
- data/app/views/alchemy/ingredients/_page_view.html.erb +4 -0
- data/app/views/alchemy/ingredients/_picture_editor.html.erb +60 -0
- data/app/views/alchemy/ingredients/_picture_view.html.erb +5 -0
- data/app/views/alchemy/ingredients/_richtext_editor.html.erb +12 -0
- data/app/views/alchemy/ingredients/_richtext_view.html.erb +3 -0
- data/app/views/alchemy/ingredients/_select_editor.html.erb +30 -0
- data/app/views/alchemy/ingredients/_select_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_text_editor.html.erb +20 -0
- data/app/views/alchemy/ingredients/_text_view.html.erb +16 -0
- data/app/views/alchemy/ingredients/_video_editor.html.erb +5 -0
- data/app/views/alchemy/ingredients/_video_view.html.erb +17 -0
- data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +20 -0
- data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +57 -0
- data/config/brakeman.ignore +66 -159
- data/config/initializers/dragonfly.rb +10 -0
- data/config/locales/alchemy.en.yml +108 -64
- data/config/routes.rb +17 -22
- data/db/migrate/20201207131309_create_page_versions.rb +19 -0
- data/db/migrate/20201207135820_add_page_version_id_to_alchemy_elements.rb +76 -0
- data/db/migrate/20210205143548_rename_public_on_and_public_until_on_alchemy_pages.rb +10 -0
- data/db/migrate/20210326105046_add_sanitized_body_to_alchemy_essence_richtexts.rb +7 -0
- data/db/migrate/20210406093436_add_alchemy_essence_headlines.rb +12 -0
- data/db/migrate/20210506135919_create_essence_audios.rb +19 -0
- data/db/migrate/20210506140258_create_essence_videos.rb +23 -0
- data/db/migrate/20210508091432_create_alchemy_ingredients.rb +22 -0
- data/lib/alchemy/admin/preview_url.rb +2 -0
- data/lib/alchemy/deprecation.rb +1 -1
- data/lib/alchemy/dragonfly/processors/auto_orient.rb +18 -0
- data/lib/alchemy/dragonfly/processors/crop_resize.rb +35 -0
- data/lib/alchemy/elements_finder.rb +14 -60
- data/lib/alchemy/essence.rb +1 -2
- data/lib/alchemy/forms/builder.rb +21 -1
- data/lib/alchemy/hints.rb +8 -4
- data/lib/alchemy/page_layout.rb +0 -13
- data/lib/alchemy/permissions.rb +30 -29
- data/lib/alchemy/resource.rb +13 -3
- data/lib/alchemy/resource_filter.rb +40 -0
- data/lib/alchemy/resources_helper.rb +1 -16
- data/lib/alchemy/tasks/tidy.rb +29 -0
- data/lib/alchemy/test_support.rb +2 -11
- data/lib/alchemy/test_support/essence_shared_examples.rb +0 -1
- data/lib/alchemy/test_support/factories/element_factory.rb +8 -8
- data/lib/alchemy/test_support/factories/essence_audio_factory.rb +7 -0
- data/lib/alchemy/test_support/factories/essence_video_factory.rb +7 -0
- data/lib/alchemy/test_support/factories/ingredient_factory.rb +25 -0
- data/lib/alchemy/test_support/factories/page_factory.rb +20 -1
- data/lib/alchemy/test_support/factories/page_version_factory.rb +23 -0
- data/lib/alchemy/test_support/having_crop_action_examples.rb +170 -0
- data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +646 -0
- data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +21 -0
- data/lib/alchemy/test_support/shared_ingredient_examples.rb +75 -0
- data/lib/alchemy/tinymce.rb +17 -0
- data/lib/alchemy/upgrader/six_point_zero.rb +21 -0
- data/lib/alchemy/upgrader/tasks/add_page_versions.rb +33 -0
- data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +62 -0
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy_cms.rb +1 -0
- data/lib/generators/alchemy/elements/elements_generator.rb +1 -0
- data/lib/generators/alchemy/elements/templates/view.html.erb +9 -0
- data/lib/generators/alchemy/elements/templates/view.html.haml +9 -0
- data/lib/generators/alchemy/elements/templates/view.html.slim +9 -0
- data/lib/generators/alchemy/ingredient/ingredient_generator.rb +38 -0
- data/lib/generators/alchemy/ingredient/templates/editor.html.erb +14 -0
- data/lib/generators/alchemy/ingredient/templates/model.rb.tt +13 -0
- data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -0
- data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +1 -1
- data/lib/generators/alchemy/menus/templates/node.html.erb +1 -1
- data/lib/generators/alchemy/menus/templates/node.html.haml +1 -1
- data/lib/generators/alchemy/menus/templates/node.html.slim +1 -1
- data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
- data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
- data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
- data/lib/tasks/alchemy/thumbnails.rake +4 -2
- data/lib/tasks/alchemy/tidy.rake +12 -0
- data/lib/tasks/alchemy/upgrade.rake +26 -0
- data/package.json +3 -2
- data/package/admin.js +11 -1
- data/package/src/__tests__/i18n.spec.js +23 -0
- data/package/src/file_editors.js +28 -0
- data/package/src/i18n.js +1 -3
- data/package/src/image_cropper.js +103 -0
- data/package/src/image_loader.js +58 -0
- data/package/src/node_tree.js +5 -5
- data/package/src/picture_editors.js +169 -0
- data/package/src/utils/__tests__/ajax.spec.js +20 -12
- data/package/src/utils/ajax.js +8 -3
- data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +3 -18
- data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +2 -28
- metadata +292 -55
- data/app/assets/javascripts/alchemy/alchemy.image_cropper.js.coffee +0 -44
- data/app/assets/javascripts/alchemy/alchemy.trash_window.js.coffee +0 -30
- data/app/assets/stylesheets/alchemy/trash.scss +0 -8
- data/app/controllers/alchemy/admin/trash_controller.rb +0 -44
- data/app/views/alchemy/admin/attachments/_filter_bar.html.erb +0 -29
- data/app/views/alchemy/admin/essence_files/assign.js.erb +0 -3
- data/app/views/alchemy/admin/essence_pictures/assign.js.erb +0 -4
- data/app/views/alchemy/admin/essence_pictures/crop.html.erb +0 -48
- data/app/views/alchemy/admin/pictures/_filter_bar.html.erb +0 -30
- data/app/views/alchemy/admin/trash/clear.js.erb +0 -4
- data/app/views/alchemy/admin/trash/index.html.erb +0 -31
- data/lib/alchemy/test_support/factories.rb +0 -16
|
@@ -16,6 +16,8 @@ module Alchemy
|
|
|
16
16
|
def call
|
|
17
17
|
max_image_size = Alchemy::Config.get(:preprocess_image_resize)
|
|
18
18
|
image_file.thumb!(max_image_size) if max_image_size.present?
|
|
19
|
+
# Auto orient the image so EXIF orientation data is taken into account
|
|
20
|
+
image_file.auto_orient!
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
private
|
|
@@ -11,53 +11,16 @@ module Alchemy
|
|
|
11
11
|
include Alchemy::Picture::Calculations
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
THUMBNAIL_WIDTH = 160
|
|
15
|
-
THUMBNAIL_HEIGHT = 120
|
|
16
|
-
|
|
17
|
-
# Returns the default centered image mask for a given size.
|
|
18
|
-
# If the mask is bigger than the image, the mask is scaled down
|
|
19
|
-
# so the largest possible part of the image is visible.
|
|
20
|
-
#
|
|
21
|
-
def default_mask(mask_arg)
|
|
22
|
-
mask = mask_arg.dup
|
|
23
|
-
mask[:width] = image_file_width if mask[:width].zero?
|
|
24
|
-
mask[:height] = image_file_height if mask[:height].zero?
|
|
25
|
-
|
|
26
|
-
crop_size = size_when_fitting({width: image_file_width, height: image_file_height}, mask)
|
|
27
|
-
top_left = get_top_left_crop_corner(crop_size)
|
|
28
|
-
|
|
29
|
-
point_and_mask_to_points(top_left, crop_size)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Returns a size value String for the thumbnail used in essence picture editors.
|
|
33
|
-
#
|
|
34
|
-
def thumbnail_size(size_string = "0x0", crop = false)
|
|
35
|
-
size = sizes_from_string(size_string)
|
|
36
|
-
|
|
37
|
-
# only if crop is set do we need to actually parse the size string, otherwise
|
|
38
|
-
# we take the base image size.
|
|
39
|
-
if crop
|
|
40
|
-
size[:width] = get_base_dimensions[:width] if size[:width].zero?
|
|
41
|
-
size[:height] = get_base_dimensions[:height] if size[:height].zero?
|
|
42
|
-
size = reduce_to_image(size)
|
|
43
|
-
else
|
|
44
|
-
size = get_base_dimensions
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
size = size_when_fitting({width: THUMBNAIL_WIDTH, height: THUMBNAIL_HEIGHT}, size)
|
|
48
|
-
"#{size[:width]}x#{size[:height]}"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
14
|
# Returns the rendered cropped image. Tries to use the crop_from and crop_size
|
|
52
15
|
# parameters. When they can't be parsed, it just crops from the center.
|
|
53
16
|
#
|
|
54
17
|
def crop(size, crop_from = nil, crop_size = nil, upsample = false)
|
|
55
18
|
raise "No size given!" if size.empty?
|
|
56
19
|
|
|
57
|
-
render_to =
|
|
20
|
+
render_to = inferred_sizes_from_string(size)
|
|
58
21
|
if crop_from && crop_size
|
|
59
22
|
top_left = point_from_string(crop_from)
|
|
60
|
-
crop_dimensions =
|
|
23
|
+
crop_dimensions = inferred_sizes_from_string(crop_size)
|
|
61
24
|
xy_crop_resize(render_to, top_left, crop_dimensions, upsample)
|
|
62
25
|
else
|
|
63
26
|
center_crop(render_to, upsample)
|
|
@@ -75,21 +38,30 @@ module Alchemy
|
|
|
75
38
|
def landscape_format?
|
|
76
39
|
image_file.landscape?
|
|
77
40
|
end
|
|
41
|
+
|
|
78
42
|
alias_method :landscape?, :landscape_format?
|
|
43
|
+
deprecate landscape_format?: "Use image_file.landscape? instead", deprecator: Alchemy::Deprecation
|
|
44
|
+
deprecate landscape?: "Use image_file.landscape? instead", deprecator: Alchemy::Deprecation
|
|
79
45
|
|
|
80
46
|
# Returns true if picture's width is smaller than it's height
|
|
81
47
|
#
|
|
82
48
|
def portrait_format?
|
|
83
49
|
image_file.portrait?
|
|
84
50
|
end
|
|
51
|
+
|
|
85
52
|
alias_method :portrait?, :portrait_format?
|
|
53
|
+
deprecate portrait_format?: "Use image_file.portrait? instead", deprecator: Alchemy::Deprecation
|
|
54
|
+
deprecate portrait?: "Use image_file.portrait? instead", deprecator: Alchemy::Deprecation
|
|
86
55
|
|
|
87
56
|
# Returns true if picture's width and height is equal
|
|
88
57
|
#
|
|
89
58
|
def square_format?
|
|
90
59
|
image_file.aspect_ratio == 1.0
|
|
91
60
|
end
|
|
61
|
+
|
|
92
62
|
alias_method :square?, :square_format?
|
|
63
|
+
deprecate square_format?: "Use image_file.aspect_ratio instead", deprecator: Alchemy::Deprecation
|
|
64
|
+
deprecate square?: "Use image_file.aspect_ratio instead", deprecator: Alchemy::Deprecation
|
|
93
65
|
|
|
94
66
|
# Returns true if the class we're included in has a meaningful render_size attribute
|
|
95
67
|
#
|
|
@@ -121,62 +93,18 @@ module Alchemy
|
|
|
121
93
|
}
|
|
122
94
|
end
|
|
123
95
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
#
|
|
128
|
-
def get_top_left_crop_corner(dimensions)
|
|
129
|
-
{
|
|
130
|
-
x: (image_file_width - dimensions[:width]) / 2,
|
|
131
|
-
y: (image_file_height - dimensions[:height]) / 2,
|
|
132
|
-
}
|
|
133
|
-
end
|
|
96
|
+
def inferred_sizes_from_string(string)
|
|
97
|
+
sizes = sizes_from_string(string)
|
|
98
|
+
ratio = image_file_width.to_f / image_file_height
|
|
134
99
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# This is the order of precedence: crop_size > image_size
|
|
138
|
-
def get_base_dimensions
|
|
139
|
-
if crop_size?
|
|
140
|
-
sizes_from_string(crop_size)
|
|
141
|
-
else
|
|
142
|
-
image_size
|
|
100
|
+
if sizes[:width].zero?
|
|
101
|
+
sizes[:width] = image_file_width * ratio
|
|
143
102
|
end
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# This function takes a target and a base dimensions hash and returns
|
|
147
|
-
# the dimensions of the image when the base dimensions hash fills
|
|
148
|
-
# the target.
|
|
149
|
-
#
|
|
150
|
-
# Aspect ratio will be preserved.
|
|
151
|
-
#
|
|
152
|
-
def size_when_fitting(target, dimensions = get_base_dimensions)
|
|
153
|
-
zoom = [
|
|
154
|
-
dimensions[:width].to_f / target[:width],
|
|
155
|
-
dimensions[:height].to_f / target[:height],
|
|
156
|
-
].max
|
|
157
|
-
|
|
158
|
-
if zoom == 0.0
|
|
159
|
-
width = target[:width]
|
|
160
|
-
height = target[:height]
|
|
161
|
-
else
|
|
162
|
-
width = (dimensions[:width] / zoom).round
|
|
163
|
-
height = (dimensions[:height] / zoom).round
|
|
103
|
+
if sizes[:height].zero?
|
|
104
|
+
sizes[:height] = image_file_width / ratio
|
|
164
105
|
end
|
|
165
106
|
|
|
166
|
-
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# Given a point as a Hash with :x and :y, and a mask with
|
|
170
|
-
# :width and :height, this function returns the area on the
|
|
171
|
-
# underlying canvas as a Hash of two points
|
|
172
|
-
#
|
|
173
|
-
def point_and_mask_to_points(point, mask)
|
|
174
|
-
{
|
|
175
|
-
x1: point[:x],
|
|
176
|
-
y1: point[:y],
|
|
177
|
-
x2: point[:x] + mask[:width],
|
|
178
|
-
y2: point[:y] + mask[:height],
|
|
179
|
-
}
|
|
107
|
+
sizes
|
|
180
108
|
end
|
|
181
109
|
|
|
182
110
|
# Converts a dimensions hash to a string of from "20x20"
|
|
@@ -197,20 +125,20 @@ module Alchemy
|
|
|
197
125
|
# Use imagemagick to custom crop an image. Uses -thumbnail for better performance when resizing.
|
|
198
126
|
#
|
|
199
127
|
def xy_crop_resize(dimensions, top_left, crop_dimensions, upsample)
|
|
200
|
-
crop_argument =
|
|
128
|
+
crop_argument = dimensions_to_string(crop_dimensions)
|
|
201
129
|
crop_argument += "+#{top_left[:x]}+#{top_left[:y]}"
|
|
202
130
|
|
|
203
|
-
resize_argument =
|
|
131
|
+
resize_argument = dimensions_to_string(dimensions)
|
|
204
132
|
resize_argument += ">" unless upsample
|
|
205
|
-
image_file.
|
|
133
|
+
image_file.crop_resize(crop_argument, resize_argument)
|
|
206
134
|
end
|
|
207
135
|
|
|
208
136
|
# Used when centercropping.
|
|
209
137
|
#
|
|
210
138
|
def reduce_to_image(dimensions)
|
|
211
139
|
{
|
|
212
|
-
width: [dimensions[:width], image_file_width].min,
|
|
213
|
-
height: [dimensions[:height], image_file_height].min,
|
|
140
|
+
width: [dimensions[:width].to_i, image_file_width.to_i].min,
|
|
141
|
+
height: [dimensions[:height].to_i, image_file_height.to_i].min,
|
|
214
142
|
}
|
|
215
143
|
end
|
|
216
144
|
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Picture thumbnails and cropping concerns
|
|
5
|
+
module PictureThumbnails
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
before_save :fix_crop_values
|
|
10
|
+
|
|
11
|
+
delegate :image_file_width, :image_file_height, :image_file, to: :picture, allow_nil: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The url to show the picture.
|
|
15
|
+
#
|
|
16
|
+
# Takes all values like +name+ and crop sizes (+crop_from+, +crop_size+ from the build in graphical image cropper)
|
|
17
|
+
# and also adds the security token.
|
|
18
|
+
#
|
|
19
|
+
# You typically want to set the size the picture should be resized to.
|
|
20
|
+
#
|
|
21
|
+
# === Example:
|
|
22
|
+
#
|
|
23
|
+
# essence_picture.picture_url(size: '200x300', crop: true, format: 'gif')
|
|
24
|
+
# # '/pictures/1/show/200x300/crop/cats.gif?sh=765rfghj'
|
|
25
|
+
#
|
|
26
|
+
# @option options size [String]
|
|
27
|
+
# The size the picture should be resized to.
|
|
28
|
+
#
|
|
29
|
+
# @option options format [String]
|
|
30
|
+
# The format the picture should be rendered in.
|
|
31
|
+
# Defaults to the +image_output_format+ from the +Alchemy::Config+.
|
|
32
|
+
#
|
|
33
|
+
# @option options crop [Boolean]
|
|
34
|
+
# If set to true the picture will be cropped to fit the size value.
|
|
35
|
+
#
|
|
36
|
+
# @return [String]
|
|
37
|
+
def picture_url(options = {})
|
|
38
|
+
return if picture.nil?
|
|
39
|
+
|
|
40
|
+
picture.url(picture_url_options.merge(options)) || "missing-image.png"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Picture rendering options
|
|
44
|
+
#
|
|
45
|
+
# Returns the +default_render_format+ of the associated +Alchemy::Picture+
|
|
46
|
+
# together with the +crop_from+ and +crop_size+ values
|
|
47
|
+
#
|
|
48
|
+
# @return [HashWithIndifferentAccess]
|
|
49
|
+
def picture_url_options
|
|
50
|
+
return {} if picture.nil?
|
|
51
|
+
|
|
52
|
+
crop = !!settings[:crop]
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
format: picture.default_render_format,
|
|
56
|
+
crop: crop,
|
|
57
|
+
crop_from: crop && crop_from.presence || nil,
|
|
58
|
+
crop_size: crop && crop_size.presence || nil,
|
|
59
|
+
size: settings[:size],
|
|
60
|
+
}.with_indifferent_access
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns an url for the thumbnail representation of the assigned picture
|
|
64
|
+
#
|
|
65
|
+
# It takes cropping values into account, so it always represents the current
|
|
66
|
+
# image displayed in the frontend.
|
|
67
|
+
#
|
|
68
|
+
# @return [String]
|
|
69
|
+
def thumbnail_url
|
|
70
|
+
return if picture.nil?
|
|
71
|
+
|
|
72
|
+
picture.url(thumbnail_url_options) || "alchemy/missing-image.svg"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Thumbnail rendering options
|
|
76
|
+
#
|
|
77
|
+
# @return [HashWithIndifferentAccess]
|
|
78
|
+
def thumbnail_url_options
|
|
79
|
+
crop = !!settings[:crop]
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
size: "160x120",
|
|
83
|
+
crop: crop,
|
|
84
|
+
crop_from: crop && crop_from.presence || default_crop_from&.join("x"),
|
|
85
|
+
crop_size: crop && crop_size.presence || default_crop_size&.join("x"),
|
|
86
|
+
flatten: true,
|
|
87
|
+
format: picture&.image_file_format || "jpg",
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Settings for the graphical JS image cropper
|
|
92
|
+
def image_cropper_settings
|
|
93
|
+
Alchemy::ImageCropperSettings.new(
|
|
94
|
+
render_size: dimensions_from_string(render_size.presence || settings[:size]),
|
|
95
|
+
default_crop_from: default_crop_from,
|
|
96
|
+
default_crop_size: default_crop_size,
|
|
97
|
+
fixed_ratio: settings[:fixed_ratio],
|
|
98
|
+
image_width: picture&.image_file_width,
|
|
99
|
+
image_height: picture&.image_file_height,
|
|
100
|
+
).to_h
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Show image cropping link for content
|
|
104
|
+
def allow_image_cropping?
|
|
105
|
+
settings[:crop] && picture &&
|
|
106
|
+
picture.can_be_cropped_to?(
|
|
107
|
+
settings[:size],
|
|
108
|
+
settings[:upsample],
|
|
109
|
+
) && !!picture.image_file
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def default_crop_size
|
|
115
|
+
return nil unless settings[:crop] && settings[:size]
|
|
116
|
+
|
|
117
|
+
mask = inferred_dimensions_from_string(settings[:size])
|
|
118
|
+
zoom = thumbnail_zoom_factor(mask)
|
|
119
|
+
return nil if zoom.zero?
|
|
120
|
+
|
|
121
|
+
[(mask[0] / zoom), (mask[1] / zoom)].map(&:round)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def thumbnail_zoom_factor(mask)
|
|
125
|
+
[
|
|
126
|
+
mask[0].to_f / (image_file_width || 1),
|
|
127
|
+
mask[1].to_f / (image_file_height || 1),
|
|
128
|
+
].max
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def default_crop_from
|
|
132
|
+
return nil unless settings[:crop]
|
|
133
|
+
return nil if default_crop_size.nil?
|
|
134
|
+
|
|
135
|
+
[
|
|
136
|
+
((image_file_width || 0) - default_crop_size[0]) / 2,
|
|
137
|
+
((image_file_height || 0) - default_crop_size[1]) / 2,
|
|
138
|
+
].map(&:round)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def dimensions_from_string(string)
|
|
142
|
+
return if string.nil?
|
|
143
|
+
|
|
144
|
+
string.split("x", 2).map(&:to_i)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def inferred_dimensions_from_string(string)
|
|
148
|
+
return if string.nil?
|
|
149
|
+
|
|
150
|
+
width, height = dimensions_from_string(string)
|
|
151
|
+
ratio = image_file_width.to_f / image_file_height.to_i
|
|
152
|
+
|
|
153
|
+
if width.zero? && ratio.is_a?(Float)
|
|
154
|
+
width = height * ratio
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if height.zero? && ratio.is_a?(Float)
|
|
158
|
+
height = width / ratio
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
[width.to_i, height.to_i]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def fix_crop_values
|
|
165
|
+
%i[crop_from crop_size].each do |crop_value|
|
|
166
|
+
if public_send(crop_value).is_a?(String)
|
|
167
|
+
public_send("#{crop_value}=", normalize_crop_value(crop_value))
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def normalize_crop_value(crop_value)
|
|
173
|
+
public_send(crop_value).split("x").map { |n| normalize_number(n) }.join("x")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def normalize_number(number)
|
|
177
|
+
number = number.to_f.round
|
|
178
|
+
number.negative? ? 0 : number
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -10,7 +10,7 @@ module Alchemy
|
|
|
10
10
|
#
|
|
11
11
|
module TouchElements
|
|
12
12
|
def self.included(base)
|
|
13
|
-
base.
|
|
13
|
+
base.after_update(:touch_elements)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
private
|
|
@@ -18,7 +18,7 @@ module Alchemy
|
|
|
18
18
|
def touch_elements
|
|
19
19
|
return unless respond_to?(:elements)
|
|
20
20
|
|
|
21
|
-
elements.
|
|
21
|
+
elements.touch_all
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Renders a picture ingredient view
|
|
5
|
+
class PictureView
|
|
6
|
+
include ActionView::Helpers::AssetTagHelper
|
|
7
|
+
include ActionView::Helpers::UrlHelper
|
|
8
|
+
include Rails.application.routes.url_helpers
|
|
9
|
+
|
|
10
|
+
attr_reader :ingredient, :html_options, :options, :picture
|
|
11
|
+
|
|
12
|
+
DEFAULT_OPTIONS = {
|
|
13
|
+
show_caption: true,
|
|
14
|
+
disable_link: false,
|
|
15
|
+
srcset: [],
|
|
16
|
+
sizes: [],
|
|
17
|
+
}.with_indifferent_access
|
|
18
|
+
|
|
19
|
+
def initialize(ingredient, options = {}, html_options = {})
|
|
20
|
+
@ingredient = ingredient
|
|
21
|
+
@options = DEFAULT_OPTIONS.merge(ingredient.settings).merge(options || {})
|
|
22
|
+
@html_options = html_options || {}
|
|
23
|
+
@picture = ingredient.picture
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render
|
|
27
|
+
return if picture.blank?
|
|
28
|
+
|
|
29
|
+
output = caption ? img_tag + caption : img_tag
|
|
30
|
+
|
|
31
|
+
if is_linked?
|
|
32
|
+
output = link_to(output, url_for(ingredient.link), {
|
|
33
|
+
title: ingredient.link_title.presence,
|
|
34
|
+
target: ingredient.link_target == "blank" ? "_blank" : nil,
|
|
35
|
+
data: { link_target: ingredient.link_target.presence },
|
|
36
|
+
})
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if caption
|
|
40
|
+
content_tag(:figure, output, { class: ingredient.css_class.presence }.merge(html_options))
|
|
41
|
+
else
|
|
42
|
+
output
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def caption
|
|
47
|
+
return unless show_caption?
|
|
48
|
+
|
|
49
|
+
@_caption ||= content_tag(:figcaption, ingredient.caption)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def src
|
|
53
|
+
ingredient.picture_url(options.except(*DEFAULT_OPTIONS.keys))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def img_tag
|
|
57
|
+
@_img_tag ||= image_tag(
|
|
58
|
+
src, {
|
|
59
|
+
alt: alt_text,
|
|
60
|
+
title: ingredient.title.presence,
|
|
61
|
+
class: caption ? nil : ingredient.css_class.presence,
|
|
62
|
+
srcset: srcset.join(", ").presence,
|
|
63
|
+
sizes: options[:sizes].join(", ").presence,
|
|
64
|
+
}.merge(caption ? {} : html_options)
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def show_caption?
|
|
69
|
+
options[:show_caption] && ingredient.caption.present?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def is_linked?
|
|
73
|
+
!options[:disable_link] && ingredient.link.present?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def srcset
|
|
77
|
+
options[:srcset].map do |size|
|
|
78
|
+
url = ingredient.picture_url(size: size)
|
|
79
|
+
width, height = size.split("x")
|
|
80
|
+
width.present? ? "#{url} #{width}w" : "#{url} #{height}h"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def alt_text
|
|
85
|
+
ingredient.alt_tag.presence || html_options.delete(:alt) || ingredient.picture.name&.humanize
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|