123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- <template>
- <b-modal
- centered
- v-model="isOpen"
- body-class="p-0"
- footer-class="d-flex justify-content-between align-items-center">
- <template #modal-header="{ close }">
- <div class="d-flex flex-grow-1 justify-content-between align-items-center">
- <span style="width:40px;"></span>
- <h5 class="font-weight-bold mb-0">Edit Post</h5>
- <b-button size="sm" variant="link" @click="close()">
- <i class="far fa-times text-dark fa-lg"></i>
- </b-button>
- </div>
- </template>
- <b-card
- v-if="isLoading"
- no-body
- flush
- class="shadow-none p-0">
- <b-card-body style="min-height:300px" class="d-flex align-items-center justify-content-center">
- <div class="d-flex justify-content-center align-items-center flex-column" style="gap: 0.4rem;">
- <b-spinner variant="primary" />
- <p class="small mb-0 font-weight-lighter">Loading Post...</p>
- </div>
- </b-card-body>
- </b-card>
- <b-card
- v-else-if="!isLoading && isOpen && status && status.id"
- no-body
- flush
- class="shadow-none p-0">
- <b-card-header header-tag="nav">
- <b-nav tabs fill card-header>
- <b-nav-item :active="tabIndex === 0" @click="toggleTab(0)">Caption</b-nav-item>
- <b-nav-item :active="tabIndex === 1" @click="toggleTab(1)">Media</b-nav-item>
- <!-- <b-nav-item :active="tabIndex === 2" @click="toggleTab(2)">Audience</b-nav-item> -->
- <b-nav-item :active="tabIndex === 4" @click="toggleTab(3)">Other</b-nav-item>
- </b-nav>
- </b-card-header>
- <b-card-body style="min-height:300px">
- <template v-if="tabIndex === 0">
- <p class="font-weight-bold small">Caption</p>
- <div class="media mb-0">
- <div class="media-body">
- <div class="form-group">
- <label class="font-weight-bold text-muted small d-none">Caption</label>
- <vue-tribute :options="tributeSettings">
- <textarea
- class="form-control border-0 rounded-0 no-focus"
- rows="4"
- placeholder="Write a caption..."
- v-model="fields.caption"
- :maxlength="config.uploader.max_caption_length"
- v-on:keyup="composeTextLength = fields.caption.length"></textarea>
- </vue-tribute>
- <p class="help-text small text-right text-muted mb-0">{{composeTextLength}}/{{config.uploader.max_caption_length}}</p>
- </div>
- </div>
- </div>
- <hr />
- <p class="font-weight-bold small">Sensitive/NSFW</p>
- <div class="border py-2 px-3 bg-light rounded">
- <b-form-checkbox v-model="fields.sensitive" name="check-button" switch style="font-weight:300">
- <span class="ml-1 small">Contains spoilers, sensitive or nsfw content</span>
- </b-form-checkbox>
- </div>
- <transition name="slide-fade">
- <div v-if="fields.sensitive" class="form-group mt-3">
- <label class="font-weight-bold small">Content Warning</label>
- <textarea
- class="form-control"
- rows="2"
- placeholder="Add an optional spoiler/content warning..."
- :maxlength="140"
- v-model="fields.spoiler_text"></textarea>
- <p class="help-text small text-right text-muted mb-0">{{fields.spoiler_text ? fields.spoiler_text.length : 0}}/140</p>
- </div>
- </transition>
- </template>
- <template v-else-if="tabIndex === 1">
- <div class="list-group">
- <div
- class="list-group-item"
- v-for="(media, idx) in fields.media"
- :key="'edm:' + media.id + ':' + idx">
- <div class="d-flex justify-content-between align-items-center">
- <template v-if="media.type === 'image'">
- <img
- :src="media.url"
- width="40"
- height="40"
- style="object-fit: cover;"
- class="bg-light rounded cursor-pointer"
- @click="toggleLightbox"
- />
- </template>
- <p class="d-none d-lg-block mb-0"><span class="small font-weight-light">{{ media.mime }}</span></p>
- <button
- class="btn btn-sm font-weight-bold rounded-pill px-4"
- style="font-size: 13px"
- :class="[ media.description && media.description.length ? 'btn-success' : 'btn-outline-muted']"
- @click.prevent="handleAddAltText(idx)"
- >
- {{ media.description && media.description.length ? 'Edit Alt Text' : 'Add Alt Text' }}
- </button>
- <div v-if="fields.media && fields.media.length > 1" class="btn-group">
- <a
- class="btn btn-outline-secondary btn-sm"
- href="#"
- :disabled="idx === 0"
- :class="{ disabled: idx === 0}"
- @click.prevent="toggleMediaOrder('prev', idx)">
- <i class="fas fa-arrow-alt-up"></i>
- </a>
- <a
- class="btn btn-outline-secondary btn-sm"
- href="#"
- :disabled="idx === fields.media.length - 1"
- :class="{ disabled: idx === fields.media.length - 1}"
- @click.prevent="toggleMediaOrder('next', idx)">
- <i class="fas fa-arrow-alt-down"></i>
- </a>
- </div>
- <button
- class="btn btn-outline-danger btn-sm"
- v-if="fields.media && fields.media.length && fields.media.length > 1"
- @click.prevent="removeMedia(idx)">
- <i class="far fa-trash-alt"></i>
- </button>
- </div>
- <transition name="slide-fade">
- <template v-if="altTextEditIndex === idx">
- <div class="form-group mt-1">
- <label class="font-weight-bold small">Alt Text</label>
- <b-form-textarea
- v-model="media.description"
- placeholder="Describe your image for the visually impaired..."
- rows="3"
- max-rows="6"
- @input="handleAltTextUpdate(idx)"
- ></b-form-textarea>
- <div class="d-flex justify-content-between">
- <a class="font-weight-bold small text-muted" href="#" @click.prevent="altTextEditIndex = undefined">Close</a>
- <p class="help-text small mb-0">
- {{ fields.media[idx].description ? fields.media[idx].description.length : 0 }}/{{config.uploader.max_altext_length}}
- </p>
- </div>
- </div>
- </template>
- </transition>
- </div>
- </div>
- </template>
- <!-- <template v-else-if="tabIndex === 2">
- <p class="font-weight-bold small">Audience</p>
- <div class="list-group">
- <div
- v-if="!status.account.locked"
- class="list-group-item font-weight-bold cursor-pointer"
- :class="{ 'text-primary': fields.visibility == 'public' }"
- @click="toggleVisibility('public')">
- Public
- <i v-if="fields.visibility == 'public'" class="far fa-check-circle ml-1"></i>
- </div>
- <div
- v-if="!status.account.locked"
- class="list-group-item font-weight-bold cursor-pointer"
- :class="{ 'text-primary': fields.visibility == 'unlisted' }"
- @click="toggleVisibility('unlisted')">
- Unlisted
- <i v-if="fields.visibility == 'unlisted'" class="far fa-check-circle ml-1"></i>
- </div>
- <div
- class="list-group-item font-weight-bold cursor-pointer"
- :class="{ 'text-primary': fields.visibility == 'private' }"
- @click="toggleVisibility('private')">
- Followers Only
- <i v-if="fields.visibility == 'private'" class="far fa-check-circle ml-1"></i>
- </div>
- </div>
- </template> -->
- <template v-else-if="tabIndex === 3">
- <p class="font-weight-bold small">Location</p>
- <autocomplete
- :search="locationSearch"
- placeholder="Search locations ..."
- aria-label="Search locations ..."
- :get-result-value="getResultValue"
- @submit="onSubmitLocation"
- >
- </autocomplete>
- <div v-if="fields.location && fields.location.hasOwnProperty('id')" class="mt-3 border rounded p-3 d-flex justify-content-between">
- <p class="font-weight-bold mb-0">
- {{ fields.location.name }}, {{ fields.location.country}}
- </p>
- <button class="btn btn-link text-danger m-0 p-0" @click.prevent="clearLocation">
- <i class="far fa-trash"></i>
- </button>
- </div>
- </template>
- </b-card-body>
- </b-card>
- <template
- #modal-footer="{ ok, cancel, hide }">
- <b-button class="rounded-pill px-3 font-weight-bold" variant="outline-muted" @click="cancel()">
- Cancel
- </b-button>
- <b-button
- class="rounded-pill font-weight-bold"
- variant="primary"
- style="min-width: 195px"
- @click="handleSave"
- :disabled="!canSave">
- <template v-if="isSubmitting">
- <b-spinner small />
- </template>
- <template v-else>
- Save Updates
- </template>
- </b-button>
- </template>
- </b-modal>
- </template>
- <script type="text/javascript">
- import Autocomplete from '@trevoreyre/autocomplete-vue';
- import BigPicture from 'bigpicture';
- export default {
- components: {
- Autocomplete,
- },
- data() {
- return {
- config: window.App.config,
- status: undefined,
- isLoading: true,
- isOpen: false,
- isSubmitting: false,
- tabIndex: 0,
- canEdit: false,
- composeTextLength: 0,
- canSave: false,
- originalFields: {
- caption: undefined,
- visibility: undefined,
- sensitive: undefined,
- location: undefined,
- spoiler_text: undefined,
- media: [],
- },
- fields: {
- caption: undefined,
- visibility: undefined,
- sensitive: undefined,
- location: undefined,
- spoiler_text: undefined,
- media: [],
- },
- medias: undefined,
- altTextEditIndex: undefined,
- tributeSettings: {
- noMatchTemplate: function () { return null; },
- collection: [
- {
- trigger: '@',
- menuShowMinLength: 2,
- values: (function (text, cb) {
- let url = '/api/compose/v0/search/mention';
- axios.get(url, { params: { q: text }})
- .then(res => {
- cb(res.data);
- })
- .catch(err => {
- console.log(err);
- })
- })
- },
- {
- trigger: '#',
- menuShowMinLength: 2,
- values: (function (text, cb) {
- let url = '/api/compose/v0/search/hashtag';
- axios.get(url, { params: { q: text }})
- .then(res => {
- cb(res.data);
- })
- .catch(err => {
- console.log(err);
- })
- })
- }
- ]
- },
- }
- },
- watch: {
- fields: {
- deep: true,
- immediate: true,
- handler: function(n, o) {
- if(!this.canEdit) {
- return;
- }
- this.canSave = this.originalFields !== JSON.stringify(this.fields);
- }
- }
- },
- methods: {
- reset() {
- this.status = undefined;
- this.tabIndex = 0;
- this.isOpen = false;
- this.canEdit = false;
- this.composeTextLength = 0;
- this.canSave = false;
- this.originalFields = {
- caption: undefined,
- visibility: undefined,
- sensitive: undefined,
- location: undefined,
- spoiler_text: undefined,
- media: [],
- };
- this.fields = {
- caption: undefined,
- visibility: undefined,
- sensitive: undefined,
- location: undefined,
- spoiler_text: undefined,
- media: [],
- };
- this.medias = undefined;
- this.altTextEditIndex = undefined;
- this.isSubmitting = false;
- },
- async show(status) {
- await axios.get('/api/v1/statuses/' + status.id, {
- params: {
- '_pe': 1
- }
- })
- .then(res => {
- this.reset();
- this.init(res.data);
- })
- .finally(() => {
- setTimeout(() => {
- this.isLoading = false;
- }, 500);
- })
- },
- init(status) {
- this.reset();
- this.originalFields = JSON.stringify({
- caption: status.content_text,
- visibility: status.visibility,
- sensitive: status.sensitive,
- location: status.place,
- spoiler_text: status.spoiler_text,
- media: status.media_attachments
- })
- this.fields = {
- caption: status.content_text,
- visibility: status.visibility,
- sensitive: status.sensitive,
- location: status.place,
- spoiler_text: status.spoiler_text,
- media: status.media_attachments
- }
- this.status = status;
- this.medias = status.media_attachments;
- this.composeTextLength = status.content_text ? status.content_text.length : 0;
- this.isOpen = true;
- setTimeout(() => {
- this.canEdit = true;
- }, 1000);
- },
- toggleTab(idx) {
- this.tabIndex = idx;
- this.altTextEditIndex = undefined;
- },
- toggleVisibility(vis) {
- this.fields.visibility = vis;
- },
- locationSearch(input) {
- if (input.length < 1) { return []; }
- let results = [];
- return axios.get('/api/compose/v0/search/location', {
- params: {
- q: input
- }
- }).then(res => {
- return res.data;
- });
- },
- getResultValue(result) {
- return result.name + ', ' + result.country
- },
- onSubmitLocation(result) {
- this.fields.location = result;
- this.tabIndex = 0;
- },
- clearLocation() {
- event.currentTarget.blur();
- this.fields.location = null;
- this.tabIndex = 0;
- },
- handleAltTextUpdate(idx) {
- if (this.fields.media[idx].description.length == 0) {
- this.fields.media[idx].description = null;
- }
- },
- moveMedia(from, to, arr) {
- const newArr = [...arr];
- const item = newArr.splice(from, 1)[0];
- newArr.splice(to, 0, item);
- return newArr;
- },
- toggleMediaOrder(dir, idx) {
- if(dir === 'prev') {
- this.fields.media = this.moveMedia(idx, idx - 1, this.fields.media);
- }
- if(dir === 'next') {
- this.fields.media = this.moveMedia(idx, idx + 1, this.fields.media);
- }
- },
- toggleLightbox(e) {
- BigPicture({
- el: e.target
- })
- },
- handleAddAltText(idx) {
- event.currentTarget.blur();
- this.altTextEditIndex = idx
- },
- removeMedia(idx) {
- swal({
- title: 'Confirm',
- text: 'Are you sure you want to remove this media from your post?',
- buttons: {
- cancel: "Cancel",
- confirm: {
- text: "Confirm Removal",
- value: "remove",
- className: "swal-button--danger"
- }
- }
- })
- .then((val) => {
- if(val === 'remove') {
- this.fields.media.splice(idx, 1);
- }
- })
- },
- async handleSave() {
- event.currentTarget.blur();
- this.canSave = false;
- this.isSubmitting = true;
- await this.checkMediaUpdates();
- axios.put('/api/v1/statuses/' + this.status.id, {
- status: this.fields.caption,
- spoiler_text: this.fields.spoiler_text,
- sensitive: this.fields.sensitive,
- media_ids: this.fields.media.map(m => m.id),
- location: this.fields.location
- })
- .then(res => {
- this.isOpen = false;
- this.$emit('update', res.data);
- swal({
- title: 'Post Updated',
- text: 'You have successfully updated this post!',
- icon: 'success',
- buttons: {
- close: {
- text: "Close",
- value: "close",
- close: true,
- className: "swal-button--cancel"
- },
- view: {
- text: "View Post",
- value: "view",
- className: "btn-primary"
- }
- }
- })
- .then((val) => {
- if(val === 'view') {
- if(this.$router.currentRoute.name === 'post') {
- window.location.reload();
- } else {
- this.$router.push('/i/web/post/' + this.status.id);
- }
- }
- });
- })
- .catch(err => {
- this.isSubmitting = false;
- if(err.response.data.hasOwnProperty('error')) {
- swal('Error', err.response.data.error, 'error');
- } else {
- swal('Error', 'An error occured, please try again later', 'error');
- }
- console.log(err);
- })
- },
- async checkMediaUpdates() {
- const cached = JSON.parse(this.originalFields);
- const medias = JSON.stringify(cached.media);
- if (medias !== JSON.stringify(this.fields.media)) {
- await axios.all(this.fields.media.map((media) => this.updateAltText(media)))
- }
- },
- async updateAltText(media) {
- return await axios.put('/api/v1/media/' + media.id, {
- description: media.description
- });
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- div, p {
- font-family: var(--font-family-sans-serif);
- }
- .nav-link {
- font-size: 13px;
- font-weight: 600;
- color: var(--text-lighter);
- &.active {
- font-weight: 800;
- color: var(--primary);
- }
- }
- .slide-fade-enter-active {
- transition: all .5s ease;
- }
- .slide-fade-leave-active {
- transition: all .2s cubic-bezier(0.5, 1.0, 0.6, 1.0);
- }
- .slide-fade-enter, .slide-fade-leave-to {
- transform: translateY(20px);
- opacity: 0;
- }
- </style>
|