diff --git a/src/web/app/desktop/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue similarity index 53% rename from src/web/app/desktop/views/components/autocomplete.vue rename to src/web/app/common/views/components/autocomplete.vue index a99d405e82e7828e629c4a8e318c14b665e05292..19808049273c77c082cbfce1aceaa008027a772b 100644 --- a/src/web/app/desktop/views/components/autocomplete.vue +++ b/src/web/app/common/views/components/autocomplete.vue @@ -1,26 +1,40 @@ <template> -<div class="mk-autocomplete"> - <ol class="users" ref="users" v-if="users.length > 0"> - <li v-for="user in users" @click="complete(user)" @keydown="onKeydown" tabindex="-1"> +<div class="mk-autocomplete" @contextmenu.prevent="() => {}"> + <ol class="users" ref="suggests" v-if="users.length > 0"> + <li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1"> <img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/> <span class="name">{{ user.name }}</span> <span class="username">@{{ user.username }}</span> </li> </ol> + <ol class="emojis" ref="suggests" v-if="emojis.length > 0"> + <li v-for="emoji in emojis" @click="complete(type, pictograph.dic[emoji])" @keydown="onKeydown" tabindex="-1"> + <span class="emoji">{{ pictograph.dic[emoji] }}</span> + <span class="name" v-html="emoji.replace(q, `<b>${q}</b>`)"></span> + </li> + </ol> </div> </template> <script lang="ts"> import Vue from 'vue'; +import * as pictograph from 'pictograph'; import contains from '../../../common/scripts/contains'; export default Vue.extend({ - props: ['q', 'textarea', 'complete', 'close'], + props: ['type', 'q', 'textarea', 'complete', 'close'], data() { return { fetching: true, users: [], - select: -1 + emojis: [], + select: -1, + pictograph + } + }, + computed: { + items(): HTMLCollection { + return (this.$refs.suggests as Element).children; } }, mounted() { @@ -30,13 +44,19 @@ export default Vue.extend({ el.addEventListener('mousedown', this.onMousedown); }); - (this as any).api('users/search_by_username', { - query: this.q, - limit: 30 - }).then(users => { - this.users = users; - this.fetching = false; - }); + if (this.type == 'user') { + (this as any).api('users/search_by_username', { + query: this.q, + limit: 30 + }).then(users => { + this.users = users; + this.fetching = false; + }); + } else if (this.type == 'emoji') { + const emojis = Object.keys(pictograph.dic).sort((a, b) => a.length - b.length); + const matched = emojis.filter(e => e.indexOf(this.q) > -1); + this.emojis = matched.filter((x, i) => i <= 30); + } }, beforeDestroy() { this.textarea.removeEventListener('keydown', this.onKeydown); @@ -61,7 +81,7 @@ export default Vue.extend({ case 13: // [ENTER] if (this.select !== -1) { cancel(); - this.complete(this.users[this.select]); + (this.items[this.select] as any).click(); } else { this.close(); } @@ -93,24 +113,22 @@ export default Vue.extend({ }, selectNext() { - if (++this.select >= this.users.length) this.select = 0; + if (++this.select >= this.items.length) this.select = 0; this.applySelect(); }, selectPrev() { - if (--this.select < 0) this.select = this.users.length - 1; + if (--this.select < 0) this.select = this.items.length - 1; this.applySelect(); }, applySelect() { - const els = (this.$refs.users as Element).children; - - Array.from(els).forEach(el => { + Array.from(this.items).forEach(el => { el.removeAttribute('data-selected'); }); - els[this.select].setAttribute('data-selected', 'true'); - (els[this.select] as any).focus(); + this.items[this.select].setAttribute('data-selected', 'true'); + (this.items[this.select] as any).focus(); } } }); @@ -126,7 +144,7 @@ export default Vue.extend({ border solid 1px rgba(0, 0, 0, 0.1) border-radius 4px - > .users + > ol display block margin 0 padding 4px 0 @@ -149,42 +167,47 @@ export default Vue.extend({ &:hover &[data-selected='true'] - color #fff background $theme-color - .name - color #fff - - .username - color #fff + &, * + color #fff !important &:active - color #fff background darken($theme-color, 10%) - .name - color #fff - - .username - color #fff - - .avatar - vertical-align middle - min-width 28px - min-height 28px - max-width 28px - max-height 28px - margin 0 8px 0 0 - border-radius 100% - - .name - margin 0 8px 0 0 - /*font-weight bold*/ - font-weight normal - color rgba(0, 0, 0, 0.8) - - .username - font-weight normal - color rgba(0, 0, 0, 0.3) + &, * + color #fff !important + + > .users > li + + .avatar + vertical-align middle + min-width 28px + min-height 28px + max-width 28px + max-height 28px + margin 0 8px 0 0 + border-radius 100% + + .name + margin 0 8px 0 0 + /*font-weight bold*/ + font-weight normal + color rgba(0, 0, 0, 0.8) + + .username + font-weight normal + color rgba(0, 0, 0, 0.3) + + > .emojis > li + + .emoji + display inline-block + margin 0 4px 0 0 + width 24px + + .name + font-weight normal + color rgba(0, 0, 0, 0.8) </style> diff --git a/src/web/app/common/views/components/messaging-room.form.vue b/src/web/app/common/views/components/messaging-room.form.vue index b89365a5d84a38425972b4dec16f8a3822fec9e5..aa07217b32f27c968b20eeae7c150db91489639c 100644 --- a/src/web/app/common/views/components/messaging-room.form.vue +++ b/src/web/app/common/views/components/messaging-room.form.vue @@ -1,6 +1,6 @@ <template> <div class="mk-messaging-form"> - <textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%"></textarea> + <textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%" v-autocomplete></textarea> <div class="file" v-if="file">{{ file.name }}</div> <mk-uploader ref="uploader"/> <button class="send" @click="send" :disabled="sending" title="%i18n:common.send%"> diff --git a/src/web/app/desktop/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts similarity index 57% rename from src/web/app/desktop/views/directives/autocomplete.ts rename to src/web/app/common/views/directives/autocomplete.ts index 53fa5a4df26fb672aecbf9d5fe6a0ff77ff06c4d..bd9c9cb611a09982c329e497028fb7ae200e67c1 100644 --- a/src/web/app/desktop/views/directives/autocomplete.ts +++ b/src/web/app/common/views/directives/autocomplete.ts @@ -25,11 +25,11 @@ class Autocomplete { * 対象ã®ãƒ†ã‚ストエリアを与ãˆã¦ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’åˆæœŸåŒ–ã—ã¾ã™ã€‚ */ constructor(textarea) { - // BIND --------------------------------- - this.onInput = this.onInput.bind(this); + //#region BIND + this.onInput = this.onInput.bind(this); this.complete = this.complete.bind(this); - this.close = this.close.bind(this); - // -------------------------------------- + this.close = this.close.bind(this); + //#endregion this.suggestion = null; this.textarea = textarea; @@ -60,14 +60,19 @@ class Autocomplete { const text = this.textarea.value.substr(0, caret); const mentionIndex = text.lastIndexOf('@'); - - if (mentionIndex == -1) return; - - const username = text.substr(mentionIndex + 1); - - if (!username.match(/^[a-zA-Z0-9-]+$/)) return; - - this.open('user', username); + const emojiIndex = text.lastIndexOf(':'); + + if (mentionIndex != -1 && mentionIndex > emojiIndex) { + const username = text.substr(mentionIndex + 1); + if (!username.match(/^[a-zA-Z0-9-]+$/)) return; + this.open('user', username); + } + + if (emojiIndex != -1 && emojiIndex > mentionIndex) { + const emoji = text.substr(emojiIndex + 1); + if (!emoji.match(/^[\+\-a-z_]+$/)) return; + this.open('emoji', emoji); + } } /** @@ -88,14 +93,14 @@ class Autocomplete { } }).$mount(); - // ~ サジェストを表示ã™ã¹ãä½ç½®ã‚’計算 ~ - + //#region サジェストを表示ã™ã¹ãä½ç½®ã‚’計算 const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); const rect = this.textarea.getBoundingClientRect(); - const x = rect.left + window.pageXOffset + caretPosition.left; - const y = rect.top + window.pageYOffset + caretPosition.top; + const x = rect.left + window.pageXOffset + caretPosition.left - this.textarea.scrollLeft; + const y = rect.top + window.pageYOffset + caretPosition.top - this.textarea.scrollTop; + //#endregion this.suggestion.$el.style.left = x + 'px'; this.suggestion.$el.style.top = y + 'px'; @@ -119,24 +124,39 @@ class Autocomplete { /** * オートコンプリートã™ã‚‹ */ - private complete(user) { + private complete(type, value) { this.close(); - const value = user.username; - const caret = this.textarea.selectionStart; - const source = this.textarea.value; - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('@')); - const after = source.substr(caret); + if (type == 'user') { + const source = this.textarea.value; - // çµæžœã‚’挿入ã™ã‚‹ - this.textarea.value = trimmedBefore + '@' + value + ' ' + after; + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('@')); + const after = source.substr(caret); - // ã‚ャレットを戻㙠- this.textarea.focus(); - const pos = caret + value.length; - this.textarea.setSelectionRange(pos, pos); + // 挿入 + this.textarea.value = trimmedBefore + '@' + value.username + ' ' + after; + + // ã‚ャレットを戻㙠+ this.textarea.focus(); + const pos = caret + value.username.length; + this.textarea.setSelectionRange(pos, pos); + } else if (type == 'emoji') { + const source = this.textarea.value; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf(':')); + const after = source.substr(caret); + + // 挿入 + this.textarea.value = trimmedBefore + value + after; + + // ã‚ャレットを戻㙠+ this.textarea.focus(); + const pos = caret + value.length; + this.textarea.setSelectionRange(pos, pos); + } } } diff --git a/src/web/app/common/views/directives/focus.ts b/src/web/app/common/views/directives/focus.ts deleted file mode 100644 index b4fbcb6a87fd29bec944f539643b4a2caf295471..0000000000000000000000000000000000000000 --- a/src/web/app/common/views/directives/focus.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - inserted(el) { - el.focus(); - } -}; diff --git a/src/web/app/common/views/directives/index.ts b/src/web/app/common/views/directives/index.ts index 358866f500628f63e3811893c6bd08842fa1343c..268f07a950839f26ebbc258f99b50289b7c1e43a 100644 --- a/src/web/app/common/views/directives/index.ts +++ b/src/web/app/common/views/directives/index.ts @@ -1,5 +1,5 @@ import Vue from 'vue'; -import focus from './focus'; +import autocomplete from './autocomplete'; -Vue.directive('focus', focus); +Vue.directive('autocomplete', autocomplete); diff --git a/src/web/app/desktop/views/directives/index.ts b/src/web/app/desktop/views/directives/index.ts index 3d0c73b6b21adf8ecfca809cf263918da294dc76..324e07596d90b0d6f6263488fbb6c1c17f3987cd 100644 --- a/src/web/app/desktop/views/directives/index.ts +++ b/src/web/app/desktop/views/directives/index.ts @@ -1,8 +1,6 @@ import Vue from 'vue'; import userPreview from './user-preview'; -import autocomplete from './autocomplete'; Vue.directive('userPreview', userPreview); Vue.directive('user-preview', userPreview); -Vue.directive('autocomplete', autocomplete);