I would like to set position on a textarea and set focus programmatically but it seems that there is a problem on iOS when the text is more long.
It’s a mention component which you can tag a people searched by the name.
<template>
<div class="mentionable-textarea-wrapper">
<textarea @ionFocus="onFocus"
@ionBlur="onBlur"
@keydown.right="isOpened = false"
@keydown.left="isOpened = false"
@ionChange="onChange"
v-model="text" rows="3" cols="20"
:placeholder="passedPlaceholder" ref="textarea">
</textarea>
<ion-content class="tooltip" v-show="isOpened && filteredItems.length">
<ion-list class="user-list" mode="ios">
<ion-item v-for="item in filteredItems" @click="onMentionSelect(item.id)" :key="item.id">
<div class="tooltip-item">
<span>{{ item.first_name }} {{ item.last_name }}</span>
</div>
</ion-item>
</ion-list>
</ion-content>
</div>
</template>
<script lang="ts">
import {nextTick, defineComponent} from 'vue';
import {IonTextarea, IonList, IonItem, IonContent} from '@ionic/vue';
export default defineComponent({
name: "MentionableTextarea",
emits: ["onFocus", "onBlur", "update:modelValue", "update:selectedIds"],
components: {
IonTextarea,
IonList,
IonItem,
IonContent
},
props: {
modelValue: {
type: String,
default: ''
},
selectedIds: {
type: Array,
default: () => [] as any
},
placeholder: {
type: String,
default: ''
},
items: {
type: Array as any,
required: true,
default: () => [] as any,
}
},
data() {
return {
isOpened: false,
passedPlaceholder: this.placeholder,
passedItems: this.items,
text: '',
textToFilter: '',
selectedMentions: [],
previousTextByAt: [],
selectionStart: 0,
};
},
computed: {
/**
* @desc Filter items by text
*/
filteredItems: function (this: any) {
if (!this.textToFilter) {
return this.items
}
const searchText = this.textToFilter.toLowerCase().trim().replace('@', '')
return this.items.filter(item => {
return item.first_name.toLowerCase().includes(searchText) || item.last_name.toLowerCase().includes(searchText) || item.username.toLowerCase().includes(searchText)
})
}
},
methods: {
/**
* @desc On focus callback
*/
onFocus() {
this.$emit('onFocus')
},
/**
* @desc On blur callback
*/
onBlur() {
this.$emit('onBlur')
},
/**
* @desc On input change event
*/
onChange(e) {
e.currentTarget.getInputElement().then(el => this.selectionStart = el.selectionStart)
nextTick(() => {
const splittedTextByAtWords = this._getTagsFromText()
// Update list of results
if (this.previousTextByAt && splittedTextByAtWords) {
const difference = splittedTextByAtWords.filter(x => !this.previousTextByAt.includes(x));
if (difference && difference.length) {
this.textToFilter = difference[0]
this.isOpened = true
} else {
this.textToFilter = ''
this.isOpened = false
}
} else {
this.isOpened = false;
}
this._updateSelectedUsers()
this.previousTextByAt = splittedTextByAtWords
this.$emit('update:modelValue', this._parseTextWithHtml())
})
},
/**
* @desc On mention select
* @param id
*/
onMentionSelect(id) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const $vm = this
// Search item in array
const foundItem = this.items.filter(x => x.id == id);
const concatName = '@' + foundItem[0].username + ' ';
const index = this.selectionStart - this.textToFilter.length;
this._replaceTextAtIndex(index, concatName)
setTimeout(function () {
$vm.isOpened = false;
// Find element and set focus to the right position
const textarea = document.getElementsByClassName("textarea")[0] as HTMLInputElement
textarea.focus()
textarea.setSelectionRange($vm.text.length, $vm.text.length)
console.log($vm.text.length)
}, 500);
},
/**
* @desc Get tags from ext
*/
_getTagsFromText() {
const splittedText = this.text.split(' ')
const splittedTextByAtWords = []
// Get all the words with @
splittedText.forEach(function (item) {
if (item.trim().startsWith('@')) {
splittedTextByAtWords.push(item)
}
})
return splittedTextByAtWords;
},
/**
* @desc Update selected users array
*/
_updateSelectedUsers() {
this.selectedMentions = [];
const splittedTextByAtWords = this._getTagsFromText()
// eslint-disable-next-line @typescript-eslint/no-this-alias
const $vm = this;
splittedTextByAtWords.forEach(function (item) {
// Update list of id if a username match
const foundUserByUsername = $vm.items.filter(x => x.username === item.replace('@', ''));
if (foundUserByUsername.length) {
$vm.selectedMentions.push({'id': foundUserByUsername[0].id, 'username': foundUserByUsername[0].username})
}
});
this.$emit('update:selectedIds', this.selectedMentions)
},
/**
* @desc Add html for tags
*/
_parseTextWithHtml() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const $vm = this;
if (this.selectedMentions.length) {
// Split text with space
const newTextSplitted = this.text.split(" ");
const newTextParsed = []
newTextSplitted.forEach(function (piece) {
// Search piece of string between mentions, if correspond attach html
const foundMention = $vm.selectedMentions.filter(x => '@' + x.username === piece) as any
if (foundMention.length) {
piece = '<strong data-id="' + foundMention[0].id + '">@' + foundMention[0].username + '</strong>'
newTextParsed.push(piece)
} else {
newTextParsed.push(piece)
}
})
return newTextParsed.join(" ")
} else {
return this.text;
}
},
_replaceTextAtIndex(index, replacement) {
this.text = this.text.substr(0, index) + replacement + this.text.substr(index + replacement.length);
},
}
});
</script>
1 post - 1 participant