Skip to content

Instantly share code, notes, and snippets.

@fazlurr
Last active April 30, 2024 15:39
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fazlurr/5b942b608160197df12a8cd7a99f8198 to your computer and use it in GitHub Desktop.
Save fazlurr/5b942b608160197df12a8cd7a99f8198 to your computer and use it in GitHub Desktop.
tiptap alignment And custom image handler
import { Mark } from 'tiptap';
import { updateMark, markInputRule } from 'tiptap-commands';
export default class Align extends Mark {
// eslint-disable-next-line class-methods-use-this
get name() {
return 'align';
}
// eslint-disable-next-line class-methods-use-this
get schema() {
return {
attrs: {
textAlign: {
default: 'left',
},
},
parseDOM: [
{
style: 'text-align',
getAttrs: value => ({ textAlign: value }),
},
],
toDOM: mark => ['span', { style: `text-align: ${mark.attrs.textAlign};display: block` }, 0],
};
}
// eslint-disable-next-line class-methods-use-this
commands({ type }) {
return attrs => updateMark(type, attrs);
}
// eslint-disable-next-line class-methods-use-this
inputRules({ type }) {
return [
markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, type),
];
}
}
<template>
<!-- WYSIWYG Editor -->
<div class="editor mb-4">
<editor-menu-bar class="editor-bar" :editor="editor">
<div slot-scope="{ commands, isActive, focused, getMarkAttrs }">
<!-- Image -->
<label
class="btn btn-plain mb-0"
:class="{ 'is-loading': isUploading }"
v-tooltip="'Add Image'">
<i class="material-icons m-0">image</i>
<input type="file" class="hidden" @change="onImageChange(commands.image, ...arguments)">
</label>
<!-- Bold -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold"
v-tooltip="'Bold'">
<i class="material-icons">format_bold</i>
</button>
<!-- Italic -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic"
v-tooltip="'Italic'">
<i class="material-icons">format_italic</i>
</button>
<!-- Underline -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': isActive.underline() }"
@click="commands.underline"
v-tooltip="'Underline'">
<i class="material-icons">format_underline</i>
</button>
<!-- Align - Left -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': getMarkAttrs('align') && getMarkAttrs('align').textAlign === 'left' }"
@click="commands.align({ textAlign: 'left' })"
v-tooltip="'Align Left'">
<i class="material-icons">format_align_left</i>
</button>
<!-- Align - Center -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': getMarkAttrs('align') && getMarkAttrs('align').textAlign === 'center' }"
@click="commands.align({ textAlign: 'center' })"
v-tooltip="'Align Center'">
<i class="material-icons">format_align_center</i>
</button>
<!-- Algin - Right -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': getMarkAttrs('align') && getMarkAttrs('align').textAlign === 'right' }"
@click="commands.align({ textAlign: 'right' })"
v-tooltip="'Align Right'">
<i class="material-icons">format_align_right</i>
</button>
<!-- P -->
<button
type="button"
class="btn btn-plain hidden"
:class="{ 'is-active': focused && isActive.paragraph() }"
@click="commands.paragraph()"
v-tooltip="'Paragraph'">
<span class="text">P</span>
</button>
<!-- H1 -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
@click="commands.heading({ level: 1 })"
v-tooltip="'Headline 1'">
<span class="text">H1</span>
</button>
<!-- H2 -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': isActive.heading({ level: 2 }) }"
@click="commands.heading({ level: 2 })"
v-tooltip="'Headline 2'">
<span class="text">H2</span>
</button>
<!-- Bullet List -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': isActive.bullet_list() }"
@click="commands.bullet_list()"
v-tooltip="'Bullet List'">
<i class="material-icons">format_list_bulleted</i>
</button>
<!-- Bullet List -->
<button
type="button"
class="btn btn-plain"
:class="{ 'is-active': isActive.ordered_list() }"
@click="commands.ordered_list()"
v-tooltip="'Number List'">
<i class="material-icons">format_list_numbered</i>
</button>
</div>
</editor-menu-bar>
<editor-content class="editor-content" :editor="editor" />
</div>
</template>
<script>
import {
Editor,
EditorContent,
EditorMenuBar,
} from 'tiptap';
import {
// Blockquote,
// CodeBlock,
// HardBreak,
Heading,
OrderedList,
BulletList,
ListItem,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
Image,
} from 'tiptap-extensions';
import mediaApi from '@/api/media';
import Align from '@/lib/tiptap/align';
export default {
name: 'ContentEditor',
props: {
value: {
type: String,
default: '',
},
},
components: {
EditorContent,
EditorMenuBar,
},
data() {
return {
editor: null,
isUploading: false,
};
},
methods: {
initEditor() {
const content = this.value;
const extensions = [
// new Blockquote(),
// new CodeBlock(),
// new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new BulletList(),
new OrderedList(),
new ListItem(),
new TodoItem(),
new TodoList(),
new Bold(),
new Code(),
new Italic(),
new Link(),
new Strike(),
new Underline(),
new History(),
new Image(),
new Align(),
];
this.editor = new Editor({
content,
extensions,
onUpdate: this.onUpdate,
onFocus: this.onFocus,
});
},
onUpdate(editor) {
const content = editor.getHTML();
this.$emit('input', content);
},
onImageChange(command, event) {
const files = event.target.files;
if (files.length > 0) {
const file = files[0];
const uploadParams = new FormData();
uploadParams.append('image', file);
uploadParams.append('resize', false);
this.isUploading = true;
const callback = (response) => {
const path = response.data;
const originalPath = path.replace('.', '-large.');
const imageUrl = `${this.mediaUrlPrefix}/${originalPath}`;
command({ src: imageUrl });
this.isUploading = false;
};
const errorCallback = () => {
this.isUploading = false;
};
// Upload Image
mediaApi.uploadImage(uploadParams, callback, errorCallback);
}
},
insert(text) {
const { selection, state } = this.editor;
const { from, to } = selection;
const transaction = state.tr.insertText(text, from, to);
this.editor.view.dispatch(transaction);
// state.doc.textBetween(from, to, null, text);
// const oldContent = this.editor.getHTML();
// const newContent = oldContent.substring(0, from) + text + oldContent.substring(to, oldContent.length);
// this.$emit('input', newContent);
// this.editor.setContent(newContent, false);
},
onFocus() {
this.$emit('focus');
},
},
mounted() {
this.initEditor();
},
beforeDestroy() {
this.editor.destroy();
},
watch: {
// value(value) {
// this.content = value;
// if (this.editor) {
// this.editor.setContent(value, false);
// }
// },
},
};
</script>
<style lang="scss">
$color__accent: #fa962b;
.editor-bar {
margin-bottom: .5em;
.btn {
margin-right: .5em;
padding: .175rem .35rem;
min-width: 30px;
color: $color__accent;
&:hover {
color: #fff;
// background-color: #f5f5f5;
background-color: $color__accent;
}
&.is-active {
color: #fff;
// background-color: #656565;
background-color: $color__accent;
}
.material-icons {
font-size: 1.25rem;
}
}
}
.ProseMirror {
padding: 0.375rem 0.75rem;
max-height: 300px;
border-radius: 3px;
background-color: #fcfcfc;
border: 1px solid #d9dee2;
overflow: auto;
p {
margin-bottom: .5em;
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment