refactor TeaserImage component

This commit is contained in:
Alina Beck 2020-02-05 20:43:47 +01:00
parent f29d486da0
commit e14cbf8173
3 changed files with 135 additions and 199 deletions

View File

@ -7,20 +7,16 @@
@submit="submit"
>
<template slot-scope="{ errors }">
<hc-teaser-image
:contribution="contribution"
@addTeaserImage="addTeaserImage"
:class="{ '--blur-image': form.blurImage }"
@addImageAspectRatio="addImageAspectRatio"
>
<img
v-if="contribution"
class="contribution-image"
:src="contribution.image | proxyApiUrl"
/>
</hc-teaser-image>
<ds-card>
<base-card>
<section class="card-image">
<img v-if="contribution" class="preview-image" :src="contribution.image | proxyApiUrl" />
<teaser-image
:contribution="contribution"
:class="{ '--blur-image': form.blurImage }"
@addTeaserImage="addTeaserImage"
@addImageAspectRatio="addImageAspectRatio"
/>
</section>
<div class="blur-toggle">
<label for="blur-img">{{ $t('contribution.inappropriatePicture') }}</label>
<input type="checkbox" id="blur-img" v-model="form.blurImage" />
@ -99,7 +95,7 @@
</ds-text>
<ds-space />
<div slot="footer" style="text-align: right">
<div style="text-align: right">
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
{{ $t('actions.cancel') }}
</base-button>
@ -108,7 +104,7 @@
</base-button>
</div>
<ds-space margin-bottom="large" />
</ds-card>
</base-card>
</template>
</ds-form>
</template>
@ -121,14 +117,14 @@ import HcEditor from '~/components/Editor/Editor'
import locales from '~/locales'
import PostMutations from '~/graphql/PostMutations.js'
import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
import HcTeaserImage from '~/components/TeaserImage/TeaserImage'
import TeaserImage from '~/components/TeaserImage/TeaserImage'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
export default {
components: {
HcEditor,
HcCategoriesSelect,
HcTeaserImage,
TeaserImage,
UserTeaser,
},
props: {
@ -297,8 +293,14 @@ export default {
<style lang="scss">
.contribution-form {
.ds-card-image.--blur-image img {
filter: blur(32px);
.card-image {
position: relative;
}
.preview-image {
width: 100%;
max-height: 2000px;
object-fit: contain;
}
.blur-toggle {

View File

@ -28,21 +28,10 @@ describe('TeaserImage.vue', () => {
const message = 'File upload failed'
const fileError = { status: 'error' }
it('defaults to error false', () => {
expect(wrapper.vm.error).toEqual(false)
})
it('shows an error toaster when verror is called', () => {
wrapper.vm.verror(fileError, message)
wrapper.vm.onDropzoneError(fileError, message)
expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, message)
})
it('changes error status from false to true to false', () => {
wrapper.vm.verror(fileError, message)
expect(wrapper.vm.error).toEqual(true)
jest.runAllTimers()
expect(wrapper.vm.error).toEqual(false)
})
})
})
})

View File

@ -1,15 +1,20 @@
<template>
<vue-dropzone
:options="dropzoneOptions"
ref="el"
id="postdropzone"
class="ds-card-image"
:use-custom-slot="true"
@vdropzone-error="verror"
@vdropzone-thumbnail="transformImage"
>
<div class="crop-overlay" ref="cropperOverlay" v-show="showCropper">
<base-button @click="cropImage" class="crop-confirm" filled>
<!-- TODO: rename to ImageUploader when delete-teaser-image PR is merged -->
<div class="image-uploader">
<vue-dropzone
v-show="!showCropper"
id="postdropzone"
:options="dropzoneOptions"
:use-custom-slot="true"
@vdropzone-error="onDropzoneError"
@vdropzone-thumbnail="initCropper"
>
<loading-spinner v-if="isLoadingImage" />
<base-icon v-else name="image" />
</vue-dropzone>
<div v-show="showCropper" class="crop-overlay">
<img id="cropping-image" />
<base-button class="crop-confirm" filled @click="cropImage">
{{ $t('contribution.teaserImage.cropperConfirm') }}
</base-button>
<base-button
@ -19,36 +24,22 @@
circle
danger
filled
@click="cancelCrop"
@click="closeCropper"
/>
</div>
<div
:class="{
'hc-attachments-upload-area-post': true,
'hc-attachments-upload-area-update-post': contribution,
}"
>
<slot></slot>
<div
:class="{
'hc-drag-marker-post': true,
'hc-drag-marker-update-post': contribution,
}"
>
<base-icon name="image" />
</div>
</div>
</vue-dropzone>
</div>
</template>
<script>
import vueDropzone from 'nuxt-dropzone'
import VueDropzone from 'nuxt-dropzone'
import Cropper from 'cropperjs'
import LoadingSpinner from '~/components/_new/generic/LoadingSpinner/LoadingSpinner'
import 'cropperjs/dist/cropper.css'
export default {
components: {
vueDropzone,
LoadingSpinner,
VueDropzone,
},
props: {
contribution: { type: Object, default: () => {} },
@ -58,181 +49,135 @@ export default {
dropzoneOptions: {
url: () => '',
maxFilesize: 5.0,
previewTemplate: this.template(),
previewTemplate: '<img class="preview-image" />',
},
image: null,
file: null,
editor: null,
cropper: null,
thumbnailElement: null,
oldImage: null,
error: false,
file: null,
showCropper: false,
isLoadingImage: false,
}
},
methods: {
template() {
return `<div class="dz-preview dz-file-preview">
<div class="dz-image">
<div data-dz-thumbnail-bg></div>
</div>
</div>
`
},
verror(file, message) {
this.error = true
onDropzoneError(file, message) {
this.$toast.error(file.status, message)
setTimeout(() => {
this.error = false
}, 2000)
},
transformImage(file) {
this.file = file
this.showCropper = true
this.initEditor()
this.initCropper()
},
initEditor() {
this.editor = this.$refs.cropperOverlay
this.clearImages()
this.thumbnailElement.appendChild(this.editor)
},
clearImages() {
this.thumbnailElement = document.querySelectorAll('#postdropzone')[0]
const thumbnailPreview = document.querySelectorAll('.thumbnail-preview')[0]
if (thumbnailPreview) thumbnailPreview.remove()
const contributionImage = document.querySelectorAll('.contribution-image')[0]
this.oldImage = contributionImage
if (contributionImage) contributionImage.remove()
const images = document.querySelectorAll('.preview-image')
images.forEach((image, index) => {
if (index === images.length - 1) image.src = ''
else image.remove()
})
},
initCropper() {
this.image = new Image()
this.image.src = URL.createObjectURL(this.file)
this.editor.appendChild(this.image)
this.cropper = new Cropper(this.image, { zoomable: false, autoCropArea: 0.9 })
initCropper(file) {
this.showCropper = true
this.file = file
this.clearImages()
const imageElement = document.querySelector('#cropping-image')
imageElement.src = URL.createObjectURL(file)
this.cropper = new Cropper(imageElement, { zoomable: false, autoCropArea: 0.9 })
},
cropImage() {
this.showCropper = false
this.isLoadingImage = true
const canvas = this.cropper.getCroppedCanvas()
canvas.toBlob(blob => {
const imageAspectRatio = canvas.width / canvas.height
this.setupPreview(canvas)
this.removeCropper()
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
this.$emit('addTeaserImage', croppedImageFile)
this.$emit('addImageAspectRatio', imageAspectRatio)
this.setupPreview(canvas.toDataURL())
}, 'image/jpeg')
this.closeCropper()
},
setupPreview(canvas) {
this.image = new Image()
this.image.src = canvas.toDataURL()
this.image.classList.add('thumbnail-preview')
this.thumbnailElement.appendChild(this.image)
setupPreview(url) {
const previewElement = document.querySelector('.preview-image')
previewElement.src = url
this.$nextTick((this.isLoadingImage = false))
},
cancelCrop() {
closeCropper() {
this.showCropper = false
if (this.oldImage) this.thumbnailElement.appendChild(this.oldImage)
this.removeCropper()
},
removeCropper() {
this.editor.removeChild(document.querySelectorAll('.cropper-container')[0])
this.cropper.destroy()
},
},
}
</script>
<style lang="scss">
#postdropzone {
width: 100%;
min-height: 400px;
background-color: $background-color-softest;
}
.hc-attachments-upload-area-post {
.image-uploader {
position: relative;
display: flex;
justify-content: center;
min-height: 200px;
overflow: hidden;
cursor: pointer;
}
.hc-attachments-upload-area-update-post img {
object-fit: cover;
object-position: center;
display: block;
width: 100%;
}
&:only-child {
background-color: $color-neutral-85;
}
.hc-attachments-upload-area-update-post:hover {
opacity: 0.7;
}
&:disabled {
pointer-events: none;
}
.hc-drag-marker-post {
position: absolute;
width: 122px;
height: 122px;
border-radius: 100%;
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;
background-color: $background-color-softest;
opacity: 0.65;
&.--blur-image img {
filter: blur(22px);
}
&:before {
.preview-image + & {
position: absolute;
content: '';
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: 100%;
border: 20px solid $text-color-base;
visibility: hidden;
bottom: 0;
left: 0;
}
&:after {
> .crop-overlay {
width: 100%;
height: 400px;
font-size: $font-size-base;
> .img {
display: block;
max-width: 100%;
}
> .crop-confirm {
position: absolute;
left: 10px;
top: 10px;
z-index: 1;
}
> .crop-cancel {
position: absolute;
right: 10px;
top: 10px;
z-index: 1;
}
}
.dz-message {
position: absolute;
content: '';
top: 10px;
left: 10px;
bottom: 10px;
right: 10px;
border-radius: 100%;
border: $border-size-base dashed $text-color-base;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
z-index: 1;
&:hover {
> .base-icon {
opacity: 1;
}
}
> .base-icon {
position: absolute;
padding: $space-small;
border-radius: 100%;
border: $border-size-base dashed $color-neutral-20;
background-color: $color-neutral-95;
font-size: 60px;
opacity: 0.7;
}
}
.hc-attachments-upload-area-post:hover & {
opacity: 1;
}
}
.hc-drag-marker-update-post {
opacity: 0;
}
.contribution-form-footer {
border-top: $border-size-base solid $border-color-softest;
}
.crop-overlay {
max-height: 2000px;
position: relative;
width: 100%;
background-color: #000;
}
.crop-confirm {
position: absolute;
left: 10px;
top: 10px;
z-index: 1;
}
.crop-cancel {
position: absolute;
right: 10px;
top: 10px;
z-index: 1;
}
</style>