diff options
| author | Henry Jameson <me@hjkos.com> | 2019-07-28 13:30:29 +0300 |
|---|---|---|
| committer | Henry Jameson <me@hjkos.com> | 2019-07-28 13:30:29 +0300 |
| commit | b3aff9bbae77b2fd34b2267ce9196c0ebd3e4691 (patch) | |
| tree | 1219e00b6bfe6784add1578a3bc986c1dbb5f34d /src/components/post_status_form | |
| parent | 7f6f025792dcb3a10c94c8952d0312abd0b46989 (diff) | |
| parent | 4827e4d972f8ee11e606693e24ae4ca21711c6b1 (diff) | |
Merge remote-tracking branch 'upstream/develop' into emoji-selector-update
* upstream/develop: (469 commits)
Feature/add sticker picker
guard more secure routes
guard secure routes by redirecting to root
closest can returns itself as well
find inside status-content div only
try to use the closest a tag as target
Update es.json
Also apply keyword filter to subjects
Remove files I accidentally pushed in
fix issues caused by merges in usersearch on @
Add user search at
fix eslint warnings
remove vue-popperjs
fix moderation menu partially hidden by usercard boundary
migrate popper css
rewrite ModerationTools using v-tooltip
make popover position for status action dropdow relative to parent node
rewrite ExtraButtons using v-tooltip
install v-tooltip
i18n/Update pedantic Japanese translation
...
Diffstat (limited to 'src/components/post_status_form')
| -rw-r--r-- | src/components/post_status_form/post_status_form.js | 235 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.vue | 401 |
2 files changed, 400 insertions, 236 deletions
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 8b0031de..5aaac8e6 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -3,17 +3,19 @@ import MediaUpload from '../media_upload/media_upload.vue' import ScopeSelector from '../scope_selector/scope_selector.vue' import EmojiInput from '../emoji-input/emoji-input.vue' import EmojiSelector from '../emoji-selector/emoji-selector.vue' +import PollForm from '../poll/poll_form.vue' +import StickerPicker from '../sticker_picker/sticker_picker.vue' import fileTypeService from '../../services/file_type/file_type.service.js' -import Completion from '../../services/completion/completion.js' -import { take, filter, reject, map, uniqBy } from 'lodash' +import { reject, map, uniqBy } from 'lodash' +import suggestor from '../emoji-input/suggestor.js' -const buildMentionsString = ({user, attentions}, currentUser) => { +const buildMentionsString = ({ user, attentions }, currentUser) => { let allAttentions = [...attentions] allAttentions.unshift(user) allAttentions = uniqBy(allAttentions, 'id') - allAttentions = reject(allAttentions, {id: currentUser.id}) + allAttentions = reject(allAttentions, { id: currentUser.id }) let mentions = map(allAttentions, (attention) => { return `@${attention.screen_name}` @@ -32,9 +34,11 @@ const PostStatusForm = { ], components: { MediaUpload, - ScopeSelector, EmojiInput, - EmojiSelector + PollForm, + StickerPicker, + EmojiSelector, + ScopeSelector }, mounted () { this.resize(this.$refs.textarea) @@ -50,17 +54,17 @@ const PostStatusForm = { let statusText = preset || '' const scopeCopy = typeof this.$store.state.config.scopeCopy === 'undefined' - ? this.$store.state.instance.scopeCopy - : this.$store.state.config.scopeCopy + ? this.$store.state.instance.scopeCopy + : this.$store.state.config.scopeCopy if (this.replyTo) { const currentUser = this.$store.state.users.currentUser statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) } - const scope = (this.copyMessageScope && scopeCopy || this.copyMessageScope === 'direct') - ? this.copyMessageScope - : this.$store.state.users.currentUser.default_scope + const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct') + ? this.copyMessageScope + : this.$store.state.users.currentUser.default_scope const contentType = typeof this.$store.state.config.postContentType === 'undefined' ? this.$store.state.instance.postContentType @@ -77,57 +81,16 @@ const PostStatusForm = { status: statusText, nsfw: false, files: [], + poll: {}, visibility: scope, contentType }, - caret: 0 + caret: 0, + pollFormVisible: false, + stickerPickerVisible: false } }, computed: { - candidates () { - const firstchar = this.textAtCaret.charAt(0) - if (firstchar === '@') { - const query = this.textAtCaret.slice(1).toUpperCase() - const matchedUsers = filter(this.users, (user) => { - return user.screen_name.toUpperCase().startsWith(query) || - user.name && user.name.toUpperCase().startsWith(query) - }) - if (matchedUsers.length <= 0) { - return false - } - // eslint-disable-next-line camelcase - return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({ - // eslint-disable-next-line camelcase - screen_name: `@${screen_name}`, - name: name, - img: profile_image_url_original, - highlighted: index === this.highlighted - })) - } else if (firstchar === ':') { - if (this.textAtCaret === ':') { return } - const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1))) - if (matchedEmoji.length <= 0) { - return false - } - return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({ - screen_name: `:${shortcode}:`, - name: '', - utf: utf || '', - // eslint-disable-next-line camelcase - img: utf ? '' : this.$store.state.instance.server + image_url, - highlighted: index === this.highlighted - })) - } else { - return false - } - }, - textAtCaret () { - return (this.wordAtCaret || {}).word || '' - }, - wordAtCaret () { - const word = Completion.wordAtPosition(this.newStatus.status, this.caret - 1) || {} - return word - }, users () { return this.$store.state.users.users }, @@ -136,10 +99,28 @@ const PostStatusForm = { }, showAllScopes () { const minimalScopesMode = typeof this.$store.state.config.minimalScopesMode === 'undefined' - ? this.$store.state.instance.minimalScopesMode - : this.$store.state.config.minimalScopesMode + ? this.$store.state.instance.minimalScopesMode + : this.$store.state.config.minimalScopesMode return !minimalScopesMode }, + emojiUserSuggestor () { + return suggestor({ + emoji: [ + ...this.$store.state.instance.emoji, + ...this.$store.state.instance.customEmoji + ], + users: this.$store.state.users.users, + updateUsersList: (input) => this.$store.dispatch('searchUsers', input) + }) + }, + emojiSuggestor () { + return suggestor({ + emoji: [ + ...this.$store.state.instance.emoji, + ...this.$store.state.instance.customEmoji + ] + }) + }, emoji () { return this.$store.state.instance.emoji || [] }, @@ -176,91 +157,32 @@ const PostStatusForm = { return true } }, - formattingOptionsEnabled () { - return this.$store.state.instance.formattingOptionsEnabled - }, postFormats () { return this.$store.state.instance.postFormats || [] }, safeDMEnabled () { return this.$store.state.instance.safeDM - } - }, - methods: { - replace (replacement) { - this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement) - const el = this.$el.querySelector('textarea') - el.focus() - this.caret = 0 - }, - replaceCandidate (e) { - const len = this.candidates.length || 0 - if (this.textAtCaret === ':' || e.ctrlKey) { return } - if (len > 0) { - e.preventDefault() - const candidate = this.candidates[this.highlighted] - const replacement = candidate.utf || (candidate.screen_name + ' ') - this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement) - const el = this.$el.querySelector('textarea') - el.focus() - this.caret = 0 - this.highlighted = 0 - } - }, - cycleBackward (e) { - const len = this.candidates.length || 0 - if (len > 0) { - e.preventDefault() - this.highlighted -= 1 - if (this.highlighted < 0) { - this.highlighted = this.candidates.length - 1 - } - } else { - this.highlighted = 0 - } }, - cycleForward (e) { - const len = this.candidates.length || 0 - if (len > 0) { - if (e.shiftKey) { return } - e.preventDefault() - this.highlighted += 1 - if (this.highlighted >= len) { - this.highlighted = 0 - } - } else { - this.highlighted = 0 + stickersAvailable () { + if (this.$store.state.instance.stickers) { + return this.$store.state.instance.stickers.length > 0 } + return 0 }, - onKeydown (e) { - e.stopPropagation() - }, - onEmoji (emoji) { - const newValue = this.newStatus.status.substr(0, this.caret) + emoji + this.newStatus.status.substr(this.caret) - this.newStatus.status = newValue - this.caret += emoji.length - setTimeout(() => { - this.updateCaretPos() - }) - }, - updateCaretPos () { - const elem = this.$refs.textarea - if (elem.createTextRange) { - const range = elem.createTextRange() - range.move('character', this.caret) - range.select() - } else { - if (elem.selectionStart) { - elem.focus() - elem.setSelectionRange(this.caret, this.caret) - } else { - elem.focus() - } - } + pollsAvailable () { + return this.$store.state.instance.pollsAvailable && + this.$store.state.instance.pollLimits.max_options >= 2 }, - setCaret ({target: {selectionStart}}) { - this.caret = selectionStart + hideScopeNotice () { + return this.$store.state.config.hideScopeNotice }, + pollContentError () { + return this.pollFormVisible && + this.newStatus.poll && + this.newStatus.poll.error + } + }, + methods: { postStatus (newStatus) { if (this.posting) { return } if (this.submitDisabled) { return } @@ -274,6 +196,12 @@ const PostStatusForm = { } } + const poll = this.pollFormVisible ? this.newStatus.poll : {} + if (this.pollContentError) { + this.error = this.pollContentError + return + } + this.posting = true statusPoster.postStatus({ status: newStatus.status, @@ -283,7 +211,8 @@ const PostStatusForm = { media: newStatus.files, store: this.$store, inReplyToStatusId: this.replyTo, - contentType: newStatus.contentType + contentType: newStatus.contentType, + poll }).then((data) => { if (!data.error) { this.newStatus = { @@ -291,9 +220,13 @@ const PostStatusForm = { spoilerText: '', files: [], visibility: newStatus.visibility, - contentType: newStatus.contentType + contentType: newStatus.contentType, + poll: {} } + this.pollFormVisible = false + this.stickerPickerVisible = false this.$refs.mediaUpload.clearFile() + this.clearPollForm() this.$emit('posted') let el = this.$el.querySelector('textarea') el.style.height = 'auto' @@ -308,6 +241,7 @@ const PostStatusForm = { addMediaFile (fileInfo) { this.newStatus.files.push(fileInfo) this.enableSubmit() + this.stickerPickerVisible = false }, removeMediaFile (fileInfo) { let index = this.newStatus.files.indexOf(fileInfo) @@ -339,7 +273,7 @@ const PostStatusForm = { }, fileDrop (e) { if (e.dataTransfer.files.length > 0) { - e.preventDefault() // allow dropping text like before + e.preventDefault() // allow dropping text like before this.dropFiles = e.dataTransfer.files } }, @@ -349,8 +283,11 @@ const PostStatusForm = { resize (e) { const target = e.target || e if (!(target instanceof window.Element)) { return } - const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) + - Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1)) + const topPaddingStr = window.getComputedStyle(target)['padding-top'] + const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom'] + // Remove "px" at the end of the values + const vertPadding = Number(topPaddingStr.substr(0, topPaddingStr.length - 2)) + + Number(bottomPaddingStr.substr(0, bottomPaddingStr.length - 2)) // Auto is needed to make textbox shrink when removing lines target.style.height = 'auto' target.style.height = `${target.scrollHeight - vertPadding}px` @@ -363,6 +300,28 @@ const PostStatusForm = { }, changeVis (visibility) { this.newStatus.visibility = visibility + }, + toggleStickerPicker () { + this.stickerPickerVisible = !this.stickerPickerVisible + }, + clearStickerPicker () { + if (this.$refs.stickerPicker) { + this.$refs.stickerPicker.clear() + } + }, + togglePollForm () { + this.pollFormVisible = !this.pollFormVisible + }, + setPoll (poll) { + this.newStatus.poll = poll + }, + clearPollForm () { + if (this.$refs.pollForm) { + this.$refs.pollForm.clear() + } + }, + dismissScopeNotice () { + this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true }) } } } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 102cb484..d15c75ad 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -1,112 +1,268 @@ <template> -<div class="post-status-form"> - <form @submit.prevent="postStatus(newStatus)"> - <div class="form-group" > - <i18n - v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'" - path="post_status.account_not_locked_warning" - tag="p" - class="visibility-notice"> - <router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link> - </i18n> - <p v-if="newStatus.visibility === 'direct'" class="visibility-notice"> - <span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span> - <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span> - </p> - <EmojiInput - v-if="newStatus.spoilerText || alwaysShowSubject" - type="text" - :placeholder="$t('post_status.content_warning')" - v-model="newStatus.spoilerText" - classname="form-control" - /> - <div class="status-input-wrapper"> - <textarea - ref="textarea" - @click="setCaret" - @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control" - @keydown="onKeydown" - @keydown.down="cycleForward" - @keydown.up="cycleBackward" - @keydown.shift.tab="cycleBackward" - @keydown.tab="cycleForward" - @keydown.enter="replaceCandidate" - @keydown.meta.enter="postStatus(newStatus)" - @keyup.ctrl.enter="postStatus(newStatus)" - @drop="fileDrop" - @dragover.prevent="fileDrag" - @input="resize" - @paste="paste" - :disabled="posting" + <div class="post-status-form"> + <form + autocomplete="off" + @submit.prevent="postStatus(newStatus)" + > + <div class="form-group"> + <i18n + v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'" + path="post_status.account_not_locked_warning" + tag="p" + class="visibility-notice" > - </textarea> - <EmojiSelector @emoji="onEmoji" /> - </div> - <div class="visibility-tray"> - <span class="text-format" v-if="formattingOptionsEnabled"> - <label for="post-content-type" class="select"> - <select id="post-content-type" v-model="newStatus.contentType" class="form-control"> - <option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat"> - {{$t(`post_status.content_type["${postFormat}"]`)}} - </option> - </select> - <i class="icon-down-open"></i> - </label> - </span> + <router-link :to="{ name: 'user-settings' }"> + {{ $t('post_status.account_not_locked_warning_link') }} + </router-link> + </i18n> + <p + v-if="!hideScopeNotice && newStatus.visibility === 'public'" + class="visibility-notice notice-dismissible" + > + <span>{{ $t('post_status.scope_notice.public') }}</span> + <a + class="button-icon dismiss" + @click.prevent="dismissScopeNotice()" + > + <i class="icon-cancel" /> + </a> + </p> + <p + v-else-if="!hideScopeNotice && newStatus.visibility === 'unlisted'" + class="visibility-notice notice-dismissible" + > + <span>{{ $t('post_status.scope_notice.unlisted') }}</span> + <a + class="button-icon dismiss" + @click.prevent="dismissScopeNotice()" + > + <i class="icon-cancel" /> + </a> + </p> + <p + v-else-if="!hideScopeNotice && newStatus.visibility === 'private' && $store.state.users.currentUser.locked" + class="visibility-notice notice-dismissible" + > + <span>{{ $t('post_status.scope_notice.private') }}</span> + <a + class="button-icon dismiss" + @click.prevent="dismissScopeNotice()" + > + <i class="icon-cancel" /> + </a> + </p> + <p + v-else-if="newStatus.visibility === 'direct'" + class="visibility-notice" + > + <span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span> + <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span> + </p> + <EmojiInput + v-if="newStatus.spoilerText || alwaysShowSubject" + v-model="newStatus.spoilerText" + :suggest="emojiSuggestor" + class="form-control" + > + <input + + v-model="newStatus.spoilerText" + type="text" + :placeholder="$t('post_status.content_warning')" + class="form-post-subject" + > + </EmojiInput> + <EmojiInput + v-model="newStatus.status" + :suggest="emojiUserSuggestor" + class="form-control main-input" + > + <textarea + ref="textarea" + v-model="newStatus.status" + :placeholder="$t('post_status.default')" + rows="1" + :disabled="posting" + class="form-post-body" + @keydown.meta.enter="postStatus(newStatus)" + @keyup.ctrl.enter="postStatus(newStatus)" + @drop="fileDrop" + @dragover.prevent="fileDrag" + @input="resize" + @paste="paste" + /> + <p + v-if="hasStatusLengthLimit" + class="character-counter faint" + :class="{ error: isOverLengthLimit }" + > + {{ charactersLeft }} + </p> + </EmojiInput> + <div class="visibility-tray"> + <scope-selector + :show-all="showAllScopes" + :user-default="userDefaultScope" + :original-scope="copyMessageScope" + :initial-scope="newStatus.visibility" + :on-scope-change="changeVis" + /> - <scope-selector - :showAll="showAllScopes" - :userDefault="userDefaultScope" - :originalScope="copyMessageScope" - :initialScope="newStatus.visibility" - :onScopeChange="changeVis"/> - </div> - </div> - <div class="autocomplete-panel" v-if="candidates"> - <div class="autocomplete-panel-body"> <div - v-for="(candidate, index) in candidates" - :key="index" - @click="replace(candidate.utf || (candidate.screen_name + ' '))" - class="autocomplete-item" - :class="{ highlighted: candidate.highlighted }" + v-if="postFormats.length > 1" + class="text-format" > - <span v-if="candidate.img"><img :src="candidate.img" /></span> - <span v-else>{{candidate.utf}}</span> - <span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span> + <label + for="post-content-type" + class="select" + > + <select + id="post-content-type" + v-model="newStatus.contentType" + class="form-control" + > + <option + v-for="postFormat in postFormats" + :key="postFormat" + :value="postFormat" + > + {{ $t(`post_status.content_type["${postFormat}"]`) }} + </option> + </select> + <i class="icon-down-open" /> + </label> + </div> + <div + v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'" + class="text-format" + > + <span class="only-format"> + {{ $t(`post_status.content_type["${postFormats[0]}"]`) }} + </span> </div> </div> </div> - <div class='form-bottom'> - <media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload> - - <p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p> - <p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p> - - <button v-if="posting" disabled class="btn btn-default">{{$t('post_status.posting')}}</button> - <button v-else-if="isOverLengthLimit" disabled class="btn btn-default">{{$t('general.submit')}}</button> - <button v-else :disabled="submitDisabled" type="submit" class="btn btn-default">{{$t('general.submit')}}</button> + <poll-form + v-if="pollsAvailable" + ref="pollForm" + :visible="pollFormVisible" + @update-poll="setPoll" + /> + <div class="form-bottom"> + <div class="form-bottom-left"> + <media-upload + ref="mediaUpload" + :drop-files="dropFiles" + @uploading="disableSubmit" + @uploaded="addMediaFile" + @upload-failed="uploadFailed" + /> + <div + v-if="stickersAvailable" + class="sticker-icon" + > + <i + :title="$t('stickers.add_sticker')" + class="icon-picture btn btn-default" + :class="{ selected: stickerPickerVisible }" + @click="toggleStickerPicker" + /> + </div> + <div + v-if="pollsAvailable" + class="poll-icon" + > + <i + :title="$t('polls.add_poll')" + class="icon-chart-bar btn btn-default" + :class="pollFormVisible && 'selected'" + @click="togglePollForm" + /> + </div> + </div> + <button + v-if="posting" + disabled + class="btn btn-default" + > + {{ $t('post_status.posting') }} + </button> + <button + v-else-if="isOverLengthLimit" + disabled + class="btn btn-default" + > + {{ $t('general.submit') }} + </button> + <button + v-else + :disabled="submitDisabled" + type="submit" + class="btn btn-default" + > + {{ $t('general.submit') }} + </button> </div> - <div class='alert error' v-if="error"> + <div + v-if="error" + class="alert error" + > Error: {{ error }} - <i class="button-icon icon-cancel" @click="clearError"></i> + <i + class="button-icon icon-cancel" + @click="clearError" + /> </div> <div class="attachments"> - <div class="media-upload-wrapper" v-for="file in newStatus.files"> - <i class="fa button-icon icon-cancel" @click="removeMediaFile(file)"></i> + <div + v-for="file in newStatus.files" + :key="file.url" + class="media-upload-wrapper" + > + <i + class="fa button-icon icon-cancel" + @click="removeMediaFile(file)" + /> <div class="media-upload-container attachment"> - <img class="thumbnail media-upload" :src="file.url" v-if="type(file) === 'image'"></img> - <video v-if="type(file) === 'video'" :src="file.url" controls></video> - <audio v-if="type(file) === 'audio'" :src="file.url" controls></audio> - <a v-if="type(file) === 'unknown'" :href="file.url">{{file.url}}</a> + <img + v-if="type(file) === 'image'" + class="thumbnail media-upload" + :src="file.url" + > + <video + v-if="type(file) === 'video'" + :src="file.url" + controls + /> + <audio + v-if="type(file) === 'audio'" + :src="file.url" + controls + /> + <a + v-if="type(file) === 'unknown'" + :href="file.url" + >{{ file.url }}</a> </div> </div> </div> - <div class="upload_settings" v-if="newStatus.files.length > 0"> - <input type="checkbox" id="filesSensitive" v-model="newStatus.nsfw"> - <label for="filesSensitive">{{$t('post_status.attachments_sensitive')}}</label> + <div + v-if="newStatus.files.length > 0" + class="upload_settings" + > + <input + id="filesSensitive" + v-model="newStatus.nsfw" + type="checkbox" + > + <label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label> </div> </form> + <sticker-picker + v-if="stickerPickerVisible" + ref="stickerPicker" + @uploaded="addMediaFile" + /> </div> </template> @@ -136,11 +292,11 @@ .visibility-tray { display: flex; justify-content: space-between; - flex-direction: row-reverse; + padding-top: 5px; } } -.post-status-form, .login { +.post-status-form { .form-bottom { display: flex; padding: 0.5em; @@ -157,6 +313,37 @@ } } + .form-bottom-left { + display: flex; + flex: 1; + } + + .text-format { + .only-format { + color: $fallback--faint; + color: var(--faint, $fallback--faint); + } + } + + .poll-icon, .sticker-icon { + font-size: 26px; + flex: 1; + + .selected { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + } + + .sticker-icon { + flex: 0; + min-width: 50px; + } + + .icon-chart-bar { + cursor: pointer; + } + .error { text-align: center; } @@ -224,7 +411,6 @@ } } - .btn { cursor: pointer; } @@ -242,7 +428,7 @@ .form-group { display: flex; flex-direction: column; - padding: 0.3em 0.5em 0.6em; + padding: 0.25em 0.5em 0.5em; line-height:24px; } @@ -254,19 +440,38 @@ min-height: 1px; } - form textarea.form-control { - line-height:16px; + .form-post-body { + height: 16px; // Only affects the empty-height + line-height: 16px; resize: none; overflow: hidden; transition: min-height 200ms 100ms; + padding-bottom: 1.75em; min-height: 1px; box-sizing: content-box; } - form textarea.form-control:focus { + .form-post-body:focus { min-height: 48px; } + .main-input { + position: relative; + } + + .character-counter { + position: absolute; + bottom: 0; + right: 0; + padding: 0; + margin: 0 0.5em; + + &.error { + color: $fallback--cRed; + color: var(--cRed, $fallback--cRed); + } + } + .btn { cursor: pointer; } |
