feat: Add support for canned response command anywhere on rich text editor (#2356)

This commit is contained in:
Nithin David Thomas 2021-06-02 10:20:40 +05:30 committed by GitHub
parent 22965be6dc
commit 2c42e70637
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 74 additions and 11 deletions

View file

@ -5,29 +5,38 @@
:search-key="mentionSearchKey" :search-key="mentionSearchKey"
@click="insertMentionNode" @click="insertMentionNode"
/> />
<canned-response
v-if="showCannedMenu"
:search-key="cannedSearchTerm"
@click="insertCannedResponse"
/>
<div ref="editor"></div> <div ref="editor"></div>
</div> </div>
</template> </template>
<script> <script>
import { EditorView } from 'prosemirror-view'; import { EditorView } from 'prosemirror-view';
import { defaultMarkdownSerializer } from 'prosemirror-markdown'; import { defaultMarkdownSerializer } from 'prosemirror-markdown';
const TYPING_INDICATOR_IDLE_TIME = 4000;
import { import {
addMentionsToMarkdownSerializer, addMentionsToMarkdownSerializer,
addMentionsToMarkdownParser, addMentionsToMarkdownParser,
schemaWithMentions, schemaWithMentions,
} from '@chatwoot/prosemirror-schema/src/mentions/schema'; } from '@chatwoot/prosemirror-schema/src/mentions/schema';
import { import {
suggestionsPlugin, suggestionsPlugin,
triggerCharacters, triggerCharacters,
} from '@chatwoot/prosemirror-schema/src/mentions/plugin'; } from '@chatwoot/prosemirror-schema/src/mentions/plugin';
import TagAgents from '../conversation/TagAgents.vue';
import { EditorState } from 'prosemirror-state'; import { EditorState } from 'prosemirror-state';
import { defaultMarkdownParser } from 'prosemirror-markdown'; import { defaultMarkdownParser } from 'prosemirror-markdown';
import { wootWriterSetup } from '@chatwoot/prosemirror-schema'; import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
import TagAgents from '../conversation/TagAgents';
import CannedResponse from '../conversation/CannedResponse';
const TYPING_INDICATOR_IDLE_TIME = 4000;
import '@chatwoot/prosemirror-schema/src/woot-editor.css'; import '@chatwoot/prosemirror-schema/src/woot-editor.css';
const createState = (content, placeholder, plugins = []) => { const createState = (content, placeholder, plugins = []) => {
@ -43,7 +52,7 @@ const createState = (content, placeholder, plugins = []) => {
export default { export default {
name: 'WootMessageEditor', name: 'WootMessageEditor',
components: { TagAgents }, components: { TagAgents, CannedResponse },
props: { props: {
value: { type: String, default: '' }, value: { type: String, default: '' },
placeholder: { type: String, default: '' }, placeholder: { type: String, default: '' },
@ -53,7 +62,9 @@ export default {
return { return {
lastValue: null, lastValue: null,
showUserMentions: false, showUserMentions: false,
showCannedMenu: false,
mentionSearchKey: '', mentionSearchKey: '',
cannedSearchTerm: '',
editorView: null, editorView: null,
range: null, range: null,
}; };
@ -86,6 +97,35 @@ export default {
return event.keyCode === 13 && this.showUserMentions; return event.keyCode === 13 && this.showUserMentions;
}, },
}), }),
suggestionsPlugin({
matcher: triggerCharacters('/'),
suggestionClass: '',
onEnter: args => {
if (this.isPrivate) {
return false;
}
this.showCannedMenu = true;
this.range = args.range;
this.editorView = args.view;
return false;
},
onChange: args => {
this.editorView = args.view;
this.range = args.range;
this.cannedSearchTerm = args.text.replace('/', '');
return false;
},
onExit: () => {
this.cannedSearchTerm = '';
this.showCannedMenu = false;
this.editorView = null;
return false;
},
onKeyDown: ({ event }) => {
return event.keyCode === 13 && this.showCannedMenu;
},
}),
]; ];
}, },
}, },
@ -93,6 +133,9 @@ export default {
showUserMentions(updatedValue) { showUserMentions(updatedValue) {
this.$emit('toggle-user-mention', this.isPrivate && updatedValue); this.$emit('toggle-user-mention', this.isPrivate && updatedValue);
}, },
showCannedMenu(updatedValue) {
this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue);
},
value(newValue) { value(newValue) {
if (newValue !== this.lastValue) { if (newValue !== this.lastValue) {
this.state = createState(newValue, this.placeholder, this.plugins); this.state = createState(newValue, this.placeholder, this.plugins);
@ -141,6 +184,21 @@ export default {
this.state = this.view.state.apply(tr); this.state = this.view.state.apply(tr);
return this.emitOnChange(); return this.emitOnChange();
}, },
insertCannedResponse(cannedItem) {
if (!this.view) {
return null;
}
const tr = this.view.state.tr.insertText(
cannedItem,
this.range.from,
this.range.to
);
this.state = this.view.state.apply(tr);
return this.emitOnChange();
},
emitOnChange() { emitOnChange() {
this.view.updateState(this.state); this.view.updateState(this.state);
this.lastValue = addMentionsToMarkdownSerializer( this.lastValue = addMentionsToMarkdownSerializer(
@ -206,10 +264,9 @@ export default {
.is-private { .is-private {
.prosemirror-mention-node { .prosemirror-mention-node {
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
background: var(--s-300); background: var(--s-50);
border-radius: var(--border-radius-small); color: var(--s-900);
padding: 1px 4px; padding: 0 var(--space-smaller);
color: var(--white);
} }
} }
</style> </style>

View file

@ -42,6 +42,7 @@
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@toggle-user-mention="toggleUserMention" @toggle-user-mention="toggleUserMention"
@toggle-canned-menu="toggleCannedMenu"
/> />
</div> </div>
<div v-if="hasAttachments" class="attachment-preview-box"> <div v-if="hasAttachments" class="attachment-preview-box">
@ -249,7 +250,8 @@ export default {
} }
}, },
message(updatedMessage) { message(updatedMessage) {
this.hasSlashCommand = updatedMessage[0] === '/'; this.hasSlashCommand =
updatedMessage[0] === '/' && !this.showRichContentEditor;
const hasNextWord = updatedMessage.includes(' '); const hasNextWord = updatedMessage.includes(' ');
const isShortCodeActive = this.hasSlashCommand && !hasNextWord; const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
if (isShortCodeActive) { if (isShortCodeActive) {
@ -271,6 +273,9 @@ export default {
toggleUserMention(currentMentionState) { toggleUserMention(currentMentionState) {
this.hasUserMention = currentMentionState; this.hasUserMention = currentMentionState;
}, },
toggleCannedMenu(value) {
this.showCannedMenu = value;
},
handleKeyEvents(e) { handleKeyEvents(e) {
if (isEscape(e)) { if (isEscape(e)) {
this.hideEmojiPicker(); this.hideEmojiPicker();
@ -279,7 +284,8 @@ export default {
const hasSendOnEnterEnabled = const hasSendOnEnterEnabled =
(this.showRichContentEditor && (this.showRichContentEditor &&
this.enterToSendEnabled && this.enterToSendEnabled &&
!this.hasUserMention) || !this.hasUserMention &&
!this.showCannedMenu) ||
!this.showRichContentEditor; !this.showRichContentEditor;
const shouldSendMessage = const shouldSendMessage =
hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused; hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused;