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