Skip to content

Commit

Permalink
feat(image): ✨ add image bubble menu, enable edit image alt, width, h…
Browse files Browse the repository at this point in the history
…eight

#21
  • Loading branch information
Leecason committed Mar 28, 2020
1 parent 98a3f4d commit 9791533
Show file tree
Hide file tree
Showing 28 changed files with 483 additions and 95 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"types/*"
],
"dependencies": {
"@juggle/resize-observer": "^3.1.2",
"core-js": "^3.4.3",
"tiptap": "^1.26.6",
"tiptap-extensions": "^1.28.6",
Expand Down
53 changes: 32 additions & 21 deletions src/components/ExtensionViews/ImageView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@
class="image-resizer__handler"
@mousedown="onMouseDown($event, direction)"
/>

<span
class="image-view__delete-trigger"
@click="removeImage"
>
<v-icon name="regular/trash-alt" />
</span>
</div>
</div>
</span>
Expand All @@ -42,10 +35,9 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
import { Node as ProsemirrorNode } from 'prosemirror-model';
import { NodeSelection } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { deleteSelection } from 'prosemirror-commands';
import Icon from 'vue-awesome/components/Icon.vue';
import 'vue-awesome/icons/regular/trash-alt';
import { ResizeObserver } from '@juggle/resize-observer';
import { resolveImg } from '@/utils/image';
import { clamp } from '@/utils/shared';
const enum ResizeDirection {
TOP_LEFT = 'tl',
Expand All @@ -55,12 +47,9 @@ const enum ResizeDirection {
};
const MIN_SIZE = 20;
const MAX_SIZE = 100000;
@Component({
components: {
'v-icon': Icon,
},
})
@Component
export default class ImageView extends Vue {
@Prop({
type: ProsemirrorNode,
Expand Down Expand Up @@ -92,11 +81,20 @@ export default class ImageView extends Vue {
})
readonly selected!: boolean;
maxSize = {
width: MAX_SIZE,
height: MAX_SIZE,
};
originalSize = {
width: 0,
height: 0,
};
resizeOb = new ResizeObserver(() => {
this.getMaxSize();
});
resizeDirections = [
ResizeDirection.TOP_LEFT,
ResizeDirection.TOP_RIGHT,
Expand Down Expand Up @@ -147,6 +145,14 @@ export default class ImageView extends Vue {
};
}
private mounted () {
this.resizeOb.observe(this.view.dom);
}
private beforeDestroy () {
this.resizeOb.disconnect();
}
// https://github.com/scrumpy/tiptap/issues/361#issuecomment-540299541
private selectImage () {
const { state } = this.view;
Expand All @@ -156,9 +162,9 @@ export default class ImageView extends Vue {
this.view.dispatch(tr);
}
private removeImage () {
const { state, dispatch } = this.view;
deleteSelection(state, dispatch);
private getMaxSize () {
const { width } = getComputedStyle(this.view.dom);
this.maxSize.width = parseInt(width, 10);
}
private onMouseDown (e: MouseEvent, dir: ResizeDirection): void {
Expand All @@ -173,14 +179,19 @@ export default class ImageView extends Vue {
const aspectRatio = originalWidth / originalHeight;
let { width, height } = this.node.attrs;
const maxWidth = this.maxSize.width;
if (width && !height) {
width = width > maxWidth ? maxWidth : width;
height = Math.round(width / aspectRatio);
} else if (height && !width) {
width = Math.round(height * aspectRatio);
width = width > maxWidth ? maxWidth : width;
} else if (!width && !height) {
width = originalWidth;
height = originalHeight;
width = originalWidth > maxWidth ? maxWidth : originalWidth;
height = Math.round(width / aspectRatio);
} else {
width = width > maxWidth ? maxWidth : width;
}
this.resizerState.w = width;
Expand All @@ -203,7 +214,7 @@ export default class ImageView extends Vue {
const dy = (e.clientY - y) * (/t/.test(dir) ? -1 : 1);
this.updateAttrs({
width: Math.max(w + dx, MIN_SIZE),
width: clamp(w + dx, MIN_SIZE, this.maxSize.width),
height: Math.max(h + dy, MIN_SIZE),
});
}
Expand Down
56 changes: 56 additions & 0 deletions src/components/MenuBubble/ImageBubbleMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<template>
<div class="image-bubble-menu">
<edit-image-command-button
:editor-context="editorContext"
:init-image-attrs="imageAttrs"
/>

<remove-image-command-button
:editor-context="editorContext"
/>
</div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Editor, MenuData } from 'tiptap';
import { isNodeSelection } from 'prosemirror-utils';
import EditImageCommandButton from '../MenuCommands/Image/EditImageCommandButton.vue';
import RemoveImageCommandButton from '../MenuCommands/Image/RemoveImageCommandButton.vue';
@Component({
components: {
EditImageCommandButton,
RemoveImageCommandButton,
},
})
export default class ImageBubbleMenu extends Vue {
@Prop({
type: Editor,
required: true,
})
readonly editor!: Editor;
@Prop({
type: Object,
required: true,
})
readonly editorContext!: MenuData;
private get imageAttrs (): Object {
const { selection } = this.editor.state;
if (isNodeSelection(selection)) {
// @ts-ignore
const { node } = selection;
return node.attrs;
}
return {};
}
};
</script>

<style lang="scss">
.image-bubble-menu {
display: flex;
}
</style>
6 changes: 3 additions & 3 deletions src/components/MenuBubble/LinkBubbleMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { MenuData } from 'tiptap';
import OpenLinkCommandButton from '@/components/MenuCommands/OpenLinkCommandButton.vue';
import EditLinkCommandButton from '@/components/MenuCommands/EditLinkCommandButton.vue';
import UnlinkCommandButton from '@/components/MenuCommands/UnlinkCommandButton.vue';
import OpenLinkCommandButton from '@/components/MenuCommands/Link/OpenLinkCommandButton.vue';
import EditLinkCommandButton from '@/components/MenuCommands/Link/EditLinkCommandButton.vue';
import UnlinkCommandButton from '@/components/MenuCommands/Link/UnlinkCommandButton.vue';
@Component({
components: {
Expand Down
16 changes: 14 additions & 2 deletions src/components/MenuBubble/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@
<div
:class="{
'el-tiptap-editor__menu-bubble--active':
editorContext.menu.isActive && (showLinkMenu || showTextMenu),
editorContext.menu.isActive && (
editorContext.isActive.image()
|| showLinkMenu
|| showTextMenu
),
}"
:style="`
left: ${ editorContext.menu.left }px;
bottom: ${ editorContext.menu.bottom + 10 }px;
`"
class="el-tiptap-editor__menu-bubble"
>
<image-bubble-menu
v-if="editorContext.isActive.image()"
:editor="editor"
:editorContext="editorContext"
/>

<link-bubble-menu
v-if="showLinkMenu"
v-else-if="showLinkMenu"
:editorContext="editorContext"
/>

Expand All @@ -42,11 +52,13 @@ import { getMarkRange } from 'tiptap-utils';
import { MenuBtnViewType } from '@/../types';
import LinkBubbleMenu from './LinkBubbleMenu.vue';
import ImageBubbleMenu from './ImageBubbleMenu.vue';
@Component({
components: {
EditorMenuBubble,
LinkBubbleMenu,
ImageBubbleMenu,
},
})
export default class MenuBubble extends Vue {
Expand Down
2 changes: 2 additions & 0 deletions src/components/MenuCommands/CommandButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import 'vue-awesome/icons/edit';
import 'vue-awesome/icons/unlink';
import 'vue-awesome/icons/external-link-alt';
import 'vue-awesome/icons/image';
import 'vue-awesome/icons/ellipsis-h';
import 'vue-awesome/icons/regular/trash-alt';
import 'vue-awesome/icons/video';
import 'vue-awesome/icons/code';
import 'vue-awesome/icons/quote-right';
Expand Down
146 changes: 146 additions & 0 deletions src/components/MenuCommands/Image/EditImageCommandButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<template>
<div>
<command-button
:command="openEditImageDialog"
:tooltip="t('editor.extensions.Image.buttons.image_options.tooltip')"
icon="ellipsis-h"
/>

<el-dialog
:title="t('editor.extensions.Image.control.edit_image.title')"
:visible.sync="editImageDialogVisible"
:append-to-body="true"
width="400px"
custom-class="el-tiptap-edit-image-dialog"
>
<el-form
:model="imageAttrs"
label-position="top"
size="small"
>
<el-form-item :label="t('editor.extensions.Image.control.edit_image.form.src')">
<el-input
:value="imageAttrs.src"
autocomplete="off"
disabled
/>
</el-form-item>

<el-form-item :label="t('editor.extensions.Image.control.edit_image.form.alt')">
<el-input
v-model="imageAttrs.alt"
autocomplete="off"
/>
</el-form-item>

<el-form-item>
<el-col :span="11">
<el-form-item :label="t('editor.extensions.Image.control.edit_image.form.width')">
<el-input
v-model="imageAttrs.width"
type="number"
/>
</el-form-item>
</el-col>
<el-col
:span="11"
:push="2"
>
<el-form-item :label="t('editor.extensions.Image.control.edit_image.form.height')">
<el-input
v-model="imageAttrs.height"
type="number"
/>
</el-form-item>
</el-col>
</el-form-item>
</el-form>

<template #footer>
<el-button
size="small"
round
@click="closeEditImageDialog"
>
{{ t('editor.extensions.Image.control.edit_image.cancel') }}
</el-button>

<el-button
type="primary"
size="small"
round
@click="updateImageAttrs"
>
{{ t('editor.extensions.Image.control.edit_image.confirm') }}
</el-button>
</template>
</el-dialog>
</div>
</template>

<script lang="ts">
import { Component, Prop, Mixins } from 'vue-property-decorator';
import { Dialog, Form, FormItem, Input, Col } from 'element-ui';
import { MenuData } from 'tiptap';
import { ImageNodeAttrs } from '@/extensions/image';
import i18nMixin from '@/mixins/i18nMixin';
import CommandButton from '../CommandButton.vue';
@Component({
components: {
[Dialog.name]: Dialog,
[Form.name]: Form,
[FormItem.name]: FormItem,
[Input.name]: Input,
[Col.name]: Col,
CommandButton,
},
})
export default class EditImageCommandButton extends Mixins(i18nMixin) {
@Prop({
type: Object,
required: true,
})
readonly editorContext!: MenuData;
@Prop({
type: Object,
required: true,
})
readonly initImageAttrs!: ImageNodeAttrs;
editImageDialogVisible = false;
imageAttrs = {
...this.initImageAttrs,
};
updateImageAttrs () {
const attrs: ImageNodeAttrs = {
...this.imageAttrs,
};
let { width, height } = attrs;
// Input converts it to string
// needs to be manually converted to number
// @ts-ignore
width = parseInt(width, 10);
// @ts-ignore
height = parseInt(height, 10);
attrs.width = width >= 0 ? width : null;
attrs.height = height >= 0 ? height : null;
this.editorContext.commands.update_image(attrs);
this.closeEditImageDialog();
}
private openEditImageDialog () {
this.editImageDialogVisible = true;
}
private closeEditImageDialog () {
this.editImageDialogVisible = false;
}
};
</script>
Loading

0 comments on commit 9791533

Please sign in to comment.