Skip to content

Commit

Permalink
feat: ✨ upload image
Browse files Browse the repository at this point in the history
  • Loading branch information
Leecason committed Dec 13, 2019
1 parent eb467e1 commit 26ac703
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 1 deletion.
22 changes: 22 additions & 0 deletions src/components/ElTiptap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,28 @@ export default {
}
}
.image-view {
display: inline-block;
line-height: 0;
margin: 0 .2rem;
max-width: 100%;
position: relative;
user-select: none;
vertical-align: baseline;
&--focused {
&::before {
border: 2px solid #b3d8ff;;
bottom: 0;
content: '';
left: 0;
position: absolute;
right: 0;
top: 0;
}
}
}
hr {
margin-top: 20px;
margin-bottom: 20px;
Expand Down
95 changes: 95 additions & 0 deletions src/components/MenuBar/ImageUploadCommandButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template>
<div>
<command-button
tooltip="Image"
icon="image"
@click.native="imageUploadDialogVisible = true"
/>

<el-dialog title="Upload image" :visible.sync="imageUploadDialogVisible">
<el-upload
:http-request="uploadImage"
:show-file-list="false"
class="image-upload"
action="#"
drag
accept="image/*"
>
<div class="image-upload__icon">
<i class="fa fa-upload" />
</div>
<div class="el-upload__text">
Choose an image file or drag it here
</div>
</el-upload>
</el-dialog>
</div>
</template>

<script>
import { readFileDataUrl } from '@/utils/shared';
import CommandButton from './CommandButton';
export default {
name: 'ImageUploadCommandButton',
components: {
CommandButton,
},
props: {
editorContext: {
type: Object,
required: true,
},
},
data () {
return {
imageUploadDialogVisible: false,
};
},
methods: {
uploadImage (uploadOptions) {
const { file } = uploadOptions;
readFileDataUrl(file)
.then((url) => {
this.editorContext.commands.image({ src: url });
this.imageUploadDialogVisible = false;
});
},
},
};
</script>

<style lang="scss">
.el-upload {
display: flex;
width: 100%;
.el-upload-dragger {
align-items: center;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
height: 300px;
width: auto;
.image-upload__icon {
font-size: 50px;
margin-bottom: 10px;
}
&:hover {
.image-upload__icon {
color: #409EFF;
}
}
}
}
</style>
6 changes: 6 additions & 0 deletions src/components/MenuBar/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
icon="link"
/>

<image-upload-command-button
:editorContext="editorContext"
/>

<command-button
:is-active="editorContext.isActive.code_block()"
:command="editorContext.commands.code_block"
Expand Down Expand Up @@ -148,6 +152,7 @@ import { isTextAlignActive } from '@/extensions/text_align';
import HeadingDropdown from './HeadingDropdown.vue';
import LineHeightDropdown from './LineHeightDropdown.vue';
import ImageUploadCommandButton from './ImageUploadCommandButton.vue';
import CommandButton from './CommandButton.vue';
export default {
Expand All @@ -157,6 +162,7 @@ export default {
EditorMenuBar,
HeadingDropdown,
LineHeightDropdown,
ImageUploadCommandButton,
CommandButton,
},
Expand Down
81 changes: 81 additions & 0 deletions src/extensions/image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Image as TiptapImage } from 'tiptap-extensions';
import { NodeSelection } from 'prosemirror-state';

export default class Image extends TiptapImage {
get schema () {
return {
inline: true,
attrs: {
src: {
default: '',
},
alt: {
default: '',
},
title: {
default: '',
},
width: {
default: null,
},
height: {
default: null,
},
},
group: 'inline',
draggable: true,
parseDOM: [{
tag: 'img[src]',
getAttrs: dom => {
const width = dom.getAttribute('width') || dom.style.width;
const height = dom.getAttribute('height') || dom.style.height;

return {
src: dom.getAttribute('src'),
title: dom.getAttribute('title'),
alt: dom.getAttribute('alt'),
width: parseInt(width, 10),
height: parseInt(height, 10),
};
},
} ],
toDOM: node => ['img', node.attrs],
};
}

get view () {
return {
name: 'ImageView',

template: `
<span
:class="{ 'image-view--focused': selected }"
class="image-view"
@click="handleImageViewClick"
>
<img
:src="node.attrs.src"
:title="node.attrs.title"
:alt="node.attrs.alt"
:width="node.attrs.width"
:height="node.attrs.height"
>
</span>
`,

props: ['node', 'view', 'getPos', 'selected'],

methods: {
// https://github.com/scrumpy/tiptap/issues/361#issuecomment-540299541
handleImageViewClick () {
let tr = this.view.state.tr;
const pos = this.getPos();
let pos1 = this.view.state.doc.resolve(pos);
let selection = new NodeSelection(pos1);
tr.setSelection(selection);
this.view.dispatch(tr);
},
},
};
}
}
1 change: 1 addition & 0 deletions src/extensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as Paragraph } from './paragraph';
export { default as Heading } from './heading';
export { default as Blockquote } from './blockquote';
export { default as ListItem } from './list_item';
export { default as Image } from './image';

// extensions
export { default as TextAlign } from './text_align';
Expand Down
5 changes: 4 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import {
DropdownMenu,
DropdownItem,
MessageBox,
Dialog,
Upload,
} from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

import App from './App.vue';
import router from './router';
import './style/index.scss';

Vue.config.productionTip = false;

Expand All @@ -26,6 +27,8 @@ Vue.use(Tooltip);
Vue.use(Dropdown);
Vue.use(DropdownMenu);
Vue.use(DropdownItem);
Vue.use(Dialog);
Vue.use(Upload);

Vue.prototype.$prompt = MessageBox.prompt;

Expand Down
11 changes: 11 additions & 0 deletions src/utils/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@ export function clamp (val, min, max) {
}
return val;
}

export function readFileDataUrl (file) {
const reader = new FileReader();

return new Promise((resolve, reject) => {
reader.onload = readerEvent => resolve(readerEvent.target.result);
reader.onerror = reject;

reader.readAsDataURL(file);
});
}
2 changes: 2 additions & 0 deletions src/views/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
Heading,
Blockquote,
ListItem,
Image,
TextAlign,
Indent,
Expand Down Expand Up @@ -72,6 +73,7 @@ export default {
new Italic(),
new Strike(),
new Link(),
new Image(),
new Blockquote(),
new CodeBlock(),
Expand Down
1 change: 1 addition & 0 deletions vue.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const path = require('path');

module.exports = {
runtimeCompiler: true,
chainWebpack: config => {
config.resolve.alias
.set('@', path.resolve(__dirname, 'src'))
Expand Down

0 comments on commit 26ac703

Please sign in to comment.