feat: Markdown editor support (#1657)

Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
Nithin David Thomas
2021-01-18 11:20:19 +05:30
committed by GitHub
parent 58fad2561d
commit 54f15b73d3
7 changed files with 378 additions and 87 deletions

View File

@@ -26,3 +26,4 @@
@import 'plugins/multiselect';
@import 'plugins/dropdown';
@import '@chatwoot/prosemirror-schema/src/woot-editor.css';

View File

@@ -0,0 +1,118 @@
<template>
<div ref="editor" class="editor-root"></div>
</template>
<script>
import { EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import {
schema,
defaultMarkdownParser,
defaultMarkdownSerializer,
} from 'prosemirror-markdown';
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
const TYPING_INDICATOR_IDLE_TIME = 4000;
const createState = (content, placeholder) =>
EditorState.create({
doc: defaultMarkdownParser.parse(content),
plugins: wootWriterSetup({ schema, placeholder }),
});
export default {
name: 'WootMessageEditor',
props: {
value: { type: String, default: '' },
placeholder: { type: String, default: '' },
},
data() {
return {
lastValue: null,
};
},
watch: {
value(newValue) {
if (newValue !== this.lastValue) {
this.state = createState(newValue, this.placeholder);
this.view.updateState(this.state);
}
},
},
created() {
this.state = createState(this.value, this.placeholder);
},
mounted() {
this.view = new EditorView(this.$refs.editor, {
state: this.state,
dispatchTransaction: tx => {
this.state = this.state.apply(tx);
this.view.updateState(this.state);
this.lastValue = defaultMarkdownSerializer.serialize(this.state.doc);
this.$emit('input', this.lastValue);
},
handleDOMEvents: {
keyup: () => {
this.onKeyup();
},
focus: () => {
this.onFocus();
},
blur: () => {
this.onBlur();
},
},
});
},
methods: {
resetTyping() {
this.$emit('typing-off');
this.idleTimer = null;
},
turnOffIdleTimer() {
if (this.idleTimer) {
clearTimeout(this.idleTimer);
}
},
onKeyup() {
if (!this.idleTimer) {
this.$emit('typing-on');
}
this.turnOffIdleTimer();
this.idleTimer = setTimeout(
() => this.resetTyping(),
TYPING_INDICATOR_IDLE_TIME
);
},
onBlur() {
this.turnOffIdleTimer();
this.resetTyping();
this.$emit('blur');
},
onFocus() {
this.$emit('focus');
},
},
};
</script>
<style lang="scss">
.ProseMirror-menubar-wrapper {
display: flex;
flex-direction: column;
> .ProseMirror {
padding: 0;
}
}
.editor-root {
width: 100%;
}
.ProseMirror-woot-style {
min-height: 8rem;
max-height: 12rem;
overflow: auto;
}
</style>

View File

@@ -1,12 +1,17 @@
<template>
<div class="bottom-box" :class="wrapClass">
<div class="left-wrap">
<button class="button clear button--emoji" @click="toggleEmojiPicker">
<button
class="button clear button--emoji"
:title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
@click="toggleEmojiPicker"
>
<emoji-or-icon icon="ion-happy-outline" emoji="😊" />
</button>
<button
v-if="showAttachButton"
class="button clear button--emoji button--upload"
:title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
>
<file-upload
:size="4096 * 4096"
@@ -16,6 +21,14 @@
<emoji-or-icon icon="ion-android-attach" emoji="📎" />
</file-upload>
</button>
<button
v-if="enableRichEditor"
class="button clear button--emoji"
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
@click="toggleFormatMode"
>
<emoji-or-icon icon="ion-quote" emoji="🖊️" />
</button>
</div>
<div class="right-wrap">
<button
@@ -70,6 +83,18 @@ export default {
type: Boolean,
default: false,
},
setFormatMode: {
type: Function,
default: () => {},
},
isFormatMode: {
type: Boolean,
default: false,
},
enableRichEditor: {
type: Boolean,
default: false,
},
},
computed: {
isNote() {
@@ -90,6 +115,11 @@ export default {
return this.showFileUpload || this.isNote;
},
},
methods: {
toggleFormatMode() {
this.setFormatMode(!this.isFormatMode);
},
},
};
</script>
@@ -108,34 +138,14 @@ export default {
}
.button {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
padding: var(--space-one) var(--space-slab);
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
background: var(--w-300);
}
&.is-active {
background: white;
}
&.button--emoji {
font-size: var(--font-size-small);
padding: var(--space-small);
border-radius: 9px;
background: var(--b-50);
border: 1px solid var(--color-border-light);
margin-right: var(--space-small);
&:hover {
background: var(--b-200);
}
}
&.button--note {
background: var(--y-800);
color: white;

View File

@@ -20,6 +20,7 @@
:on-click="emojiOnClick"
/>
<resizable-text-area
v-if="!isFormatMode"
ref="messageInput"
v-model="message"
class="input"
@@ -30,6 +31,17 @@
@focus="onFocus"
@blur="onBlur"
/>
<woot-message-editor
v-else
v-model="message"
class="input"
:placeholder="messagePlaceHolder"
:min-height="4"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
@blur="onBlur"
/>
</div>
<div v-if="hasAttachments" class="attachment-preview-box">
<attachment-preview
@@ -46,6 +58,9 @@
:show-emoji-picker="showEmojiPicker"
:on-send="sendMessage"
:is-send-disabled="isReplyButtonDisabled"
:set-format-mode="setFormatMode"
:is-format-mode="isFormatMode"
:enable-rich-editor="isRichEditorEnabled"
/>
</div>
</template>
@@ -61,6 +76,7 @@ import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
import {
isEscape,
isEnter,
@@ -77,6 +93,7 @@ export default {
AttachmentPreview,
ReplyTopPanel,
ReplyBottomPanel,
WootMessageEditor,
},
mixins: [clickaway, inboxMixin],
props: {
@@ -94,6 +111,7 @@ export default {
attachedFiles: [],
isUploading: false,
replyType: REPLY_EDITOR_MODES.REPLY,
isFormatMode: false,
};
},
computed: {
@@ -180,6 +198,13 @@ export default {
hasAttachments() {
return this.attachedFiles.length;
},
isRichEditorEnabled() {
return (
this.isAWebWidgetInbox ||
this.isAnEmailChannel ||
this.replyType === REPLY_EDITOR_MODES.NOTE
);
},
},
watch: {
currentChat(conversation) {
@@ -222,7 +247,9 @@ export default {
this.hideEmojiPicker();
this.hideCannedResponse();
} else if (isEnter(e)) {
if (!hasPressedShift(e)) {
const shouldSendMessage =
!this.isFormatMode && !hasPressedShift(e) && this.isFocused;
if (shouldSendMessage) {
e.preventDefault();
this.sendMessage();
}
@@ -335,6 +362,9 @@ export default {
return messagePayload;
},
setFormatMode(value) {
this.isFormatMode = value;
},
},
};
</script>

View File

@@ -41,7 +41,10 @@
"PRIVATE_NOTE": "Private Note",
"SEND": "Send",
"CREATE": "Add Note",
"TWEET": "Tweet"
"TWEET": "Tweet",
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files"
},
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
"CHANGE_STATUS": "Conversation status changed",