Feature/polls attempt 2

This commit is contained in:
lain 2019-06-18 20:28:31 +00:00 committed by Shpuld Shpludson
parent 69eff65130
commit 0eed2ccca8
56 changed files with 1364 additions and 1458 deletions

View file

@ -13,7 +13,7 @@
<style>
.media-upload {
font-size: 26px;
flex: 1;
min-width: 50px;
}
.icon-upload {

View file

@ -1,6 +1,7 @@
import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -13,7 +14,10 @@ const Notification = {
},
props: [ 'notification' ],
components: {
Status, UserAvatar, UserCard
Status,
UserAvatar,
UserCard,
Timeago
},
methods: {
toggleUserExpanded () {

View file

@ -30,12 +30,12 @@
</div>
<div class="timeago" v-if="notification.type === 'follow'">
<span class="faint">
<timeago :since="notification.created_at" :auto-update="240"></timeago>
<Timeago :time="notification.created_at" :auto-update="240"></Timeago>
</span>
</div>
<div class="timeago" v-else>
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
<timeago :since="notification.created_at" :auto-update="240"></timeago>
<Timeago :time="notification.created_at" :auto-update="240"></Timeago>
</router-link>
</div>
</span>

107
src/components/poll/poll.js Normal file
View file

@ -0,0 +1,107 @@
import Timeago from '../timeago/timeago.vue'
import { forEach, map } from 'lodash'
export default {
name: 'Poll',
props: ['poll', 'statusId'],
components: { Timeago },
data () {
return {
loading: false,
choices: [],
refreshInterval: null
}
},
created () {
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000)
// Initialize choices to booleans and set its length to match options
this.choices = this.poll.options.map(_ => false)
},
destroyed () {
clearTimeout(this.refreshInterval)
},
computed: {
expired () {
return Date.now() > Date.parse(this.poll.expires_at)
},
loggedIn () {
return this.$store.state.users.currentUser
},
showResults () {
return this.poll.voted || this.expired || !this.loggedIn
},
totalVotesCount () {
return this.poll.votes_count
},
expiresAt () {
return Date.parse(this.poll.expires_at).toLocaleString()
},
containerClass () {
return {
loading: this.loading
}
},
choiceIndices () {
// Convert array of booleans into an array of indices of the
// items that were 'true', so [true, false, false, true] becomes
// [0, 3].
return this.choices
.map((entry, index) => entry && index)
.filter(value => typeof value === 'number')
},
isDisabled () {
const noChoice = this.choiceIndices.length === 0
return this.loading || noChoice
}
},
methods: {
refreshPoll () {
if (this.expired) return
this.fetchPoll()
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000)
},
percentageForOption (count) {
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
},
resultTitle (option) {
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
},
fetchPoll () {
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
},
activateOption (index) {
// forgive me father: doing checking the radio/checkboxes
// in code because of customized input elements need either
// a) an extra element for the actual graphic, or b) use a
// pseudo element for the label. We use b) which mandates
// using "for" and "id" matching which isn't nice when the
// same poll appears multiple times on the site (notifs and
// timeline for example). With code we can make sure it just
// works without altering the pseudo element implementation.
const allElements = this.$el.querySelectorAll('input')
const clickedElement = this.$el.querySelector(`input[value="${index}"]`)
if (this.poll.multiple) {
// Checkboxes, toggle only the clicked one
clickedElement.checked = !clickedElement.checked
} else {
// Radio button, uncheck everything and check the clicked one
forEach(allElements, element => { element.checked = false })
clickedElement.checked = true
}
this.choices = map(allElements, e => e.checked)
},
optionId (index) {
return `poll${this.poll.id}-${index}`
},
vote () {
if (this.choiceIndices.length === 0) return
this.loading = true
this.$store.dispatch(
'votePoll',
{ id: this.statusId, pollId: this.poll.id, choices: this.choiceIndices }
).then(poll => {
this.loading = false
})
}
}
}

View file

@ -0,0 +1,117 @@
<template>
<div class="poll" v-bind:class="containerClass">
<div
class="poll-option"
v-for="(option, index) in poll.options"
:key="index"
>
<div v-if="showResults" :title="resultTitle(option)" class="option-result">
<div class="option-result-label">
<span class="result-percentage">
{{percentageForOption(option.votes_count)}}%
</span>
<span>{{option.title}}</span>
</div>
<div
class="result-fill"
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
>
</div>
</div>
<div v-else @click="activateOption(index)">
<input
v-if="poll.multiple"
type="checkbox"
:disabled="loading"
:value="index"
>
<input
v-else
type="radio"
:disabled="loading"
:value="index"
>
<label>
{{option.title}}
</label>
</div>
</div>
<div class="footer faint">
<button
v-if="!showResults"
class="btn btn-default poll-vote-button"
type="button"
@click="vote"
:disabled="isDisabled"
>
{{$t('polls.vote')}}
</button>
<div class="total">
{{totalVotesCount}} {{ $t("polls.votes") }}&nbsp;·&nbsp;
</div>
<i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
<Timeago :time="this.poll.expires_at" :auto-update="60" :now-threshold="0" />
</i18n>
</div>
</div>
</template>
<script src="./poll.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.poll {
.votes {
display: flex;
flex-direction: column;
margin: 0 0 0.5em;
}
.poll-option {
margin: 0.5em 0;
height: 1.5em;
}
.option-result {
height: 100%;
display: flex;
flex-direction: row;
position: relative;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
.option-result-label {
display: flex;
align-items: center;
padding: 0.1em 0.25em;
z-index: 1;
}
.result-percentage {
width: 3.5em;
}
.result-fill {
height: 100%;
position: absolute;
background-color: $fallback--lightBg;
background-color: var(--linkBg, $fallback--lightBg);
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
top: 0;
left: 0;
transition: width 0.5s;
}
input {
width: 3.5em;
}
.footer {
display: flex;
align-items: center;
}
&.loading * {
cursor: progress;
}
.poll-vote-button {
padding: 0 0.5em;
margin-right: 0.5em;
}
}
</style>

View file

@ -0,0 +1,121 @@
import * as DateUtils from 'src/services/date_utils/date_utils.js'
import { uniq } from 'lodash'
export default {
name: 'PollForm',
props: ['visible'],
data: () => ({
pollType: 'single',
options: ['', ''],
expiryAmount: 10,
expiryUnit: 'minutes'
}),
computed: {
pollLimits () {
return this.$store.state.instance.pollLimits
},
maxOptions () {
return this.pollLimits.max_options
},
maxLength () {
return this.pollLimits.max_option_chars
},
expiryUnits () {
const allUnits = ['minutes', 'hours', 'days']
const expiry = this.convertExpiryFromUnit
return allUnits.filter(
unit => this.pollLimits.max_expiration >= expiry(unit, 1)
)
},
minExpirationInCurrentUnit () {
return Math.ceil(
this.convertExpiryToUnit(
this.expiryUnit,
this.pollLimits.min_expiration
)
)
},
maxExpirationInCurrentUnit () {
return Math.floor(
this.convertExpiryToUnit(
this.expiryUnit,
this.pollLimits.max_expiration
)
)
}
},
methods: {
clear () {
this.pollType = 'single'
this.options = ['', '']
this.expiryAmount = 10
this.expiryUnit = 'minutes'
},
nextOption (index) {
const element = this.$el.querySelector(`#poll-${index + 1}`)
if (element) {
element.focus()
} else {
// Try adding an option and try focusing on it
const addedOption = this.addOption()
if (addedOption) {
this.$nextTick(function () {
this.nextOption(index)
})
}
}
},
addOption () {
if (this.options.length < this.maxOptions) {
this.options.push('')
return true
}
return false
},
deleteOption (index, event) {
if (this.options.length > 2) {
this.options.splice(index, 1)
}
},
convertExpiryToUnit (unit, amount) {
// Note: we want seconds and not milliseconds
switch (unit) {
case 'minutes': return (1000 * amount) / DateUtils.MINUTE
case 'hours': return (1000 * amount) / DateUtils.HOUR
case 'days': return (1000 * amount) / DateUtils.DAY
}
},
convertExpiryFromUnit (unit, amount) {
// Note: we want seconds and not milliseconds
switch (unit) {
case 'minutes': return 0.001 * amount * DateUtils.MINUTE
case 'hours': return 0.001 * amount * DateUtils.HOUR
case 'days': return 0.001 * amount * DateUtils.DAY
}
},
expiryAmountChange () {
this.expiryAmount =
Math.max(this.minExpirationInCurrentUnit, this.expiryAmount)
this.expiryAmount =
Math.min(this.maxExpirationInCurrentUnit, this.expiryAmount)
this.updatePollToParent()
},
updatePollToParent () {
const expiresIn = this.convertExpiryFromUnit(
this.expiryUnit,
this.expiryAmount
)
const options = uniq(this.options.filter(option => option !== ''))
if (options.length < 2) {
this.$emit('update-poll', { error: this.$t('polls.not_enough_options') })
return
}
this.$emit('update-poll', {
options,
multiple: this.pollType === 'multiple',
expiresIn
})
}
}
}

View file

@ -0,0 +1,133 @@
<template>
<div class="poll-form" v-if="visible">
<div class="poll-option" v-for="(option, index) in options" :key="index">
<div class="input-container">
<input
class="poll-option-input"
type="text"
:placeholder="$t('polls.option')"
:maxlength="maxLength"
:id="`poll-${index}`"
v-model="options[index]"
@change="updatePollToParent"
@keydown.enter.stop.prevent="nextOption(index)"
>
</div>
<div class="icon-container" v-if="options.length > 2">
<i class="icon-cancel" @click="deleteOption(index)"></i>
</div>
</div>
<a
v-if="options.length < maxOptions"
class="add-option faint"
@click="addOption"
>
<i class="icon-plus" />
{{ $t("polls.add_option") }}
</a>
<div class="poll-type-expiry">
<div class="poll-type" :title="$t('polls.type')">
<label for="poll-type-selector" class="select">
<select class="select" v-model="pollType" @change="updatePollToParent">
<option value="single">{{$t('polls.single_choice')}}</option>
<option value="multiple">{{$t('polls.multiple_choices')}}</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
<div class="poll-expiry" :title="$t('polls.expiry')">
<input
type="number"
class="expiry-amount hide-number-spinner"
:min="minExpirationInCurrentUnit"
:max="maxExpirationInCurrentUnit"
v-model="expiryAmount"
@change="expiryAmountChange"
>
<label class="expiry-unit select">
<select
v-model="expiryUnit"
@change="expiryAmountChange"
>
<option v-for="unit in expiryUnits" :value="unit">
{{ $t(`time.${unit}_short`, ['']) }}
</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
</div>
</div>
</template>
<script src="./poll_form.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.poll-form {
display: flex;
flex-direction: column;
padding: 0 0.5em 0.5em;
.add-option {
align-self: flex-start;
padding-top: 0.25em;
cursor: pointer;
}
.poll-option {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 0.25em;
}
.input-container {
width: 100%;
input {
// Hack: dodge the floating X icon
padding-right: 2.5em;
width: 100%;
}
}
.icon-container {
// Hack: Move the icon over the input box
width: 2em;
margin-left: -2em;
z-index: 1;
}
.poll-type-expiry {
margin-top: 0.5em;
display: flex;
width: 100%;
}
.poll-type {
margin-right: 0.75em;
flex: 1 1 60%;
.select {
border: none;
box-shadow: none;
background-color: transparent;
}
}
.poll-expiry {
display: flex;
.expiry-amount {
width: 3em;
text-align: right;
}
.expiry-unit {
border: none;
box-shadow: none;
background-color: transparent;
}
}
}
</style>

