Add basic mention completion.

This commit is contained in:
Roger Braun 2017-03-15 17:06:48 +01:00
parent df2a39c0d6
commit 5249b1d23a
2 changed files with 33 additions and 65 deletions

View file

@ -1,10 +1,9 @@
import statusPoster from '../../services/status_poster/status_poster.service.js' import statusPoster from '../../services/status_poster/status_poster.service.js'
import MediaUpload from '../media_upload/media_upload.vue' import MediaUpload from '../media_upload/media_upload.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import Tribute from '../../../node_modules/tributejs/src/Tribute.js' import Completion from '../../services/completion/completion.js'
require('../../../node_modules/tributejs/scss/tribute.scss')
import { merge, reject, map, uniqBy } from 'lodash' import { take, filter, reject, map, uniqBy } from 'lodash'
const buildMentionsString = ({user, attentions}, currentUser) => { const buildMentionsString = ({user, attentions}, currentUser) => {
let allAttentions = [...attentions] let allAttentions = [...attentions]
@ -21,51 +20,6 @@ const buildMentionsString = ({user, attentions}, currentUser) => {
return mentions.join(' ') + ' ' return mentions.join(' ') + ' '
} }
const defaultCollection = {
// symbol that starts the lookup
trigger: '@',
// element to target for @mentions
iframe: null,
// class added in the flyout menu for active item
selectClass: 'highlight',
// function called on select that returns the content to insert
selectTemplate: function (item) {
return '@' + item.original.screen_name
},
// template for displaying item in menu
menuItemTemplate: function (item) {
return `<img src="${item.original.profile_image_url}"></img> <div class='name'>${item.string}</div>`
},
// template for when no match is found (optional),
// If no template is provided, menu is hidden.
noMatchTemplate: null,
// specify an alternative parent container for the menu
menuContainer: document.body,
// column to search against in the object (accepts function or string)
lookup: ({name, screen_name}) => `${name} (@${screen_name})`, // eslint-disable-line camelcase
// column that contains the content to insert by default
fillAttr: 'screen_name',
// REQUIRED: array of objects to match
values: [],
// specify whether a space is required before the trigger character
requireLeadingSpace: true,
// specify whether a space is allowed in the middle of mentions
allowSpaces: false
}
const tribute = new Tribute({ collection: [] })
const PostStatusForm = { const PostStatusForm = {
props: [ props: [
'replyTo', 'replyTo',
@ -89,30 +43,37 @@ const PostStatusForm = {
newStatus: { newStatus: {
status: statusText, status: statusText,
files: [] files: []
} },
caret: 0
} }
}, },
computed: { computed: {
candidates () {
if (this.textAtCaret.charAt(0) === '@') {
const matchedUsers = filter(this.users, (user) => (user.name + user.screen_name).match(this.textAtCaret.slice(1)))
return map(take(matchedUsers, 5), ({screen_name, name}) => screen_name)
} else {
return ['nothing']
}
},
textAtCaret () {
return (this.wordAtCaret || {}).word || ''
},
wordAtCaret () {
const word = Completion.wordAtPosition(this.newStatus.status, this.caret - 1) || {}
return word
},
users () { users () {
return this.$store.state.users.users return this.$store.state.users.users
},
completions () {
let users = this.users
users = merge({values: users}, defaultCollection)
return [users]
} }
}, },
watch: {
completions () {
tribute.collection = this.completions
}
},
mounted () {
const textarea = this.$el.querySelector('textarea')
tribute.collection = this.completions
tribute.attach(textarea)
},
methods: { methods: {
replace (replacement) {
this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
},
setCaret ({target: {selectionStart}}) {
this.caret = selectionStart
},
postStatus (newStatus) { postStatus (newStatus) {
statusPoster.postStatus({ statusPoster.postStatus({
status: newStatus.status, status: newStatus.status,

View file

@ -2,7 +2,7 @@
<div class="post-status-form"> <div class="post-status-form">
<form @submit.prevent="postStatus(newStatus)"> <form @submit.prevent="postStatus(newStatus)">
<div class="form-group" > <div class="form-group" >
<textarea v-model="newStatus.status" placeholder="Just landed in L.A." rows="3" class="form-control" @keyup.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag"></textarea> <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" placeholder="Just landed in L.A." rows="3" class="form-control" @keyup.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag"></textarea>
</div> </div>
<div class="attachments"> <div class="attachments">
<div class="attachment" v-for="file in newStatus.files"> <div class="attachment" v-for="file in newStatus.files">
@ -13,6 +13,13 @@
<a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a> <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
</div> </div>
</div> </div>
<div>
<h1>Word</h1>
<h2>{{textAtCaret}}</h2>
<h1>Candidates</h1>
<h3 v-for="candidate in candidates" @click="replace('@' + candidate)">{{candidate}}</h3>
</div>
<div class='form-bottom'> <div class='form-bottom'>
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload> <media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload>
<button :disabled="submitDisabled" type="submit" class="btn btn-default base05 base01-background">Submit</button> <button :disabled="submitDisabled" type="submit" class="btn btn-default base05 base01-background">Submit</button>