| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 | <template lang="pug">  div(v-intersect.once='onIntersect')    v-textarea#discussion-new(      outlined      flat      :placeholder='$t(`common:comments.newPlaceholder`)'      auto-grow      dense      rows='3'      hide-details      v-model='newcomment'      color='blue-grey darken-2'      :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'      v-if='permissions.write'      :aria-label='$t(`common:comments.fieldContent`)'    )    v-row.mt-2(dense, v-if='!isAuthenticated && permissions.write')      v-col(cols='12', lg='6')        v-text-field(          outlined          color='blue-grey darken-2'          :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'          :placeholder='$t(`common:comments.fieldName`)'          hide-details          dense          autocomplete='name'          v-model='guestName'          :aria-label='$t(`common:comments.fieldName`)'        )      v-col(cols='12', lg='6')        v-text-field(          outlined          color='blue-grey darken-2'          :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'          :placeholder='$t(`common:comments.fieldEmail`)'          hide-details          type='email'          dense          autocomplete='email'          v-model='guestEmail'          :aria-label='$t(`common:comments.fieldEmail`)'        )    .d-flex.align-center.pt-3(v-if='permissions.write')      v-icon.mr-1(color='blue-grey') mdi-language-markdown-outline      .caption.blue-grey--text {{$t('common:comments.markdownFormat')}}      v-spacer      .caption.mr-3(v-if='isAuthenticated')        i18next(tag='span', path='common:comments.postingAs')          strong(place='name') {{userDisplayName}}      v-btn(        dark        color='blue-grey darken-2'        @click='postComment'        depressed        :aria-label='$t(`common:comments.postComment`)'        )        v-icon(left) mdi-comment        span.text-none {{$t('common:comments.postComment')}}    v-divider.mt-3(v-if='permissions.write')    .pa-5.d-flex.align-center.justify-center(v-if='isLoading && !hasLoadedOnce')      v-progress-circular(        indeterminate        size='20'        width='1'        color='blue-grey'      )      .caption.blue-grey--text.pl-3: em {{$t('common:comments.loading')}}    v-timeline(      dense      v-else-if='comments && comments.length > 0'      )      v-timeline-item.comments-post(        color='pink darken-4'        large        v-for='cm of comments'        :key='`comment-` + cm.id'        :id='`comment-post-id-` + cm.id'        )        template(v-slot:icon)          v-avatar(color='blue-grey')            //- v-img(src='http://i.pravatar.cc/64')            span.white--text.title {{cm.initials}}        v-card.elevation-1          v-card-text            .comments-post-actions(v-if='permissions.manage && !isBusy && commentEditId === 0')              v-icon.mr-3(small, @click='editComment(cm)') mdi-pencil              v-icon(small, @click='deleteCommentConfirm(cm)') mdi-delete            .comments-post-name.caption: strong {{cm.authorName}}            .comments-post-date.overline.grey--text {{cm.createdAt | moment('from') }} #[em(v-if='cm.createdAt !== cm.updatedAt') - {{$t('common:comments.modified', { reldate: $options.filters.moment(cm.updatedAt, 'from') })}}]            .comments-post-content.mt-3(v-if='commentEditId !== cm.id', v-html='cm.render')            .comments-post-editcontent.mt-3(v-else)              v-textarea(                outlined                flat                auto-grow                dense                rows='3'                hide-details                v-model='commentEditContent'                color='blue-grey darken-2'                :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'              )              .d-flex.align-center.pt-3                v-spacer                v-btn.mr-3(                  dark                  color='blue-grey darken-2'                  @click='editCommentCancel'                  outlined                  )                  v-icon(left) mdi-close                  span.text-none {{$t('common:actions.cancel')}}                v-btn(                  dark                  color='blue-grey darken-2'                  @click='updateComment'                  depressed                  )                  v-icon(left) mdi-comment                  span.text-none {{$t('common:comments.updateComment')}}    .pt-5.text-center.body-2.blue-grey--text(v-else-if='permissions.write') {{$t('common:comments.beFirst')}}    .text-center.body-2.blue-grey--text(v-else) {{$t('common:comments.none')}}    v-dialog(v-model='deleteCommentDialogShown', max-width='500')      v-card        .dialog-header.is-red {{$t('common:comments.deleteConfirmTitle')}}        v-card-text.pt-5          span {{$t('common:comments.deleteWarn')}}          .caption: strong {{$t('common:comments.deletePermanentWarn')}}        v-card-chin          v-spacer          v-btn(text, @click='deleteCommentDialogShown = false') {{$t('common:actions.cancel')}}          v-btn(color='red', dark, @click='deleteComment') {{$t('common:actions.delete')}}</template><script>import gql from 'graphql-tag'import { get } from 'vuex-pathify'import validate from 'validate.js'import _ from 'lodash'export default {  data () {    return {      newcomment: '',      isLoading: true,      hasLoadedOnce: false,      comments: [],      guestName: '',      guestEmail: '',      commentToDelete: {},      commentEditId: 0,      commentEditContent: null,      deleteCommentDialogShown: false,      isBusy: false,      scrollOpts: {        duration: 1500,        offset: 0,        easing: 'easeInOutCubic'      }    }  },  computed: {    pageId: get('page/id'),    permissions: get('page/effectivePermissions@comments'),    isAuthenticated: get('user/authenticated'),    userDisplayName: get('user/name')  },  methods: {    onIntersect (entries, observer, isIntersecting) {      if (isIntersecting) {        this.fetch(true)      }    },    async fetch (silent = false) {      this.isLoading = true      try {        const results = await this.$apollo.query({          query: gql`            query ($locale: String!, $path: String!) {              comments {                list(locale: $locale, path: $path) {                  id                  render                  authorName                  createdAt                  updatedAt                }              }            }          `,          variables: {            locale: this.$store.get('page/locale'),            path: this.$store.get('page/path')          },          fetchPolicy: 'network-only'        })        this.comments = _.get(results, 'data.comments.list', []).map(c => {          const nameParts = c.authorName.toUpperCase().split(' ')          let initials = _.head(nameParts).charAt(0)          if (nameParts.length > 1) {            initials += _.last(nameParts).charAt(0)          }          c.initials = initials          return c        })      } catch (err) {        console.warn(err)        if (!silent) {          this.$store.commit('showNotification', {            style: 'red',            message: err.message,            icon: 'alert'          })        }      }      this.isLoading = false      this.hasLoadedOnce = true    },    /**     * Post New Comment     */    async postComment () {      let rules = {        comment: {          presence: {            allowEmpty: false          },          length: {            minimum: 2          }        }      }      if (!this.isAuthenticated && this.permissions.write) {        rules.name = {          presence: {            allowEmpty: false          },          length: {            minimum: 2,            maximum: 255          }        }        rules.email = {          presence: {            allowEmpty: false          },          email: true        }      }      const validationResults = validate({        comment: this.newcomment,        name: this.guestName,        email: this.guestEmail      }, rules, { format: 'flat' })      if (validationResults) {        this.$store.commit('showNotification', {          style: 'red',          message: validationResults[0],          icon: 'alert'        })        return      }      try {        const resp = await this.$apollo.mutate({          mutation: gql`            mutation (              $pageId: Int!              $replyTo: Int              $content: String!              $guestName: String              $guestEmail: String            ) {              comments {                create (                  pageId: $pageId                  replyTo: $replyTo                  content: $content                  guestName: $guestName                  guestEmail: $guestEmail                ) {                  responseResult {                    succeeded                    errorCode                    slug                    message                  }                  id                }              }            }          `,          variables: {            pageId: this.pageId,            replyTo: 0,            content: this.newcomment,            guestName: this.guestName,            guestEmail: this.guestEmail          }        })        if (_.get(resp, 'data.comments.create.responseResult.succeeded', false)) {          this.$store.commit('showNotification', {            style: 'success',            message: this.$t('common:comments.postSuccess'),            icon: 'check'          })          this.newcomment = ''          await this.fetch()          this.$nextTick(() => {            this.$vuetify.goTo(`#comment-post-id-${_.get(resp, 'data.comments.create.id', 0)}`, this.scrollOpts)          })        } else {          throw new Error(_.get(resp, 'data.comments.create.responseResult.message', 'An unexpected error occurred.'))        }      } catch (err) {        this.$store.commit('showNotification', {          style: 'red',          message: err.message,          icon: 'alert'        })      }    },    /**     * Show Comment Editing Form     */    async editComment (cm) {      this.$store.commit(`loadingStart`, 'comments-edit')      this.isBusy = true      try {        const results = await this.$apollo.query({          query: gql`            query ($id: Int!) {              comments {                single(id: $id) {                  content                }              }            }          `,          variables: {            id: cm.id          },          fetchPolicy: 'network-only'        })        this.commentEditContent = _.get(results, 'data.comments.single.content', null)        if (this.commentEditContent === null) {          throw new Error('Failed to load comment content.')        }      } catch (err) {        console.warn(err)        this.$store.commit('showNotification', {          style: 'red',          message: err.message,          icon: 'alert'        })      }      this.commentEditId = cm.id      this.isBusy = false      this.$store.commit(`loadingStop`, 'comments-edit')    },    /**     * Cancel Comment Edit     */    editCommentCancel () {      this.commentEditId = 0      this.commentEditContent = null    },    /**     * Update Comment with new content     */    async updateComment () {      this.$store.commit(`loadingStart`, 'comments-edit')      this.isBusy = true      try {        if (this.commentEditContent.length < 2) {          throw new Error(this.$t('common:comments.contentMissingError'))        }        const resp = await this.$apollo.mutate({          mutation: gql`            mutation (              $id: Int!              $content: String!            ) {              comments {                update (                  id: $id,                  content: $content                ) {                  responseResult {                    succeeded                    errorCode                    slug                    message                  }                  render                }              }            }          `,          variables: {            id: this.commentEditId,            content: this.commentEditContent          }        })        if (_.get(resp, 'data.comments.update.responseResult.succeeded', false)) {          this.$store.commit('showNotification', {            style: 'success',            message: this.$t('common:comments.updateSuccess'),            icon: 'check'          })          const cm = _.find(this.comments, ['id', this.commentEditId])          cm.render = _.get(resp, 'data.comments.update.render', '-- Failed to load updated comment --')          cm.updatedAt = (new Date()).toISOString()          this.editCommentCancel()        } else {          throw new Error(_.get(resp, 'data.comments.delete.responseResult.message', 'An unexpected error occurred.'))        }      } catch (err) {        console.warn(err)        this.$store.commit('showNotification', {          style: 'red',          message: err.message,          icon: 'alert'        })      }      this.isBusy = false      this.$store.commit(`loadingStop`, 'comments-edit')    },    /**     * Show Delete Comment Confirmation Dialog     */    deleteCommentConfirm (cm) {      this.commentToDelete = cm      this.deleteCommentDialogShown = true    },    /**     * Delete Comment     */    async deleteComment () {      this.$store.commit(`loadingStart`, 'comments-delete')      this.isBusy = true      this.deleteCommentDialogShown = false      try {        const resp = await this.$apollo.mutate({          mutation: gql`            mutation (              $id: Int!            ) {              comments {                delete (                  id: $id                ) {                  responseResult {                    succeeded                    errorCode                    slug                    message                  }                }              }            }          `,          variables: {            id: this.commentToDelete.id          }        })        if (_.get(resp, 'data.comments.delete.responseResult.succeeded', false)) {          this.$store.commit('showNotification', {            style: 'success',            message: this.$t('common:comments.deleteSuccess'),            icon: 'check'          })          this.comments = _.reject(this.comments, ['id', this.commentToDelete.id])        } else {          throw new Error(_.get(resp, 'data.comments.delete.responseResult.message', 'An unexpected error occurred.'))        }      } catch (err) {        this.$store.commit('showNotification', {          style: 'red',          message: err.message,          icon: 'alert'        })      }      this.isBusy = false      this.$store.commit(`loadingStop`, 'comments-delete')    }  }}</script><style lang="scss">.comments-post {  position: relative;  &:hover {    .comments-post-actions {      opacity: 1;    }  }  &-actions {    position: absolute;    top: 16px;    right: 16px;    opacity: 0;    transition: opacity .4s ease;  }  &-content {    > p:first-child {      padding-top: 0;    }    p {      padding-top: 1rem;      margin-bottom: 0;    }    img {      max-width: 100%;      border-radius: 5px;    }    code {      background-color: rgba(mc('pink', '500'), .1);      box-shadow: none;    }    pre > code {      margin-top: 1rem;      padding: 12px;      background-color: #111;      box-shadow: none;      border-radius: 5px;      width: 100%;      color: #FFF;      font-weight: 400;      font-size: .85rem;      font-family: Roboto Mono, monospace;    }  }}</style>
 |