View file

@ -2,6 +2,7 @@ import statusPoster from '../../services/status_poster/status_poster.service.js'
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 PollForm from '../poll/poll_form.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { reject, map, uniqBy } from 'lodash'
import suggestor from '../emoji-input/suggestor.js'
@ -31,8 +32,9 @@ const PostStatusForm = {
],
components: {
MediaUpload,
ScopeSelector,
EmojiInput
EmojiInput,
PollForm,
ScopeSelector
},
mounted () {
this.resize(this.$refs.textarea)
@ -75,10 +77,12 @@ const PostStatusForm = {
status: statusText,
nsfw: false,
files: [],
poll: {},
visibility: scope,
contentType
},
caret: 0
caret: 0,
pollFormVisible: false
}
},
computed: {
@ -153,8 +157,17 @@ const PostStatusForm = {
safeDMEnabled () {
return this.$store.state.instance.safeDM
},
pollsAvailable () {
return this.$store.state.instance.pollsAvailable &&
this.$store.state.instance.pollLimits.max_options >= 2
},
hideScopeNotice () {
return this.$store.state.config.hideScopeNotice
},
pollContentError () {
return this.pollFormVisible &&
this.newStatus.poll &&
this.newStatus.poll.error
}
},
methods: {
@ -171,6 +184,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,
@ -180,7 +199,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 = {
@ -188,9 +208,12 @@ const PostStatusForm = {
spoilerText: '',
files: [],
visibility: newStatus.visibility,
contentType: newStatus.contentType
contentType: newStatus.contentType,
poll: {}
}
this.pollFormVisible = false
this.$refs.mediaUpload.clearFile()
this.clearPollForm()
this.$emit('posted')
let el = this.$el.querySelector('textarea')
el.style.height = 'auto'
@ -261,6 +284,17 @@ const PostStatusForm = {
changeVis (visibility) {
this.newStatus.visibility = visibility
},
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 })
}

View file

@ -1,6 +1,6 @@
<template>
<div class="post-status-form">
<form @submit.prevent="postStatus(newStatus)">
<form @submit.prevent="postStatus(newStatus)" autocomplete="off">
<div class="form-group" >
<i18n
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
@ -91,37 +91,52 @@
:onScopeChange="changeVis"/>
</div>
</div>
<div class='form-bottom'>
<poll-form
ref="pollForm"
v-if="pollsAvailable"
:visible="pollFormVisible"
@update-poll="setPoll"
/>
<div class='form-bottom'>
<div class='form-bottom-left'>
<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>
</div>
<div class='alert error' v-if="error">
Error: {{ error }}
<i class="button-icon icon-cancel" @click="clearError"></i>
</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 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>
</div>
<div v-if="pollsAvailable" class="poll-icon">
<i
:title="$t('polls.add_poll')"
@click="togglePollForm"
class="icon-chart-bar btn btn-default"
:class="pollFormVisible && 'selected'"
/>
</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>
<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>
</div>
<div class='alert error' v-if="error">
Error: {{ error }}
<i class="button-icon icon-cancel" @click="clearError"></i>
</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 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>
</div>
</div>
</form>
</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>
</form>
</div>
</template>
<script src="./post_status_form.js"></script>
@ -172,6 +187,11 @@
}
}
.form-bottom-left {
display: flex;
flex: 1;
}
.text-format {
.only-format {
color: $fallback--faint;
@ -179,6 +199,20 @@
}
}
.poll-icon {
font-size: 26px;
flex: 1;
.selected {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
}
.icon-chart-bar {
cursor: pointer;
}
.error {
text-align: center;
@ -240,7 +274,6 @@
}
}
.btn {
cursor: pointer;
}

