From 4b7fcb79fa785a40098a39ba0db5ca9921bc9fe9 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Wed, 29 Apr 2020 16:44:34 -0400 Subject: [PATCH] Add reusable image editor modal (#503) * Install cropperjs * Add cropper js styles * Add editing image string * Track inner active state * Add temp edit button * Start on image editor modal * Add custom icons for image manipulation * Add image manipulation strings * Tweak cropper styles * Remove unused import * Save as blob * Expose getItem method for manual refreshes * Add cache-busting to file preview * Use new API post endpoint, emit refresh event on success * Add a cache buster to the image editor --- package.json | 1 + .../v-icon/custom-icons/flip_horizontal.vue | 18 + .../v-icon/custom-icons/flip_vertical.vue | 19 + src/components/v-icon/v-icon.vue | 6 + src/components/v-modal/v-modal.vue | 34 +- src/compositions/use-item/use-item.ts | 1 + src/interfaces/wysiwyg/wysiwyg.vue | 2 - src/lang/en-US/index.json | 8 + src/modules/files/routes/detail/detail.vue | 24 +- src/styles/lib/_cropperjs.scss | 273 +++++++++++++ src/styles/main.scss | 1 + .../components/image-editor/image-editor.vue | 366 ++++++++++++++++++ .../private/components/image-editor/index.ts | 4 + yarn.lock | 5 + 14 files changed, 751 insertions(+), 11 deletions(-) create mode 100644 src/components/v-icon/custom-icons/flip_horizontal.vue create mode 100644 src/components/v-icon/custom-icons/flip_vertical.vue create mode 100644 src/styles/lib/_cropperjs.scss create mode 100644 src/views/private/components/image-editor/image-editor.vue create mode 100644 src/views/private/components/image-editor/index.ts diff --git a/package.json b/package.json index f796256b1b..9338d5b623 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@vue/composition-api": "^0.5.0", "axios": "^0.19.2", "base-64": "^0.1.0", + "cropperjs": "^1.5.6", "date-fns": "^2.12.0", "lodash": "^4.17.15", "marked": "^1.0.0", diff --git a/src/components/v-icon/custom-icons/flip_horizontal.vue b/src/components/v-icon/custom-icons/flip_horizontal.vue new file mode 100644 index 0000000000..aee31b1062 --- /dev/null +++ b/src/components/v-icon/custom-icons/flip_horizontal.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/v-icon/custom-icons/flip_vertical.vue b/src/components/v-icon/custom-icons/flip_vertical.vue new file mode 100644 index 0000000000..c2275850c3 --- /dev/null +++ b/src/components/v-icon/custom-icons/flip_vertical.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/v-icon/v-icon.vue b/src/components/v-icon/v-icon.vue index 90565bcba5..144257de53 100644 --- a/src/components/v-icon/v-icon.vue +++ b/src/components/v-icon/v-icon.vue @@ -25,6 +25,8 @@ import CustomIconGrid6 from './custom-icons/grid_6.vue'; import CustomIconSignalWifi1Bar from './custom-icons/signal_wifi_1_bar.vue'; import CustomIconSignalWifi2Bar from './custom-icons/signal_wifi_2_bar.vue'; import CustomIconSignalWifi3Bar from './custom-icons/signal_wifi_3_bar.vue'; +import CustomIconFlipHorizontal from './custom-icons/flip_horizontal.vue'; +import CustomIconFlipVertical from './custom-icons/flip_vertical.vue'; const customIcons: string[] = [ 'box', @@ -37,6 +39,8 @@ const customIcons: string[] = [ 'signal_wifi_1_bar', 'signal_wifi_2_bar', 'signal_wifi_3_bar', + 'flip_horizontal', + 'flip_vertical', ]; export default defineComponent({ @@ -51,6 +55,8 @@ export default defineComponent({ CustomIconSignalWifi1Bar, CustomIconSignalWifi2Bar, CustomIconSignalWifi3Bar, + CustomIconFlipHorizontal, + CustomIconFlipVertical, }, props: { name: { diff --git a/src/components/v-modal/v-modal.vue b/src/components/v-modal/v-modal.vue index fbb8fd0048..cfd4093651 100644 --- a/src/components/v-modal/v-modal.vue +++ b/src/components/v-modal/v-modal.vue @@ -1,5 +1,5 @@ @@ -165,6 +181,10 @@ export default defineComponent({ padding: 32px; } } + + &.no-padding .main { + padding: 0px; + } } .footer { diff --git a/src/compositions/use-item/use-item.ts b/src/compositions/use-item/use-item.ts index 173ef2b575..01284c5a55 100644 --- a/src/compositions/use-item/use-item.ts +++ b/src/compositions/use-item/use-item.ts @@ -44,6 +44,7 @@ export function useItem(collection: Ref, primaryKey: Ref + + + + [ { diff --git a/src/styles/lib/_cropperjs.scss b/src/styles/lib/_cropperjs.scss new file mode 100644 index 0000000000..aadc821564 --- /dev/null +++ b/src/styles/lib/_cropperjs.scss @@ -0,0 +1,273 @@ +.cropper-container { + position: relative; + font-size: 0; + line-height: 0; + direction: ltr; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -ms-touch-action: none; + touch-action: none; +} + +.cropper-container img { + display: block; + width: 100%; + min-width: 0 !important; + max-width: none !important; + height: 100%; + min-height: 0 !important; + max-height: none !important; + image-orientation: 0deg; +} + +.cropper-wrap-box, +.cropper-canvas, +.cropper-drag-box, +.cropper-crop-box, +.cropper-modal { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.cropper-wrap-box, +.cropper-canvas { + overflow: hidden; +} + +.cropper-drag-box { + background-color: #fff; + opacity: 0; +} + +.cropper-modal { + background-color: #000; + opacity: 0.5; +} + +.cropper-view-box { + display: block; + width: 100%; + height: 100%; + overflow: hidden; + outline: 1px solid var(--primary); + outline-color: var(--primary-50); +} + +.cropper-dashed { + position: absolute; + display: block; + border: 0 dashed #fff; + border-style: solid; + box-shadow: 0 0px 0px 1px rgba(0, 0, 0, 0.3); + opacity: 0.4; +} + +.cropper-dashed.dashed-h { + top: calc(100% / 3); + left: 0; + width: 100%; + height: calc(100% / 3); + border-top-width: 1px; + border-bottom-width: 1px; +} + +.cropper-dashed.dashed-v { + top: 0; + left: calc(100% / 3); + width: calc(100% / 3); + height: 100%; + border-right-width: 1px; + border-left-width: 1px; +} + +.cropper-center { + position: absolute; + top: 50%; + left: 50%; + display: block; + width: 0; + height: 0; + opacity: 0.75; +} + +.cropper-center::before, +.cropper-center::after { + position: absolute; + display: block; + background-color: var(--background-subdued); + content: ' '; +} + +.cropper-center::before { + top: 0; + left: -3px; + width: 7px; + height: 1px; +} + +.cropper-center::after { + top: -3px; + left: 0; + width: 1px; + height: 7px; +} + +.cropper-face, +.cropper-line, +.cropper-point { + position: absolute; + display: block; + width: 100%; + height: 100%; + opacity: 0.1; +} + +.cropper-face { + top: 0; + left: 0; + background-color: var(--foreground-inverted); +} + +.cropper-line { + background-color: #000; + opacity: 0.05; +} + +.cropper-line.line-e { + top: 0; + right: -3px; + width: 5px; + cursor: ew-resize; +} + +.cropper-line.line-n { + top: -3px; + left: 0; + height: 5px; + cursor: ns-resize; +} + +.cropper-line.line-w { + top: 0; + left: -3px; + width: 5px; + cursor: ew-resize; +} + +.cropper-line.line-s { + bottom: -3px; + left: 0; + height: 5px; + cursor: ns-resize; +} + +.cropper-point { + width: 10px; + height: 10px; + background: #fff; + border-radius: 50%; + opacity: 1; +} + +.cropper-point.point-e { + top: 50%; + right: -5px; + margin-top: -5px; + cursor: ew-resize; +} + +.cropper-point.point-n { + top: -5px; + left: 50%; + margin-left: -5px; + cursor: ns-resize; +} + +.cropper-point.point-w { + top: 50%; + left: -5px; + margin-top: -5px; + cursor: ew-resize; +} + +.cropper-point.point-s { + bottom: -5px; + left: 50%; + margin-left: -5px; + cursor: s-resize; +} + +.cropper-point.point-ne { + top: -5px; + right: -5px; + cursor: nesw-resize; +} + +.cropper-point.point-nw { + top: -5px; + left: -5px; + cursor: nwse-resize; +} + +.cropper-point.point-sw { + bottom: -5px; + left: -5px; + cursor: nesw-resize; +} + +.cropper-point.point-se { + right: -5px; + bottom: -5px; + cursor: nwse-resize; +} + +.cropper-point.point-se::before { + position: absolute; + right: -50%; + bottom: -50%; + display: block; + width: 200%; + height: 200%; + background-color: var(--primary); + opacity: 0; + content: ' '; +} + +.cropper-invisible { + opacity: 0; +} + +.cropper-bg { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC'); +} + +.cropper-hide { + position: absolute; + display: block; + width: 0; + height: 0; +} + +.cropper-hidden { + display: none !important; +} + +.cropper-move { + cursor: move; +} + +.cropper-crop { + cursor: crosshair; +} + +.cropper-disabled .cropper-drag-box, +.cropper-disabled .cropper-face, +.cropper-disabled .cropper-line, +.cropper-disabled .cropper-point { + cursor: not-allowed; +} diff --git a/src/styles/main.scss b/src/styles/main.scss index 5022ab423d..c3bbbb9271 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -7,6 +7,7 @@ @import 'themes/light'; @import 'lib/codemirror'; @import 'lib/portal-vue'; +@import 'lib/cropperjs'; body.light { @include light; diff --git a/src/views/private/components/image-editor/image-editor.vue b/src/views/private/components/image-editor/image-editor.vue new file mode 100644 index 0000000000..32cc087cff --- /dev/null +++ b/src/views/private/components/image-editor/image-editor.vue @@ -0,0 +1,366 @@ + + + + + diff --git a/src/views/private/components/image-editor/index.ts b/src/views/private/components/image-editor/index.ts new file mode 100644 index 0000000000..f97f64ff0a --- /dev/null +++ b/src/views/private/components/image-editor/index.ts @@ -0,0 +1,4 @@ +import ImageEditor from './image-editor.vue'; + +export { ImageEditor }; +export default ImageEditor; diff --git a/yarn.lock b/yarn.lock index c6674d0fc9..0486d2510b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4923,6 +4923,11 @@ create-react-context@0.3.0, create-react-context@^0.3.0: gud "^1.0.0" warning "^4.0.3" +cropperjs@^1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.5.6.tgz#82faf432bec709d828f2f7a96d1179198edaf0e2" + integrity sha512-eAgWf4j7sNJIG329qUHIFi17PSV0VtuWyAu9glZSgu/KlQSrfTQOC2zAz+jHGa5fAB+bJldEnQwvJEaJ8zRf5A== + cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"