aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortaehoon <th.dev91@gmail.com>2019-02-07 03:05:59 -0500
committertaehoon <th.dev91@gmail.com>2019-02-15 13:34:33 -0500
commit13725f040bca346a7b35b832f36f4e86c5da11e4 (patch)
treeaafd73aa352b6f2e52b968d1e2e79cb225cdc0d6 /src
parent4f95371081fd54291e3d81d7e254e9cfa1bd5b82 (diff)
Add avatar crop popup
Diffstat (limited to 'src')
-rw-r--r--src/components/image_cropper/image_cropper.js88
-rw-r--r--src/components/image_cropper/image_cropper.vue39
-rw-r--r--src/components/modal/modal.js17
-rw-r--r--src/components/modal/modal.vue101
-rw-r--r--src/components/settings/settings.vue14
-rw-r--r--src/components/user_settings/user_settings.js29
-rw-r--r--src/components/user_settings/user_settings.vue52
-rw-r--r--src/i18n/en.json2
8 files changed, 294 insertions, 48 deletions
diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
new file mode 100644
index 00000000..e4bf5ea2
--- /dev/null
+++ b/src/components/image_cropper/image_cropper.js
@@ -0,0 +1,88 @@
+import Cropper from 'cropperjs'
+import Modal from '../modal/modal.vue'
+import 'cropperjs/dist/cropper.css'
+
+const ImageCropper = {
+ props: {
+ trigger: {
+ type: [String, Element],
+ required: true
+ },
+ cropperOptions: {
+ type: Object,
+ default () {
+ return {
+ aspectRatio: 1,
+ autoCropArea: 1,
+ viewMode: 1,
+ movable: false,
+ zoomable: false,
+ guides: false
+ }
+ }
+ },
+ mimes: {
+ type: String,
+ default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
+ },
+ title: {
+ type: String,
+ default: 'Crop picture'
+ },
+ saveButtonLabel: {
+ type: String,
+ default: 'Save'
+ }
+ },
+ data () {
+ return {
+ cropper: undefined,
+ dataUrl: undefined,
+ filename: undefined
+ }
+ },
+ components: {
+ Modal
+ },
+ methods: {
+ destroy () {
+ this.cropper.destroy()
+ this.$refs.input.value = ''
+ this.dataUrl = undefined
+ },
+ submit () {
+ this.$emit('submit', this.cropper, this.filename)
+ this.destroy()
+ },
+ pickImage () {
+ this.$refs.input.click()
+ },
+ createCropper () {
+ this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
+ }
+ },
+ mounted () {
+ // listen for click event on trigger
+ let trigger = typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
+ if (!trigger) {
+ this.$emit('error', 'No image make trigger found.', 'user')
+ } else {
+ trigger.addEventListener('click', this.pickImage)
+ }
+ // listen for input file changes
+ let fileInput = this.$refs.input
+ fileInput.addEventListener('change', () => {
+ if (fileInput.files != null && fileInput.files[0] != null) {
+ let reader = new FileReader()
+ reader.onload = (e) => {
+ this.dataUrl = e.target.result
+ }
+ reader.readAsDataURL(fileInput.files[0])
+ this.filename = fileInput.files[0].name || 'unknown'
+ this.$emit('changed', fileInput.files[0], reader)
+ }
+ })
+ }
+}
+
+export default ImageCropper
diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue
new file mode 100644
index 00000000..1c52842c
--- /dev/null
+++ b/src/components/image_cropper/image_cropper.vue
@@ -0,0 +1,39 @@
+<template>
+ <div class="image-cropper">
+ <modal :show="dataUrl" :title="title" @close="destroy">
+ <div class="modal-body">
+ <div class="image-cropper-image-container">
+ <img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn image-cropper-btn" type="button" @click="submit" v-text="saveButtonLabel"></button>
+ </div>
+ </modal>
+ <input ref="input" type="file" class="image-cropper-img-input" :accept="mimes">
+ </div>
+</template>
+
+<script src="./image_cropper.js"></script>
+
+<style lang="scss">
+.image-cropper {
+ &-img-input {
+ display: none;
+ }
+
+ &-image-container {
+ position: relative;
+
+ img {
+ display: block;
+ max-width: 100%;
+ }
+ }
+
+ &-btn {
+ display: block;
+ width: 100%;
+ }
+}
+</style>
diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js
new file mode 100644
index 00000000..36cd7f4a
--- /dev/null
+++ b/src/components/modal/modal.js
@@ -0,0 +1,17 @@
+const Modal = {
+ props: ['show', 'title'],
+ methods: {
+ close: function () {
+ this.$emit('close')
+ }
+ },
+ mounted: function () {
+ document.addEventListener('keydown', (e) => {
+ if (this.show && e.keyCode === 27) {
+ this.close()
+ }
+ })
+ }
+}
+
+export default Modal
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
new file mode 100644
index 00000000..2e3cbe75
--- /dev/null
+++ b/src/components/modal/modal.vue
@@ -0,0 +1,101 @@
+<template>
+ <transition name="modal">
+ <div class="modal-mask" @click="close" v-if="show">
+ <div class="modal-container" @click.stop>
+ <div class="modal-header">
+ <h3 class="modal-title">{{title}}</h3>
+ </div>
+ <slot></slot>
+ <a class="modal-close" @click="close"><i class="icon-cancel"></i></a>
+ </div>
+ </div>
+ </transition>
+</template>
+
+<script src="./modal.js"></script>
+
+<style lang="scss">
+.modal-mask {
+ position: fixed;
+ z-index: 9998;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(27,31,35,.5);
+ transition: opacity .3s ease;
+}
+
+.modal-container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: 450px;
+ margin: 10vh auto;
+ max-height: 80vh;
+ max-width: 90vw;
+ background-color: #fff;
+ border: 1px solid #444d56;
+ border-radius: 3px;
+ box-shadow: 0 0 18px rgba(0,0,0,.4);
+ transition: all .3s ease;
+}
+
+.modal-header {
+ flex: none;
+ background-color: #f6f8fa;
+ border-bottom: 1px solid #d1d5da;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ padding: 16px;
+ margin: 0;
+}
+
+h3.modal-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #24292e;
+ margin: 0;
+}
+
+.modal-close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 16px;
+ cursor: pointer;
+}
+
+.modal-body {
+ border-bottom: 1px solid #e1e4e8;
+ padding: 16px;
+ overflow-y: auto;
+}
+
+.modal-footer {
+ flex: none;
+ border-top: 1px solid #e1e4e8;
+ margin-top: -1px;
+ padding: 16px;
+}
+
+/*
+ * The following styles are auto-applied to elements with
+ * transition="modal" when their visibility is toggled
+ * by Vue.js.
+ */
+
+.modal-enter {
+ opacity: 0;
+}
+
+.modal-leave-active {
+ opacity: 0;
+}
+
+.modal-enter .modal-container,
+.modal-leave-active .modal-container {
+ -webkit-transform: scale(1.1);
+ transform: scale(1.1);
+}
+</style>
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index dfb2e49d..91232382 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -311,20 +311,6 @@
color: $fallback--cRed;
}
- .old-avatar {
- width: 128px;
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
- }
-
- .new-avatar {
- object-fit: cover;
- width: 128px;
- height: 128px;
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
- }
-
.btn {
min-height: 28px;
min-width: 10em;
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index d20bf308..b3d31d67 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,6 +1,7 @@
import { unescape } from 'lodash'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
+import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
@@ -24,7 +25,6 @@ const UserSettings = {
bannerUploading: false,
backgroundUploading: false,
followListUploading: false,
- avatarPreview: null,
bannerPreview: null,
backgroundPreview: null,
avatarUploadError: null,
@@ -41,7 +41,8 @@ const UserSettings = {
},
components: {
StyleSwitcher,
- TabSwitcher
+ TabSwitcher,
+ ImageCropper
},
computed: {
user () {
@@ -117,31 +118,13 @@ const UserSettings = {
}
reader.readAsDataURL(file)
},
- submitAvatar () {
- if (!this.avatarPreview) { return }
-
- let img = this.avatarPreview
- // eslint-disable-next-line no-undef
- let imginfo = new Image()
- let cropX, cropY, cropW, cropH
- imginfo.src = img
- if (imginfo.height > imginfo.width) {
- cropX = 0
- cropW = imginfo.width
- cropY = Math.floor((imginfo.height - imginfo.width) / 2)
- cropH = imginfo.width
- } else {
- cropY = 0
- cropH = imginfo.height
- cropX = Math.floor((imginfo.width - imginfo.height) / 2)
- cropW = imginfo.height
- }
+ submitAvatar (cropper) {
+ const img = cropper.getCroppedCanvas({ width: 150, height: 150 }).toDataURL('image/jpeg')
this.avatarUploading = true
- this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
+ this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
- this.avatarPreview = null
} else {
this.avatarUploadError = this.$t('upload.error.base') + user.error
}
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index d2381da2..9fcd3752 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -47,20 +47,20 @@
<div class="setting-item">
<h2>{{$t('settings.avatar')}}</h2>
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
- <p>{{$t('settings.current_avatar')}}</p>
- <img :src="user.profile_image_url_original" class="old-avatar"></img>
- <p>{{$t('settings.set_new_avatar')}}</p>
- <img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
- </img>
- <div>
- <input type="file" @change="uploadFile('avatar', $event)" ></input>
+ <div class="avatar-upload-wrapper">
+ <div class="avatar-upload">
+ <img :src="user.profile_image_url_original" class="avatar" />
+ <div class="avatar-upload-loading-wrapper" v-if="avatarUploading">
+ <i class="icon-spin4 animate-spin"></i>
+ </div>
+ </div>
</div>
- <i class="icon-spin4 animate-spin" v-if="avatarUploading"></i>
- <button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button>
- <div class='alert error' v-if="avatarUploadError">
+ <button class="btn" type="button" id="pick-avatar" :disabled="avatarUploading">{{$t('settings.set_new_avatar')}}</button>
+ <div class="alert error" v-if="avatarUploadError">
Error: {{ avatarUploadError }}
<i class="button-icon icon-cancel" @click="clearUploadError('avatar')"></i>
</div>
+ <image-cropper trigger="#pick-avatar" :title="$t('settings.crop_your_new_avatar')" :saveButtonLabel="$t('settings.set_new_avatar')" @submit="submitAvatar" />
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_banner')}}</h2>
@@ -167,6 +167,8 @@
</script>
<style lang="scss">
+@import '../../_variables.scss';
+
.profile-edit {
.bio {
margin: 0;
@@ -193,5 +195,35 @@
.bg {
max-width: 100%;
}
+
+ .avatar-upload {
+ display: inline-block;
+ position: relative;
+ }
+
+ .avatar-upload-loading-wrapper {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba(0,0,0,.3);
+
+ i {
+ font-size: 50px;
+ color: #FFF;
+ }
+ }
+
+ .avatar {
+ display: block;
+ width: 150px;
+ height: 150px;
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
+ }
}
</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index c664fbfa..90132e3e 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -112,7 +112,7 @@
"collapse_subject": "Collapse posts with subjects",
"composing": "Composing",
"confirm_new_password": "Confirm new password",
- "current_avatar": "Your current avatar",
+ "crop_your_new_avatar": "Crop your new avatar",
"current_password": "Current password",
"current_profile_banner": "Your current profile banner",
"data_import_export_tab": "Data Import / Export",