View file

@ -302,4 +302,4 @@
</template>
<script src="./settings.js">
</script>
</script>

View file

@ -1,6 +1,7 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import Poll from '../poll/poll.vue'
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCard from '../user_card/user_card.vue'
@ -8,6 +9,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
@ -216,8 +218,8 @@ const Status = {
if (!this.status.summary) return ''
const decodedSummary = unescape(this.status.summary)
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
? this.$store.state.instance.subjectLineBehavior
: this.$store.state.config.subjectLineBehavior
? this.$store.state.instance.subjectLineBehavior
: this.$store.state.config.subjectLineBehavior
const startsWithRe = decodedSummary.match(/^re[: ]/i)
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
return decodedSummary
@ -285,11 +287,13 @@ const Status = {
RetweetButton,
ExtraButtons,
PostStatusForm,
Poll,
UserCard,
UserAvatar,
Gallery,
LinkPreview,
AvatarList
AvatarList,
Timeago
},
methods: {
visibilityIcon (visibility) {
@ -377,7 +381,7 @@ const Status = {
this.preview = find(statuses, { 'id': targetId })
// or if we have to fetch it
if (!this.preview) {
this.$store.state.api.backendInteractor.fetchStatus({id}).then((status) => {
this.$store.state.api.backendInteractor.fetchStatus({ id }).then((status) => {
this.preview = status
})
}

View file

@ -52,7 +52,7 @@
<span class="heading-right">
<router-link class="timeago faint-link" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
<Timeago :time="status.created_at" :auto-update="60"></Timeago>
</router-link>
<div class="button-icon visibility-icon" v-if="status.visibility">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
@ -123,6 +123,10 @@
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">{{$t("general.show_less")}}</a>
</div>
<div v-if="status.poll && status.poll.options">
<poll :poll="status.poll" :status-id="status.id" />
</div>
<div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
<attachment
class="non-gallery"

View file

@ -0,0 +1,48 @@
<template>
<time :datetime="time" :title="localeDateString">
{{ $t(relativeTime.key, [relativeTime.num]) }}
</time>
</template>
<script>
import * as DateUtils from 'src/services/date_utils/date_utils.js'
export default {
name: 'Timeago',
props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold'],
data () {
return {
relativeTime: { key: 'time.now', num: 0 },
interval: null
}
},
created () {
this.refreshRelativeTimeObject()
},
destroyed () {
clearTimeout(this.interval)
},
computed: {
localeDateString () {
return typeof this.time === 'string'
? new Date(Date.parse(this.time)).toLocaleString()
: this.time.toLocaleString()
}
},
methods: {
refreshRelativeTimeObject () {
const nowThreshold = typeof this.nowThreshold === 'number' ? this.nowThreshold : 1
this.relativeTime = this.longFormat
? DateUtils.relativeTime(this.time, nowThreshold)
: DateUtils.relativeTimeShort(this.time, nowThreshold)
if (this.autoUpdate) {
this.interval = setTimeout(
this.refreshRelativeTimeObject,
1000 * this.autoUpdate
)
}
}
}
}
</script>