refactor: handling keyboard shortcuts (#9242)
* fix: Resolve and go next keyboard shortcuts doesn't work * refactor: use buildHotKeys instead of hasPressedCommandPlusAltAndEKey * feat: install tinykeys * refactor: use tinykeys * test: update buildKeyEvents * fix: remove stray import * feat: handle action list globally * feat: allow configuring `allowOnFocusedInput` * chore: Navigate chat list item * chore: Navigate dashboard * feat: Navigate editor top panel * feat: Toggle file upload * chore: More keyboard shortcuts * chore: Update mention selection mixin * chore: Phone input * chore: Clean up * chore: Clean up * chore: Dropdown and editor * chore: Enter key to send and clean up * chore: Rename mixin * chore: Review fixes * chore: Removed unused shortcut from modal * fix: Specs --------- Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -185,7 +185,7 @@ import ConversationBasicFilter from './widgets/conversation/ConversationBasicFil
|
||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||
import ConversationItem from './ConversationItem.vue';
|
||||
import timeMixin from '../mixins/time';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import conversationMixin from '../mixins/conversations';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
|
||||
@@ -199,11 +199,6 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
|
||||
import {
|
||||
hasPressedAltAndJKey,
|
||||
hasPressedAltAndKKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import { conversationListPageURL } from '../helper/URLHelper';
|
||||
import {
|
||||
isOnMentionsView,
|
||||
@@ -228,7 +223,7 @@ export default {
|
||||
mixins: [
|
||||
timeMixin,
|
||||
conversationMixin,
|
||||
eventListenerMixins,
|
||||
keyboardEventListenerMixins,
|
||||
alertMixin,
|
||||
filterMixin,
|
||||
uiSettingsMixin,
|
||||
@@ -691,30 +686,40 @@ export default {
|
||||
lastConversationIndex,
|
||||
};
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndJKey(e)) {
|
||||
const { allConversations, activeConversationIndex } =
|
||||
this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
if (activeConversationIndex >= 1) {
|
||||
allConversations[activeConversationIndex - 1].click();
|
||||
}
|
||||
handlePreviousConversation() {
|
||||
const { allConversations, activeConversationIndex } =
|
||||
this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
if (hasPressedAltAndKKey(e)) {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
}
|
||||
if (activeConversationIndex >= 1) {
|
||||
allConversations[activeConversationIndex - 1].click();
|
||||
}
|
||||
},
|
||||
handleNextConversation() {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
}
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyJ': {
|
||||
action: () => this.handlePreviousConversation(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyK': {
|
||||
action: () => this.handleNextConversation(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
resetAndFetchData() {
|
||||
this.appliedFilter = [];
|
||||
this.resetBulkActions();
|
||||
|
||||
@@ -91,12 +91,7 @@ import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
hasPressedAltAndEKey,
|
||||
hasPressedCommandPlusAltAndEKey,
|
||||
hasPressedAltAndMKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
@@ -114,7 +109,7 @@ export default {
|
||||
WootDropdownMenu,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [clickaway, alertMixin, eventListenerMixins],
|
||||
mixins: [clickaway, alertMixin, keyboardEventListenerMixins],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
data() {
|
||||
return {
|
||||
@@ -159,37 +154,52 @@ export default {
|
||||
bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
},
|
||||
methods: {
|
||||
async handleKeyEvents(e) {
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyM': {
|
||||
action: () => this.$refs.arrowDownButton?.$el.click(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyE': this.resolveOrToast,
|
||||
'$mod+Alt+KeyE': async event => {
|
||||
const { all, activeIndex, lastIndex } = this.getConversationParams();
|
||||
await this.resolveOrToast();
|
||||
|
||||
if (activeIndex < lastIndex) {
|
||||
all[activeIndex + 1].click();
|
||||
} else if (all.length > 1) {
|
||||
all[0].click();
|
||||
document.querySelector('.conversations-list').scrollTop = 0;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
};
|
||||
},
|
||||
getConversationParams() {
|
||||
const allConversations = document.querySelectorAll(
|
||||
'.conversations-list .conversation'
|
||||
);
|
||||
if (hasPressedAltAndMKey(e)) {
|
||||
if (this.$refs.arrowDownButton) {
|
||||
this.$refs.arrowDownButton.$el.click();
|
||||
}
|
||||
}
|
||||
if (hasPressedAltAndEKey(e)) {
|
||||
const activeConversation = document.querySelector(
|
||||
'div.conversations-list div.conversation.active'
|
||||
);
|
||||
const activeConversationIndex = [...allConversations].indexOf(
|
||||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
try {
|
||||
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
if (hasPressedCommandPlusAltAndEKey(e)) {
|
||||
if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
} else if (allConversations.length > 1) {
|
||||
allConversations[0].click();
|
||||
document.querySelector('.conversations-list').scrollTop = 0;
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const activeConversation = document.querySelector(
|
||||
'div.conversations-list div.conversation.active'
|
||||
);
|
||||
const activeConversationIndex = [...allConversations].indexOf(
|
||||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
|
||||
return {
|
||||
all: allConversations,
|
||||
activeIndex: activeConversationIndex,
|
||||
lastIndex: lastConversationIndex,
|
||||
};
|
||||
},
|
||||
async resolveOrToast() {
|
||||
try {
|
||||
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
},
|
||||
onCmdSnoozeConversation(snoozeType) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside class="h-full flex">
|
||||
<aside class="flex h-full">
|
||||
<primary-sidebar
|
||||
:logo-source="globalConfig.logoThumbnail"
|
||||
:installation-name="globalConfig.installationName"
|
||||
@@ -36,15 +36,7 @@ import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
||||
import {
|
||||
hasPressedAltAndCKey,
|
||||
hasPressedAltAndRKey,
|
||||
hasPressedAltAndSKey,
|
||||
hasPressedAltAndVKey,
|
||||
hasPressedCommandAndForwardSlash,
|
||||
isEscape,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import router from '../../routes';
|
||||
|
||||
export default {
|
||||
@@ -52,7 +44,7 @@ export default {
|
||||
PrimarySidebar,
|
||||
SecondarySidebar,
|
||||
},
|
||||
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
||||
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
showSecondarySidebar: {
|
||||
type: Boolean,
|
||||
@@ -173,30 +165,27 @@ export default {
|
||||
closeKeyShortcutModal() {
|
||||
this.$emit('close-key-shortcut-modal');
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedCommandAndForwardSlash(e)) {
|
||||
this.toggleKeyShortcutModal();
|
||||
}
|
||||
if (isEscape(e)) {
|
||||
this.closeKeyShortcutModal();
|
||||
}
|
||||
|
||||
if (hasPressedAltAndCKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('home')) {
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
} else if (hasPressedAltAndVKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('contacts_dashboard')) {
|
||||
router.push({ name: 'contacts_dashboard' });
|
||||
}
|
||||
} else if (hasPressedAltAndRKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('settings_account_reports')) {
|
||||
router.push({ name: 'settings_account_reports' });
|
||||
}
|
||||
} else if (hasPressedAltAndSKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('agent_list')) {
|
||||
router.push({ name: 'agent_list' });
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+Slash': this.toggleKeyShortcutModal,
|
||||
'$mod+Escape': this.closeKeyShortcutModal,
|
||||
'Alt+KeyC': {
|
||||
action: () => this.navigateToRoute('home'),
|
||||
},
|
||||
'Alt+KeyV': {
|
||||
action: () => this.navigateToRoute('contacts_dashboard'),
|
||||
},
|
||||
'Alt+KeyR': {
|
||||
action: () => this.navigateToRoute('account_overview_reports'),
|
||||
},
|
||||
'Alt+KeyS': {
|
||||
action: () => this.navigateToRoute('agent_list'),
|
||||
},
|
||||
};
|
||||
},
|
||||
navigateToRoute(routeName) {
|
||||
if (!this.isCurrentRouteSameAsNavigation(routeName)) {
|
||||
router.push({ name: routeName });
|
||||
}
|
||||
},
|
||||
isCurrentRouteSameAsNavigation(routeName) {
|
||||
|
||||
@@ -40,8 +40,7 @@ import AIAssistanceModal from './AIAssistanceModal.vue';
|
||||
import adminMixin from 'dashboard/mixins/aiMixin';
|
||||
import aiMixin from 'dashboard/mixins/isAdmin';
|
||||
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
||||
|
||||
@@ -51,7 +50,7 @@ export default {
|
||||
AICTAModal,
|
||||
AIAssistanceCTAButton,
|
||||
},
|
||||
mixins: [aiMixin, eventListenerMixins, adminMixin, uiSettingsMixin],
|
||||
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
|
||||
data: () => ({
|
||||
showAIAssistanceModal: false,
|
||||
showAICtaModal: false,
|
||||
@@ -87,14 +86,17 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDownHandler(event) {
|
||||
const keyPattern = buildHotKeys(event);
|
||||
const shouldRevertTheContent =
|
||||
['meta+z', 'ctrl+z'].includes(keyPattern) && !!this.initialMessage;
|
||||
if (shouldRevertTheContent) {
|
||||
this.$emit('replace-text', this.initialMessage);
|
||||
this.initialMessage = '';
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+KeyZ': {
|
||||
action: () => {
|
||||
if (this.initialMessage) {
|
||||
this.$emit('replace-text', this.initialMessage);
|
||||
this.initialMessage = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
hideAIAssistanceModal() {
|
||||
this.recordAnalytics('DISMISS_AI_SUGGESTION', {
|
||||
|
||||
@@ -10,11 +10,10 @@
|
||||
</template>
|
||||
<script>
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@@ -31,14 +30,18 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndNKey(e)) {
|
||||
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
this.onTabChange(0);
|
||||
} else {
|
||||
this.onTabChange(this.activeTabIndex + 1);
|
||||
}
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyN': {
|
||||
action: () => {
|
||||
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
this.onTabChange(0);
|
||||
} else {
|
||||
this.onTabChange(this.activeTabIndex + 1);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
onTabChange(selectedTabIndex) {
|
||||
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
||||
|
||||
@@ -31,15 +31,10 @@
|
||||
|
||||
<script>
|
||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import {
|
||||
buildHotKeys,
|
||||
isEscape,
|
||||
isActiveElementTypeable,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -47,7 +42,7 @@ export default {
|
||||
LabelDropdown,
|
||||
},
|
||||
|
||||
mixins: [clickaway, adminMixin, eventListenerMixins],
|
||||
mixins: [clickaway, adminMixin, keyboardEventListenerMixins],
|
||||
|
||||
props: {
|
||||
allLabels: {
|
||||
@@ -88,17 +83,19 @@ export default {
|
||||
closeDropdownLabel() {
|
||||
this.showSearchDropdownLabel = false;
|
||||
},
|
||||
|
||||
handleKeyEvents(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
|
||||
if (keyPattern === 'l' && !isActiveElementTypeable(e)) {
|
||||
this.toggleLabels();
|
||||
e.preventDefault();
|
||||
} else if (isEscape(e) && this.showSearchDropdownLabel) {
|
||||
this.closeDropdownLabel();
|
||||
e.preventDefault();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
KeyL: {
|
||||
action: e => {
|
||||
this.toggleLabels();
|
||||
e.preventDefault();
|
||||
},
|
||||
},
|
||||
Escape: {
|
||||
action: () => this.closeDropdownLabel(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -78,10 +78,8 @@ const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
import {
|
||||
hasPressedEnterAndNotCmdOrShift,
|
||||
hasPressedCommandAndEnter,
|
||||
hasPressedAltAndPKey,
|
||||
hasPressedAltAndLKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||
import {
|
||||
@@ -121,7 +119,7 @@ const createState = (
|
||||
export default {
|
||||
name: 'WootMessageEditor',
|
||||
components: { TagAgents, CannedResponse, VariableList },
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
editorId: { type: String, default: '' },
|
||||
@@ -522,13 +520,21 @@ export default {
|
||||
isCmdPlusEnterToSendEnabled() {
|
||||
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndPKey(e)) {
|
||||
this.focusEditorInputField();
|
||||
}
|
||||
if (hasPressedAltAndLKey(e)) {
|
||||
this.focusEditorInputField();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyP': {
|
||||
action: () => {
|
||||
this.focusEditorInputField();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyL': {
|
||||
action: () => {
|
||||
this.focusEditorInputField();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
focusEditorInputField(pos = 'end') {
|
||||
const { tr } = this.editorView.state;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
@@ -51,7 +51,7 @@ const createState = (
|
||||
};
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
editorId: { type: String, default: '' },
|
||||
|
||||
@@ -136,8 +136,7 @@
|
||||
<script>
|
||||
import FileUpload from 'vue-upload-component';
|
||||
import * as ActiveStorage from 'activestorage';
|
||||
import { hasPressedAltAndAKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
@@ -154,7 +153,7 @@ import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
name: 'ReplyBottomPanel',
|
||||
components: { FileUpload, VideoCallButton, AIAssistanceButton },
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -340,10 +339,15 @@ export default {
|
||||
ActiveStorage.start();
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndAKey(e)) {
|
||||
this.$refs.upload.$children[1].$el.click();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyA': {
|
||||
action: () => {
|
||||
this.$refs.upload.$children[1].$el.click();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
toggleMessageSignature() {
|
||||
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-black-50 flex justify-between dark:bg-slate-800">
|
||||
<div class="flex justify-between bg-black-50 dark:bg-slate-800">
|
||||
<div class="button-group">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
@@ -20,7 +20,7 @@
|
||||
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="flex items-center my-0 mx-4">
|
||||
<div class="flex items-center mx-4 my-0">
|
||||
<div v-if="isMessageLengthReachingThreshold" class="text-xs">
|
||||
<span :class="charLengthClass">
|
||||
{{ characterLengthWarning }}
|
||||
@@ -48,14 +48,10 @@
|
||||
|
||||
<script>
|
||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||
import {
|
||||
hasPressedAltAndPKey,
|
||||
hasPressedAltAndLKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -99,13 +95,17 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndPKey(e)) {
|
||||
this.handleNoteClick();
|
||||
}
|
||||
if (hasPressedAltAndLKey(e)) {
|
||||
this.handleReplyClick();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyP': {
|
||||
action: () => this.handleNoteClick(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyL': {
|
||||
action: () => this.handleReplyClick(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
handleReplyClick() {
|
||||
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-slate-25 dark:hover:bg-slate-800 group"
|
||||
:class="{
|
||||
'animate-card-select bg-slate-25 dark:bg-slate-800 border-woot-500':
|
||||
'active animate-card-select bg-slate-25 dark:bg-slate-800 border-woot-500':
|
||||
isActiveChat,
|
||||
'unread-chat': hasUnread,
|
||||
'has-inbox-name': showInboxName,
|
||||
|
||||
@@ -77,11 +77,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import { mapGetters } from 'vuex';
|
||||
import agentMixin from '../../../mixins/agentMixin.js';
|
||||
import BackButton from '../BackButton.vue';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import InboxName from '../InboxName.vue';
|
||||
import MoreActions from './MoreActions.vue';
|
||||
@@ -99,7 +98,7 @@ export default {
|
||||
Thumbnail,
|
||||
SLACardLabel,
|
||||
},
|
||||
mixins: [inboxMixin, agentMixin, eventListenerMixins],
|
||||
mixins: [inboxMixin, agentMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
@@ -182,10 +181,12 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndOKey(e)) {
|
||||
this.$emit('contact-panel-toggle');
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyO': {
|
||||
action: () => this.$emit('contact-panel-toggle'),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -116,13 +116,12 @@ import conversationMixin, {
|
||||
} from '../../../mixins/conversations';
|
||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
|
||||
// utils
|
||||
import { getTypingUsersText } from '../../../helper/commons';
|
||||
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
|
||||
import { isEscape } from 'shared/helpers/KeyboardHelpers';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
// constants
|
||||
@@ -141,7 +140,7 @@ export default {
|
||||
mixins: [
|
||||
conversationMixin,
|
||||
inboxMixin,
|
||||
eventListenerMixins,
|
||||
keyboardEventListenerMixins,
|
||||
configMixin,
|
||||
aiMixin,
|
||||
],
|
||||
@@ -418,10 +417,12 @@ export default {
|
||||
closePopoutReplyBox() {
|
||||
this.isPopoutReplyBox = false;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (isEscape(e)) {
|
||||
this.closePopoutReplyBox();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => this.closePopoutReplyBox(),
|
||||
},
|
||||
};
|
||||
},
|
||||
addScrollListener() {
|
||||
this.conversationPanel = this.$el.querySelector('.conversation-panel');
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
import CannedResponse from './CannedResponse.vue';
|
||||
import ReplyToMessage from './ReplyToMessage.vue';
|
||||
@@ -178,7 +179,6 @@ import {
|
||||
replaceVariablesInMessage,
|
||||
} from '@chatwoot/utils';
|
||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
@@ -225,6 +225,7 @@ export default {
|
||||
messageFormatterMixin,
|
||||
rtlMixin,
|
||||
fileUploadMixin,
|
||||
keyboardEventListenerMixins,
|
||||
],
|
||||
props: {
|
||||
popoutReplyBox: {
|
||||
@@ -701,24 +702,41 @@ export default {
|
||||
this.$store.dispatch('draftMessages/delete', { key });
|
||||
}
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
const keyCode = buildHotKeys(e);
|
||||
if (keyCode === 'escape') {
|
||||
this.hideEmojiPicker();
|
||||
this.hideMentions();
|
||||
} else if (keyCode === 'meta+k') {
|
||||
const ninja = document.querySelector('ninja-keys');
|
||||
ninja.open();
|
||||
e.preventDefault();
|
||||
} else if (keyCode === 'enter' && this.isAValidEvent('enter')) {
|
||||
this.onSendReply();
|
||||
e.preventDefault();
|
||||
} else if (
|
||||
['meta+enter', 'ctrl+enter'].includes(keyCode) &&
|
||||
this.isAValidEvent('cmd_enter')
|
||||
) {
|
||||
this.onSendReply();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => {
|
||||
this.hideEmojiPicker();
|
||||
this.hideMentions();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'$mod+KeyK': {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
const ninja = document.querySelector('ninja-keys');
|
||||
ninja.open();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
Enter: {
|
||||
action: e => {
|
||||
if (this.isAValidEvent('enter')) {
|
||||
this.onSendReply();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'$mod+Enter': {
|
||||
action: () => {
|
||||
if (this.isAValidEvent('cmd_enter')) {
|
||||
this.onSendReply();
|
||||
}
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
isAValidEvent(selectedKey) {
|
||||
return (
|
||||
|
||||
@@ -75,9 +75,10 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
this.$el.scrollTop = 50 * this.selectedIndex;
|
||||
adjustScroll() {
|
||||
this.$nextTick(() => {
|
||||
this.$el.scrollTop = 50 * this.selectedIndex;
|
||||
});
|
||||
},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
|
||||
@@ -182,12 +182,7 @@
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
isEscape,
|
||||
hasPressedArrowLeftKey,
|
||||
hasPressedArrowRightKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
@@ -205,7 +200,7 @@ export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [eventListenerMixins, clickaway, timeMixin],
|
||||
mixins: [keyboardEventListenerMixins, clickaway, timeMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -304,20 +299,30 @@ export default {
|
||||
this.activeAttachment = attachment;
|
||||
this.activeFileType = type;
|
||||
},
|
||||
onKeyDownHandler(e) {
|
||||
if (isEscape(e)) {
|
||||
this.onClose();
|
||||
} else if (hasPressedArrowLeftKey(e)) {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex - 1],
|
||||
this.activeImageIndex - 1
|
||||
);
|
||||
} else if (hasPressedArrowRightKey(e)) {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex + 1],
|
||||
this.activeImageIndex + 1
|
||||
);
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => {
|
||||
this.onClose();
|
||||
},
|
||||
},
|
||||
ArrowLeft: {
|
||||
action: () => {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex - 1],
|
||||
this.activeImageIndex - 1
|
||||
);
|
||||
},
|
||||
},
|
||||
ArrowRight: {
|
||||
action: () => {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex + 1],
|
||||
this.activeImageIndex + 1
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
onClickDownload() {
|
||||
const { file_type: type, data_url: url } = this.activeAttachment;
|
||||
|
||||
@@ -74,15 +74,10 @@
|
||||
<script>
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import parsePhoneNumber from 'libphonenumber-js';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
hasPressedArrowUpKey,
|
||||
hasPressedArrowDownKey,
|
||||
isEnter,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
@@ -183,6 +178,7 @@ export default {
|
||||
this.$emit('blur', e.target.value);
|
||||
},
|
||||
dropdownItem() {
|
||||
if (!this.showDropdown) return [];
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll(
|
||||
'div.country-dropdown div.country-dropdown--item'
|
||||
@@ -190,34 +186,27 @@ export default {
|
||||
);
|
||||
},
|
||||
focusedItem() {
|
||||
if (!this.showDropdown) return [];
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll('div.country-dropdown div.focus')
|
||||
);
|
||||
},
|
||||
focusedItemIndex() {
|
||||
if (!this.showDropdown) return -1;
|
||||
return Array.from(this.dropdownItem()).indexOf(this.focusedItem()[0]);
|
||||
},
|
||||
onKeyDownHandler(e) {
|
||||
const { showDropdown, filteredCountriesBySearch, onSelectCountry } = this;
|
||||
const { selectedIndex } = this;
|
||||
|
||||
if (showDropdown) {
|
||||
if (hasPressedArrowDownKey(e)) {
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.min(
|
||||
selectedIndex + 1,
|
||||
filteredCountriesBySearch.length - 1
|
||||
);
|
||||
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28;
|
||||
} else if (hasPressedArrowUpKey(e)) {
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
|
||||
} else if (isEnter(e)) {
|
||||
e.preventDefault();
|
||||
onSelectCountry(filteredCountriesBySearch[selectedIndex]);
|
||||
}
|
||||
}
|
||||
moveUp() {
|
||||
if (!this.showDropdown) return;
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
|
||||
},
|
||||
moveDown() {
|
||||
if (!this.showDropdown) return;
|
||||
this.selectedIndex = Math.min(
|
||||
this.selectedIndex + 1,
|
||||
this.filteredCountriesBySearch.length - 1
|
||||
);
|
||||
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
|
||||
},
|
||||
onSelectCountry(country) {
|
||||
this.activeCountryCode = country.id;
|
||||
@@ -235,6 +224,33 @@ export default {
|
||||
this.activeDialCode = number.countryCallingCode;
|
||||
}
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
ArrowUp: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.moveUp();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
ArrowDown: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.moveDown();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
Enter: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.onSelectCountry(
|
||||
this.filteredCountriesBySearch[this.selectedIndex]
|
||||
);
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
toggleCountryDropdown() {
|
||||
this.showDropdown = !this.showDropdown;
|
||||
this.selectedIndex = -1;
|
||||
|
||||
@@ -84,9 +84,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
},
|
||||
adjustScroll() {},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
},
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.handleKeyboardEvent);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.handleKeyboardEvent);
|
||||
},
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
methods: {
|
||||
moveSelectionUp() {
|
||||
if (!this.selectedIndex) {
|
||||
@@ -14,6 +9,7 @@ export default {
|
||||
} else {
|
||||
this.selectedIndex -= 1;
|
||||
}
|
||||
this.adjustScroll();
|
||||
},
|
||||
moveSelectionDown() {
|
||||
if (this.selectedIndex === this.items.length - 1) {
|
||||
@@ -21,19 +17,46 @@ export default {
|
||||
} else {
|
||||
this.selectedIndex += 1;
|
||||
}
|
||||
this.adjustScroll();
|
||||
},
|
||||
processKeyDownEvent(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
if (['arrowup', 'ctrl+p'].includes(keyPattern)) {
|
||||
this.moveSelectionUp();
|
||||
e.preventDefault();
|
||||
} else if (['arrowdown', 'ctrl+n'].includes(keyPattern)) {
|
||||
this.moveSelectionDown();
|
||||
e.preventDefault();
|
||||
} else if (keyPattern === 'enter') {
|
||||
this.onSelect();
|
||||
e.preventDefault();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
ArrowUp: {
|
||||
action: e => {
|
||||
this.moveSelectionUp();
|
||||
e.preventDefault();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Control+KeyP': {
|
||||
action: e => {
|
||||
this.moveSelectionUp();
|
||||
e.preventDefault();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
ArrowDown: {
|
||||
action: e => {
|
||||
this.moveSelectionDown();
|
||||
e.preventDefault();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Control+KeyN': {
|
||||
action: e => {
|
||||
this.moveSelectionDown();
|
||||
e.preventDefault();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
Enter: {
|
||||
action: e => {
|
||||
this.onSelect();
|
||||
e.preventDefault();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,64 +1,69 @@
|
||||
import mentionSelectionKeyboardMixin from '../mentionSelectionKeyboardMixin';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
const buildComponent = ({ data = {}, methods = {} }) => ({
|
||||
render() {},
|
||||
data() {
|
||||
return data;
|
||||
return { ...data, selectedIndex: 0, items: [1, 2, 3] };
|
||||
},
|
||||
methods,
|
||||
mixins: [mentionSelectionKeyboardMixin],
|
||||
methods: { ...methods, onSelect: jest.fn(), adjustScroll: jest.fn() },
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
});
|
||||
|
||||
describe('mentionSelectionKeyboardMixin', () => {
|
||||
test('register listeners', () => {
|
||||
jest.spyOn(document, 'addEventListener');
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const Component = buildComponent({});
|
||||
shallowMount(Component);
|
||||
// undefined expected as the method is not defined in the component
|
||||
expect(document.addEventListener).toHaveBeenCalledWith(
|
||||
'keydown',
|
||||
undefined
|
||||
);
|
||||
wrapper = shallowMount(Component, { localVue });
|
||||
});
|
||||
|
||||
test('processKeyDownEvent updates index on arrow up', () => {
|
||||
const Component = buildComponent({
|
||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
wrapper.vm.processKeyDownEvent({
|
||||
ctrlKey: true,
|
||||
key: 'p',
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(wrapper.vm.selectedIndex).toBe(2);
|
||||
it('ArrowUp and Control+KeyP update selectedIndex correctly', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||
|
||||
if (keyboardEvents && keyboardEvents.ArrowUp) {
|
||||
keyboardEvents.ArrowUp.action({ preventDefault });
|
||||
expect(wrapper.vm.selectedIndex).toBe(2);
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
wrapper.setData({ selectedIndex: 1 });
|
||||
if (keyboardEvents && keyboardEvents['Control+KeyP']) {
|
||||
keyboardEvents['Control+KeyP'].action({ preventDefault });
|
||||
expect(wrapper.vm.selectedIndex).toBe(0);
|
||||
expect(preventDefault).toHaveBeenCalledTimes(2);
|
||||
}
|
||||
});
|
||||
|
||||
test('processKeyDownEvent updates index on arrow down', () => {
|
||||
const Component = buildComponent({
|
||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
wrapper.vm.processKeyDownEvent({
|
||||
key: 'ArrowDown',
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(wrapper.vm.selectedIndex).toBe(1);
|
||||
it('ArrowDown and Control+KeyN update selectedIndex correctly', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||
|
||||
if (keyboardEvents && keyboardEvents.ArrowDown) {
|
||||
keyboardEvents.ArrowDown.action({ preventDefault });
|
||||
expect(wrapper.vm.selectedIndex).toBe(1);
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
wrapper.setData({ selectedIndex: 1 });
|
||||
if (keyboardEvents && keyboardEvents['Control+KeyN']) {
|
||||
keyboardEvents['Control+KeyN'].action({ preventDefault });
|
||||
expect(wrapper.vm.selectedIndex).toBe(2);
|
||||
expect(preventDefault).toHaveBeenCalledTimes(2);
|
||||
}
|
||||
});
|
||||
|
||||
test('processKeyDownEvent calls select methods on Enter Key', () => {
|
||||
const onSelectMockFn = jest.fn();
|
||||
const Component = buildComponent({
|
||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
||||
methods: { onSelect: () => onSelectMockFn('enterKey pressed') },
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
wrapper.vm.processKeyDownEvent({
|
||||
key: 'Enter',
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(onSelectMockFn).toHaveBeenCalledWith('enterKey pressed');
|
||||
wrapper.vm.onSelect();
|
||||
it('Enter key triggers onSelect method', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||
|
||||
if (keyboardEvents && keyboardEvents.Enter) {
|
||||
keyboardEvents.Enter.action({ preventDefault });
|
||||
expect(wrapper.vm.onSelect).toHaveBeenCalled();
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,12 +53,6 @@ export const SHORTCUT_KEYS = [
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'S',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
label: 'SWITCH_CONVERSATION_STATUS',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'B',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
label: 'SWITCH_TO_PRIVATE_NOTE',
|
||||
|
||||
@@ -316,7 +316,6 @@
|
||||
"GO_TO_REPORTS_SIDEBAR": "Go to Reports sidebar",
|
||||
"MOVE_TO_NEXT_TAB": "Move to next tab in conversation list",
|
||||
"GO_TO_SETTINGS": "Go to Settings",
|
||||
"SWITCH_CONVERSATION_STATUS": "Switch to the next conversation status",
|
||||
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
|
||||
"SWITCH_TO_REPLY": "Switch to Reply",
|
||||
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
|
||||
<script>
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import { hasPressedCommandAndEnter } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
export default {
|
||||
components: {
|
||||
WootMessageEditor,
|
||||
},
|
||||
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
data() {
|
||||
return {
|
||||
noteContent: '',
|
||||
@@ -40,21 +40,14 @@ export default {
|
||||
return this.noteContent === '';
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onMetaEnter);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onMetaEnter);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onMetaEnter(e) {
|
||||
if (hasPressedCommandAndEnter(e)) {
|
||||
e.preventDefault();
|
||||
this.onAdd();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+Enter': {
|
||||
action: () => this.onAdd(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
onAdd() {
|
||||
if (this.noteContent !== '') {
|
||||
|
||||
@@ -50,13 +50,8 @@ import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
import {
|
||||
buildHotKeys,
|
||||
isEscape,
|
||||
isActiveElementTypeable,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -65,7 +60,12 @@ export default {
|
||||
AddLabel,
|
||||
},
|
||||
|
||||
mixins: [clickaway, conversationLabelMixin, adminMixin, eventListenerMixins],
|
||||
mixins: [
|
||||
clickaway,
|
||||
conversationLabelMixin,
|
||||
adminMixin,
|
||||
keyboardEventListenerMixins,
|
||||
],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: Number,
|
||||
@@ -93,16 +93,23 @@ export default {
|
||||
closeDropdownLabel() {
|
||||
this.showSearchDropdownLabel = false;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
|
||||
if (keyPattern === 'l' && !isActiveElementTypeable(e)) {
|
||||
this.toggleLabels();
|
||||
e.preventDefault();
|
||||
} else if (isEscape(e) && this.showSearchDropdownLabel) {
|
||||
this.closeDropdownLabel();
|
||||
e.preventDefault();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
KeyL: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.toggleLabels();
|
||||
},
|
||||
},
|
||||
Escape: {
|
||||
action: () => {
|
||||
if (this.showSearchDropdownLabel) {
|
||||
this.toggleLabels();
|
||||
}
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,15 +34,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
buildHotKeys,
|
||||
isActiveElementTypeable,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
name: 'ChatwootSearch',
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
@@ -71,13 +67,15 @@ export default {
|
||||
onBlur() {
|
||||
this.isInputFocused = false;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
|
||||
if (keyPattern === '/' && !isActiveElementTypeable(e)) {
|
||||
e.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Slash: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -35,10 +35,7 @@
|
||||
<script>
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import {
|
||||
isEscape,
|
||||
isActiveElementTypeable,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
import SearchHeader from './Header.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
@@ -55,7 +52,7 @@ export default {
|
||||
SearchResults,
|
||||
ArticleView,
|
||||
},
|
||||
mixins: [clickaway, portalMixin, alertMixin],
|
||||
mixins: [clickaway, portalMixin, alertMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
selectedPortalSlug: {
|
||||
type: String,
|
||||
@@ -97,10 +94,6 @@ export default {
|
||||
mounted() {
|
||||
this.fetchArticlesByQuery(this.searchQuery);
|
||||
this.debounceSearch = debounce(this.fetchArticlesByQuery, 500, false);
|
||||
document.body.addEventListener('keydown', this.closeOnEsc);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.removeEventListener('keydown', this.closeOnEsc);
|
||||
},
|
||||
methods: {
|
||||
generateArticleUrl(article) {
|
||||
@@ -158,11 +151,15 @@ export default {
|
||||
);
|
||||
this.onClose();
|
||||
},
|
||||
closeOnEsc(e) {
|
||||
if (isEscape(e) && !isActiveElementTypeable(e)) {
|
||||
e.preventDefault();
|
||||
this.onClose();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => {
|
||||
this.onClose();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -109,9 +109,10 @@ export default {
|
||||
generateArticleUrl(article) {
|
||||
return `/hc/${article.portal.slug}/articles/${article.slug}`;
|
||||
},
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
this.$el.scrollTop = 102 * this.selectedIndex;
|
||||
adjustScroll() {
|
||||
this.$nextTick(() => {
|
||||
this.$el.scrollTop = 102 * this.selectedIndex;
|
||||
});
|
||||
},
|
||||
prepareContent(content) {
|
||||
return this.highlightContent(
|
||||
|
||||
@@ -8,16 +8,12 @@
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
hasPressedArrowUpKey,
|
||||
hasPressedArrowDownKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
export default {
|
||||
name: 'WootDropdownMenu',
|
||||
componentName: 'WootDropdownMenu',
|
||||
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
|
||||
props: {
|
||||
placement: {
|
||||
@@ -31,38 +27,46 @@ export default {
|
||||
'ul.dropdown li.dropdown-menu__item .button'
|
||||
);
|
||||
},
|
||||
activeElementIndex() {
|
||||
const menuButtons = this.dropdownMenuButtons();
|
||||
getActiveButtonIndex(menuButtons) {
|
||||
const focusedButton = this.$refs.dropdownMenu.querySelector(
|
||||
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||
);
|
||||
const activeIndex = [...menuButtons].indexOf(focusedButton);
|
||||
return activeIndex;
|
||||
return Array.from(menuButtons).indexOf(focusedButton);
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
getKeyboardEvents() {
|
||||
const menuButtons = this.dropdownMenuButtons();
|
||||
const lastElementIndex = menuButtons.length - 1;
|
||||
|
||||
return {
|
||||
ArrowUp: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.focusPreviousButton(menuButtons);
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
ArrowDown: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.focusNextButton(menuButtons);
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
focusPreviousButton(menuButtons) {
|
||||
const activeIndex = this.getActiveButtonIndex(menuButtons);
|
||||
const newIndex =
|
||||
activeIndex >= 1 ? activeIndex - 1 : menuButtons.length - 1;
|
||||
this.focusButton(menuButtons, newIndex);
|
||||
},
|
||||
focusNextButton(menuButtons) {
|
||||
const activeIndex = this.getActiveButtonIndex(menuButtons);
|
||||
const newIndex =
|
||||
activeIndex === menuButtons.length - 1 ? 0 : activeIndex + 1;
|
||||
this.focusButton(menuButtons, newIndex);
|
||||
},
|
||||
focusButton(menuButtons, newIndex) {
|
||||
if (menuButtons.length === 0) return;
|
||||
|
||||
if (hasPressedArrowUpKey(e)) {
|
||||
const activeIndex = this.activeElementIndex();
|
||||
|
||||
if (activeIndex >= 1) {
|
||||
menuButtons[activeIndex - 1].focus();
|
||||
} else {
|
||||
menuButtons[lastElementIndex].focus();
|
||||
}
|
||||
}
|
||||
if (hasPressedArrowDownKey(e)) {
|
||||
const activeIndex = this.activeElementIndex();
|
||||
|
||||
if (activeIndex === lastElementIndex) {
|
||||
menuButtons[0].focus();
|
||||
} else {
|
||||
menuButtons[activeIndex + 1].focus();
|
||||
}
|
||||
}
|
||||
menuButtons[newIndex].focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export const isEnter = e => {
|
||||
return e.keyCode === 13;
|
||||
return e.key === 'Enter';
|
||||
};
|
||||
|
||||
export const isEscape = e => {
|
||||
return e.keyCode === 27;
|
||||
return e.key === 'Escape';
|
||||
};
|
||||
|
||||
export const hasPressedShift = e => {
|
||||
@@ -19,118 +19,7 @@ export const hasPressedEnterAndNotCmdOrShift = e => {
|
||||
};
|
||||
|
||||
export const hasPressedCommandAndEnter = e => {
|
||||
return e.metaKey && e.keyCode === 13;
|
||||
};
|
||||
|
||||
export const hasPressedCommandAndForwardSlash = e => {
|
||||
return e.metaKey && e.keyCode === 191;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndCKey = e => {
|
||||
return e.altKey && e.keyCode === 67;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndVKey = e => {
|
||||
return e.altKey && e.keyCode === 86;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndRKey = e => {
|
||||
return e.altKey && e.keyCode === 82;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndSKey = e => {
|
||||
return e.altKey && e.keyCode === 83;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndBKey = e => {
|
||||
return e.altKey && e.keyCode === 66;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndNKey = e => {
|
||||
return e.altKey && e.keyCode === 78;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndAKey = e => {
|
||||
return e.altKey && e.keyCode === 65;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndPKey = e => {
|
||||
return e.altKey && e.keyCode === 80;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndLKey = e => {
|
||||
return e.altKey && e.keyCode === 76;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndEKey = e => {
|
||||
return e.altKey && e.keyCode === 69;
|
||||
};
|
||||
|
||||
export const hasPressedCommandPlusAltAndEKey = e => {
|
||||
return e.metaKey && e.altKey && e.keyCode === 69;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndOKey = e => {
|
||||
return e.altKey && e.keyCode === 79;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndJKey = e => {
|
||||
return e.altKey && e.keyCode === 74;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndKKey = e => {
|
||||
return e.altKey && e.keyCode === 75;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndMKey = e => {
|
||||
return e.altKey && e.keyCode === 77;
|
||||
};
|
||||
|
||||
export const hasPressedArrowUpKey = e => {
|
||||
return e.keyCode === 38;
|
||||
};
|
||||
|
||||
export const hasPressedArrowDownKey = e => {
|
||||
return e.keyCode === 40;
|
||||
};
|
||||
|
||||
export const hasPressedArrowLeftKey = e => {
|
||||
return e.keyCode === 37;
|
||||
};
|
||||
|
||||
export const hasPressedArrowRightKey = e => {
|
||||
return e.keyCode === 39;
|
||||
};
|
||||
|
||||
export const hasPressedCommandPlusKKey = e => {
|
||||
return e.metaKey && e.keyCode === 75;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a string representation of the hotkey pattern based on the provided event object.
|
||||
* @param {KeyboardEvent} e - The keyboard event object.
|
||||
* @returns {string} - The hotkey pattern string.
|
||||
*/
|
||||
export const buildHotKeys = e => {
|
||||
const key = e.key.toLowerCase();
|
||||
if (['shift', 'meta', 'alt', 'control'].includes(key)) {
|
||||
return key;
|
||||
}
|
||||
let hotKeyPattern = '';
|
||||
if (e.altKey) {
|
||||
hotKeyPattern += 'alt+';
|
||||
}
|
||||
if (e.ctrlKey) {
|
||||
hotKeyPattern += 'ctrl+';
|
||||
}
|
||||
if (e.metaKey && !e.ctrlKey) {
|
||||
hotKeyPattern += 'meta+';
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
hotKeyPattern += 'shift+';
|
||||
}
|
||||
hotKeyPattern += key;
|
||||
return hotKeyPattern;
|
||||
return hasPressedCommand(e) && isEnter(e);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,20 +3,19 @@ import {
|
||||
isEscape,
|
||||
hasPressedShift,
|
||||
hasPressedCommand,
|
||||
buildHotKeys,
|
||||
isActiveElementTypeable,
|
||||
} from '../KeyboardHelpers';
|
||||
|
||||
describe('#KeyboardHelpers', () => {
|
||||
describe('#isEnter', () => {
|
||||
it('return correct values', () => {
|
||||
expect(isEnter({ keyCode: 13 })).toEqual(true);
|
||||
expect(isEnter({ key: 'Enter' })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEscape', () => {
|
||||
it('return correct values', () => {
|
||||
expect(isEscape({ keyCode: 27 })).toEqual(true);
|
||||
expect(isEscape({ key: 'Escape' })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,14 +30,6 @@ describe('#KeyboardHelpers', () => {
|
||||
expect(hasPressedCommand({ metaKey: true })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#buildHotKeys', () => {
|
||||
it('returns hot keys', () => {
|
||||
expect(buildHotKeys({ altKey: true, key: 'alt' })).toEqual('alt');
|
||||
expect(buildHotKeys({ ctrlKey: true, key: 'a' })).toEqual('ctrl+a');
|
||||
expect(buildHotKeys({ metaKey: true, key: 'b' })).toEqual('meta+b');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActiveElementTypeable', () => {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { isActiveElementTypeable, isEscape } from '../helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onKeyDownHandler);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onKeyDownHandler);
|
||||
},
|
||||
methods: {
|
||||
onKeyDownHandler(e) {
|
||||
const isTypeable = isActiveElementTypeable(e);
|
||||
|
||||
if (isTypeable) {
|
||||
if (isEscape(e)) {
|
||||
e.target.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleKeyEvents(e);
|
||||
},
|
||||
},
|
||||
};
|
||||
63
app/javascript/shared/mixins/keyboardEventListenerMixins.js
Normal file
63
app/javascript/shared/mixins/keyboardEventListenerMixins.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { isActiveElementTypeable, isEscape } from '../helpers/KeyboardHelpers';
|
||||
|
||||
import { createKeybindingsHandler } from 'tinykeys';
|
||||
|
||||
// this is a store that stores the handler globally, and only gets reset on reload
|
||||
const taggedHandlers = [];
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
const events = this.getKeyboardEvents();
|
||||
if (events) {
|
||||
const wrappedEvents = this.wrapEventsInKeybindingsHandler(events);
|
||||
const keydownHandler = createKeybindingsHandler(wrappedEvents);
|
||||
this.appendToHandler(keydownHandler);
|
||||
document.addEventListener('keydown', keydownHandler);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$el && this.$el.dataset.keydownHandlerIndex) {
|
||||
const handlerToRemove =
|
||||
taggedHandlers[this.$el.dataset.keydownHandlerIndex];
|
||||
document.removeEventListener('keydown', handlerToRemove);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
appendToHandler(keydownHandler) {
|
||||
const indexToAppend = taggedHandlers.push(keydownHandler) - 1;
|
||||
const root = this.$el;
|
||||
root.dataset.keydownHandlerIndex = indexToAppend;
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return null;
|
||||
},
|
||||
wrapEventsInKeybindingsHandler(events) {
|
||||
const wrappedEvents = {};
|
||||
Object.keys(events).forEach(eventName => {
|
||||
wrappedEvents[eventName] = this.keydownWrapper(events[eventName]);
|
||||
});
|
||||
|
||||
return wrappedEvents;
|
||||
},
|
||||
keydownWrapper(handler) {
|
||||
return e => {
|
||||
const actionToPerform =
|
||||
typeof handler === 'function' ? handler : handler.action;
|
||||
const allowOnFocusedInput =
|
||||
typeof handler === 'function' ? false : handler.allowOnFocusedInput;
|
||||
|
||||
const isTypeable = isActiveElementTypeable(e);
|
||||
|
||||
if (isTypeable) {
|
||||
if (isEscape(e)) {
|
||||
e.target.blur();
|
||||
}
|
||||
|
||||
if (!allowOnFocusedInput) return;
|
||||
}
|
||||
|
||||
actionToPerform(e);
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -33,8 +33,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import Header from '../../components/playground/Header.vue';
|
||||
import UserMessage from '../../components/playground/UserMessage.vue';
|
||||
import BotMessage from '../../components/playground/BotMessage.vue';
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
BotMessage,
|
||||
TypingIndicator,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
mixins: [messageFormatterMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
componentData: {
|
||||
type: Object,
|
||||
@@ -67,17 +67,17 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.focusInput();
|
||||
document.addEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
const keyCode = buildHotKeys(e);
|
||||
if (['meta+enter', 'ctrl+enter'].includes(keyCode)) {
|
||||
this.onMessageSend();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+Enter': {
|
||||
action: () => {
|
||||
this.onMessageSend();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
focusInput() {
|
||||
this.$refs.messageInput.focus();
|
||||
|
||||
@@ -220,25 +220,26 @@ export default {
|
||||
},
|
||||
dropdownItem() {
|
||||
// This function is used to get all the items in the dropdown.
|
||||
if (!this.showDropdown) return [];
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll(
|
||||
this.$refs.dropdown?.querySelectorAll(
|
||||
'div.country-dropdown div.country-dropdown--item'
|
||||
)
|
||||
);
|
||||
},
|
||||
focusedOrActiveItem(className) {
|
||||
// This function is used to get the focused or active item in the dropdown.
|
||||
if (!this.showDropdown) return [];
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll(
|
||||
this.$refs.dropdown?.querySelectorAll(
|
||||
`div.country-dropdown div.country-dropdown--item.${className}`
|
||||
)
|
||||
);
|
||||
},
|
||||
handleKeyboardEvent(e) {
|
||||
if (this.showDropdown) {
|
||||
this.processKeyDownEvent(e);
|
||||
adjustScroll() {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToFocusedOrActiveItem(this.focusedOrActiveItem('focus'));
|
||||
}
|
||||
});
|
||||
},
|
||||
onSelect() {
|
||||
this.onSelectCountry(this.items[this.selectedIndex]);
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"postcss-loader": "^4.2.0",
|
||||
"semver": "7.5.3",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tinykeys": "^2.1.0",
|
||||
"turbolinks": "^5.2.0",
|
||||
"url-loader": "^2.0.0",
|
||||
"urlpattern-polyfill": "^6.0.2",
|
||||
|
||||
@@ -19615,6 +19615,11 @@ tinycolor2@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
|
||||
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
|
||||
|
||||
tinykeys@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tinykeys/-/tinykeys-2.1.0.tgz#1341563e92a7fac9ca90053fddaf2b7553500298"
|
||||
integrity sha512-/MESnqBD1xItZJn5oGQ4OsNORQgJfPP96XSGoyu4eLpwpL0ifO0SYR5OD76u0YMhMXsqkb0UqvI9+yXTh4xv8Q==
|
||||
|
||||
titleize@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53"
|
||||
|
||||
Reference in New Issue
Block a user