feat: Add support for canned response command anywhere on rich text editor (#2356)
This commit is contained in:
parent
22965be6dc
commit
2c42e70637
2 changed files with 74 additions and 11 deletions
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue