Merge pull request #1666 from Human-Connection/1466_image_cropping

🍰 Implement basic image cropping solution
This commit is contained in:
mattwr18 2019-10-14 21:12:11 +02:00 committed by GitHub
commit 63cc82edea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 137 additions and 75 deletions

View File

@ -18,7 +18,6 @@ export default async function fileUpload(params, { file, url }, uploadCallback =
const fileLocation = `/uploads/${Date.now()}-${slug(name)}`
await uploadCallback({ createReadStream, fileLocation })
delete params[file]
params[url] = fileLocation
}

View File

@ -262,7 +262,7 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.smallTag {
width: 100%;
position: relative;

View File

@ -131,10 +131,11 @@ export default {
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.ds-card-image img {
width: 100%;
max-height: 300px;
max-height: 2000px;
object-fit: contain;
-o-object-fit: cover;
object-fit: cover;
-o-object-position: center;

View File

@ -25,17 +25,6 @@ describe('TeaserImage.vue', () => {
wrapper = Wrapper()
})
describe('File upload', () => {
const imageUpload = [
{ file: { filename: 'avataar.svg', previewElement: '' }, url: 'someUrlToImage' },
]
it('supports adding a teaser image', () => {
wrapper.vm.addTeaserImage(imageUpload)
expect(wrapper.emitted().addTeaserImage[0]).toEqual(imageUpload)
})
})
describe('handles errors', () => {
beforeEach(() => jest.useFakeTimers())
const message = 'File upload failed'

View File

@ -5,25 +5,24 @@
id="postdropzone"
class="ds-card-image"
:use-custom-slot="true"
@vdropzone-thumbnail="thumbnail"
@vdropzone-error="verror"
@vdropzone-thumbnail="transformImage"
@vdropzone-drop="dropzoneDrop"
>
<div class="dz-message">
<div
:class="{
'hc-attachments-upload-area-post': true,
'hc-attachments-upload-area-update-post': contribution,
}"
>
<slot></slot>
<div
:class="{
'hc-attachments-upload-area-post': true,
'hc-attachments-upload-area-update-post': contribution,
'hc-drag-marker-post': true,
'hc-drag-marker-update-post': contribution,
}"
>
<slot></slot>
<div
:class="{
'hc-drag-marker-post': true,
'hc-drag-marker-update-post': contribution,
}"
>
<ds-icon name="image" size="xxx-large" />
</div>
<ds-icon name="image" size="xxx-large" />
</div>
</div>
</vue-dropzone>
@ -31,6 +30,8 @@
<script>
import vueDropzone from 'nuxt-dropzone'
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
export default {
components: {
@ -42,7 +43,7 @@ export default {
data() {
return {
dropzoneOptions: {
url: this.addTeaserImage,
url: () => '',
maxFilesize: 5.0,
previewTemplate: this.template(),
},
@ -70,27 +71,50 @@ export default {
this.error = true
this.$toast.error(file.status, message)
},
addTeaserImage(file) {
this.$emit('addTeaserImage', file[0])
return ''
transformImage(file) {
let thumbnailElement, editor, confirm, thumbnailPreview, contributionImage
// Create the image editor overlay
editor = document.createElement('div')
thumbnailElement = document.querySelectorAll('#postdropzone')[0]
thumbnailPreview = document.querySelectorAll('.thumbnail-preview')[0]
if (thumbnailPreview) thumbnailPreview.remove()
contributionImage = document.querySelectorAll('.contribution-image')[0]
if (contributionImage) contributionImage.remove()
editor.classList.add('crop-overlay')
thumbnailElement.appendChild(editor)
// Create the confirm button
confirm = document.createElement('button')
confirm.classList.add('crop-confirm', 'ds-button', 'ds-button-primary')
confirm.textContent = this.$t('contribution.teaserImage.cropperConfirm')
confirm.addEventListener('click', () => {
// Get the canvas with image data from Cropper.js
let canvas = cropper.getCroppedCanvas()
canvas.toBlob(blob => {
this.$refs.el.manuallyAddFile(blob, canvas.toDataURL(), null, null, {
dontSubstractMaxFiles: false,
addToFiles: true,
})
image = new Image()
image.src = canvas.toDataURL()
image.classList.add('thumbnail-preview')
thumbnailElement.appendChild(image)
// Remove the editor from view
editor.parentNode.removeChild(editor)
this.$emit('addTeaserImage', blob)
})
})
editor.appendChild(confirm)
// Load the image
let image = new Image()
image.src = URL.createObjectURL(file)
editor.appendChild(image)
// Create Cropper.js and pass image
let cropper = new Cropper(image, { zoomable: false })
},
thumbnail: (file, dataUrl) => {
let thumbnailElement, contributionImage, uploadArea, thumbnailPreview, image
if (file.previewElement) {
thumbnailElement = document.querySelectorAll('#postdropzone')[0]
contributionImage = document.querySelectorAll('.contribution-image')[0]
thumbnailPreview = document.querySelectorAll('.thumbnail-preview')[0]
if (contributionImage) {
uploadArea = document.querySelectorAll('.hc-attachments-upload-area-update-post')[0]
uploadArea.removeChild(contributionImage)
uploadArea.classList.remove('hc-attachments-upload-area-update-post')
}
image = new Image()
image.src = URL.createObjectURL(file)
image.classList.add('thumbnail-preview')
if (thumbnailPreview) return thumbnailElement.replaceChild(image, thumbnailPreview)
thumbnailElement.appendChild(image)
}
dropzoneDrop() {
let cropOverlay = document.querySelectorAll('.crop-overlay')[0]
if (cropOverlay) cropOverlay.remove()
},
},
}
@ -98,16 +122,10 @@ export default {
<style lang="scss">
#postdropzone {
width: 100%;
min-height: 300px;
min-height: 500px;
background-color: $background-color-softest;
}
@media only screen and (max-width: 960px) {
#postdropzone {
min-height: 200px;
}
}
.hc-attachments-upload-area-post {
position: relative;
display: flex;
@ -134,11 +152,10 @@ export default {
display: flex;
align-items: center;
justify-content: center;
margin: 180px 5px;
color: hsl(0, 0%, 25%);
transition: all 0.2s ease-out;
font-size: 60px;
margin: 80px 5px;
background-color: $background-color-softest;
opacity: 0.65;
@ -178,7 +195,17 @@ export default {
border-top: $border-size-base solid $border-color-softest;
}
.contribution-image {
max-height: 300px;
.crop-overlay {
max-height: 2000px;
position: relative;
width: 100%;
background-color: #000;
}
.crop-confirm {
position: absolute;
left: 10px;
top: 10px;
z-index: 1;
}
</style>

View File

@ -562,6 +562,9 @@
"it-internet-data-privacy": "IT, Internet & Datenschutz",
"art-culture-sport": "Kunst, Kultur & Sport"
}
},
"teaserImage": {
"cropperConfirm": "Bestätigen"
}
},
"code-of-conduct": {

View File

@ -563,6 +563,9 @@
"it-internet-data-privacy": "IT, Internet & Data Privacy",
"art-culture-sport": "Art, Culture, & Sport"
}
},
"teaserImage": {
"cropperConfirm": "Confirm"
}
},
"code-of-conduct": {

View File

@ -292,6 +292,11 @@
"message": "¿Realmente quieres liberar el comentario de \"<b>{name}</b>\"?"
}
},
"contribution": {
"teaserImage": {
"cropperConfirm": "Confirmar"
}
},
"user": {
"avatar": {
"submitted": "Carga con éxito"

View File

@ -287,6 +287,11 @@
"message": "Voulez-vous vraiment publier le commentaire de \"<b>{name}</b>\"?"
}
},
"contribution": {
"teaserImage": {
"cropperConfirm": "Confirmer"
}
},
"user": {
"avatar": {
"submitted": "Téléchargement réussi"

View File

@ -140,5 +140,10 @@
"save": "Salva",
"edit": "Modifica",
"delete": "Cancella"
},
"contribution": {
"teaserImage": {
"cropperConfirm": "Confermare"
}
}
}

View File

@ -158,7 +158,10 @@
},
"contribution": {
"edit": "Bijdrage bewerken",
"delete": "Bijdrage verwijderen"
"delete": "Bijdrage verwijderen",
"teaserImage": {
"cropperConfirm": "Bevestigen"
}
},
"comment": {
"edit": "Commentaar bewerken",

View File

@ -362,6 +362,9 @@
"languageSelectLabel": "Język",
"categories": {
"infoSelectedNoOfMaxCategories": "{chosen} z {max} wybrane kategorie"
},
"teaserImage": {
"cropperConfirm": "Potwierdzać"
}
}
}

View File

@ -203,7 +203,10 @@
},
"contribution": {
"edit": "Editar Contribuição",
"delete": "Apagar Contribuição"
"delete": "Apagar Contribuição",
"teaserImage": {
"cropperConfirm": "Confirmar"
}
},
"comment": {
"content": {

View File

@ -44,7 +44,8 @@
],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1",
"^~/(.*)$": "<rootDir>/$1"
"^~/(.*)$": "<rootDir>/$1",
"\\.(css|less)$": "identity-obj-proxy"
},
"testMatch": [
"**/?(*.)+(spec|test).js?(x)"
@ -62,6 +63,7 @@
"apollo-cache-inmemory": "~1.6.3",
"apollo-client": "~2.6.4",
"cookie-universal-nuxt": "~2.0.18",
"cropperjs": "^1.5.5",
"cross-env": "~6.0.3",
"date-fns": "2.4.1",
"express": "~4.17.1",
@ -121,6 +123,7 @@
"eslint-plugin-vue": "~5.2.3",
"flush-promises": "^1.0.2",
"fuse.js": "^3.4.5",
"identity-obj-proxy": "^3.0.0",
"jest": "~24.9.0",
"mutation-observer": "^1.0.3",
"node-sass": "~4.12.0",
@ -133,4 +136,4 @@
"vue-svg-loader": "~0.12.0",
"vue-template-compiler": "^2.6.10"
}
}
}

View File

@ -190,6 +190,11 @@ export default {
</script>
<style lang="scss">
.ds-card-image img {
max-height: 2000px;
object-fit: contain;
}
.masonry-grid {
display: grid;
grid-gap: 10px;

View File

@ -200,8 +200,8 @@ export default {
.ds-card-image {
img {
max-height: 710px;
object-fit: cover;
max-height: 2000px;
object-fit: contain;
object-position: center;
}
}

View File

@ -87,9 +87,5 @@ export default {
<style lang="scss">
.related-post {
box-shadow: $box-shadow-base;
.ds-card-image {
max-height: 80px;
}
}
</style>

View File

@ -5538,6 +5538,11 @@ create-react-context@^0.2.1:
fbjs "^0.8.0"
gud "^1.0.0"
cropperjs@^1.5.5:
version "1.5.6"
resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.5.6.tgz#82faf432bec709d828f2f7a96d1179198edaf0e2"
integrity sha512-eAgWf4j7sNJIG329qUHIFi17PSV0VtuWyAu9glZSgu/KlQSrfTQOC2zAz+jHGa5fAB+bJldEnQwvJEaJ8zRf5A==
cross-env@~6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941"
@ -7856,6 +7861,11 @@ hard-source-webpack-plugin@^0.13.1:
webpack-sources "^1.0.1"
write-json-file "^2.3.0"
harmony-reflect@^1.4.6:
version "1.6.1"
resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.1.tgz#c108d4f2bb451efef7a37861fdbdae72c9bdefa9"
integrity sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA==
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@ -8266,6 +8276,13 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
dependencies:
postcss "^7.0.14"
identity-obj-proxy@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14"
integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=
dependencies:
harmony-reflect "^1.4.6"
ieee754@^1.1.4:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
@ -13618,11 +13635,6 @@ serve-static@1.14.1, serve-static@^1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
server-destroy@^1.0.1:
version "1.0.1"