diff --git a/website/client/src/components/chat/emojiAutoComplete.vue b/website/client/src/components/chat/emojiAutoComplete.vue
new file mode 100644
index 0000000000..5b41c44811
--- /dev/null
+++ b/website/client/src/components/chat/emojiAutoComplete.vue
@@ -0,0 +1,191 @@
+
+
+
+ {{ result.emoji }}
+ :{{ result.shortcode }}:
+
+
+
+
+
+
+
diff --git a/website/client/src/components/groups/chat.vue b/website/client/src/components/groups/chat.vue
index d1b0e7ae4d..2d242863bc 100644
--- a/website/client/src/components/groups/chat.vue
+++ b/website/client/src/components/groups/chat.vue
@@ -41,6 +41,14 @@
:chat="group.chat"
@select="selectedAutocomplete"
/>
+
@@ -90,6 +98,7 @@ import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants';
import externalLinks from '../../mixins/externalLinks';
import autocomplete from '../chat/autoComplete';
+import emojiAutoComplete from '../chat/emojiAutoComplete';
import communityGuidelines from './communityGuidelines';
import chatMessages from '../chat/chatMessages';
import { mapState } from '@/libs/store';
@@ -102,6 +111,7 @@ export default {
},
components: {
autocomplete,
+ emojiAutoComplete,
communityGuidelines,
chatMessages,
},
diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue
index b2f17bf5bb..7117cbf927 100644
--- a/website/client/src/components/tasks/taskModal.vue
+++ b/website/client/src/components/tasks/taskModal.vue
@@ -70,6 +70,13 @@
spellcheck="true"
:disabled="challengeAccessRequired"
:placeholder="$t('addATitle')"
+ @focus="setActiveField('title')"
+ @keydown="autoCompleteMixinUpdateCarretPosition"
+ @keydown.tab="autoCompleteMixinHandleTab($event)"
+ @keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
+ @keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
+ @keypress.enter="titleEnterHandler($event)"
+ @keydown.esc="autoCompleteMixinHandleEscape($event)"
>
+
{
+ this.textbox.setSelectionRange(newCaret, newCaret);
+ this.textbox.focus();
+ });
},
async addTag (name) {
const tagResult = await this.createTag({ name });
diff --git a/website/client/src/components/userMenu/profile.vue b/website/client/src/components/userMenu/profile.vue
index d42713d4a0..ac979bca98 100644
--- a/website/client/src/components/userMenu/profile.vue
+++ b/website/client/src/components/userMenu/profile.vue
@@ -398,14 +398,29 @@
:placeholder="$t('imageUrl')"
>
-
@@ -1001,6 +1016,8 @@ import mute from '@/assets/svg/mute.svg?raw';
import shadowMute from '@/assets/svg/shadow-mute.svg?raw';
import externalLinks from '../../mixins/externalLinks';
import { userCustomStateMixin } from '../../mixins/userState';
+import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
+import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
@@ -1012,8 +1029,9 @@ export default {
MemberDetails,
profileStats,
toggleSwitch,
+ emojiAutoComplete,
},
- mixins: [externalLinks, userCustomStateMixin('userLoggedIn')],
+ mixins: [externalLinks, userCustomStateMixin('userLoggedIn'), autoCompleteHelperMixin],
props: ['userId', 'startingPage'],
data () {
return {
@@ -1033,6 +1051,7 @@ export default {
mute,
shadowMute,
}),
+ textbox: null,
userIdToMessage: '',
editing: false,
editingProfile: {
@@ -1121,6 +1140,13 @@ export default {
userLoggedIn () {
this.loadUser();
},
+ editing (val) {
+ if (val) {
+ this.$nextTick(() => {
+ this.textbox = this.$refs.blurbTextarea;
+ });
+ }
+ },
},
mounted () {
this.loadUser();
@@ -1331,6 +1357,13 @@ export default {
this.$emit('toggled', this.isOpened);
},
+ selectedAutocomplete (newText, newCaret) {
+ this.editingProfile.blurb = newText;
+ this.$nextTick(() => {
+ this.textbox.setSelectionRange(newCaret, newCaret);
+ this.textbox.focus();
+ });
+ },
reportPlayer () {
this.$root.$emit('habitica::report-profile', {
memberId: this.user._id,
diff --git a/website/client/src/mixins/autoCompleteHelper.js b/website/client/src/mixins/autoCompleteHelper.js
index d2651df88d..e2ad31c27d 100644
--- a/website/client/src/mixins/autoCompleteHelper.js
+++ b/website/client/src/mixins/autoCompleteHelper.js
@@ -15,46 +15,60 @@ export const autoCompleteHelperMixin = {
};
},
methods: {
+ _getActiveAutocomplete () {
+ if (this.$refs.autocomplete && this.$refs.autocomplete.searchActive) {
+ return this.$refs.autocomplete;
+ }
+ if (this.$refs.emojiAutocomplete && this.$refs.emojiAutocomplete.searchActive) {
+ return this.$refs.emojiAutocomplete;
+ }
+ return null;
+ },
+
autoCompleteMixinHandleTab (e) {
- if (this.$refs.autocomplete.searchActive) {
+ const ac = this._getActiveAutocomplete();
+ if (ac) {
e.preventDefault();
if (e.shiftKey) {
- this.$refs.autocomplete.selectPrevious();
+ ac.selectPrevious();
} else {
- this.$refs.autocomplete.selectNext();
+ ac.selectNext();
}
}
},
autoCompleteMixinHandleEscape (e) {
- if (this.$refs.autocomplete.searchActive) {
+ const ac = this._getActiveAutocomplete();
+ if (ac) {
e.preventDefault();
- this.$refs.autocomplete.cancel();
+ ac.cancel();
}
},
autoCompleteMixinSelectNextAutocomplete (e) {
- if (this.$refs.autocomplete.searchActive) {
+ const ac = this._getActiveAutocomplete();
+ if (ac) {
e.preventDefault();
- this.$refs.autocomplete.selectNext();
+ ac.selectNext();
}
},
autoCompleteMixinSelectPreviousAutocomplete (e) {
- if (this.$refs.autocomplete.searchActive) {
+ const ac = this._getActiveAutocomplete();
+ if (ac) {
e.preventDefault();
- this.$refs.autocomplete.selectPrevious();
+ ac.selectPrevious();
}
},
autoCompleteMixinSelectAutocomplete (e) {
- if (this.$refs.autocomplete.searchActive) {
- if (this.$refs.autocomplete.selected !== null) {
+ const ac = this._getActiveAutocomplete();
+ if (ac) {
+ if (ac.selected !== null) {
e.preventDefault();
- this.$refs.autocomplete.makeSelection();
+ ac.makeSelection();
} else {
- // no autocomplete selected, newline instead
- this.$refs.autocomplete.cancel();
+ ac.cancel();
}
}
},
diff --git a/website/client/src/pages/private-messages/index.vue b/website/client/src/pages/private-messages/index.vue
index 4e347ce870..f3dd2ee699 100644
--- a/website/client/src/pages/private-messages/index.vue
+++ b/website/client/src/pages/private-messages/index.vue
@@ -153,9 +153,23 @@
:placeholder="$t('needsTextPlaceholder')"
:maxlength="MAX_MESSAGE_LENGTH"
:class="{'has-content': newMessage.trim() !== '', 'disabled': newMessageDisabled}"
+ @keydown="autoCompleteMixinUpdateCarretPosition"
@keyup.ctrl.enter="sendPrivateMessage()"
+ @keydown.tab="autoCompleteMixinHandleTab($event)"
+ @keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
+ @keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
+ @keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
+ @keydown.esc="autoCompleteMixinHandleEscape($event)"
>
+
{
const data = vm.$store.state.privateMessageOptions;
@@ -751,6 +769,7 @@ export default defineComponent({
/** @type {Record} */
messagesByConversation: {}, // cache {uuid: []}
+ textbox: null,
newMessage: '',
messages: [],
messagesLoading: false,
@@ -964,6 +983,7 @@ export default defineComponent({
},
},
async mounted () {
+ this.textbox = this.$refs.textarea;
this.$store.dispatch('common:setTitle', {
section: this.$t('messages'),
});
@@ -1224,6 +1244,13 @@ export default defineComponent({
triggerStartNewConversationState () {
this.showStartNewConversationInput = true;
},
+ selectedAutocomplete (newText, newCaret) {
+ this.newMessage = newText;
+ this.$nextTick(() => {
+ this.textbox.setSelectionRange(newCaret, newCaret);
+ this.textbox.focus();
+ });
+ },
async startConversationByUsername (targetUserName) {
// check if the target user exists in current conversations, select that conversation
/** @type {PrivateMessages.ConversationSummaryMessageEntry} */