cms/resources/js/components/MediaLibrary.vue

718 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div id="media-library">
<div class="container-fluid">
<div class="row no-gutters">
<div class="close-window" @click="onCloseWindow"><i class="fa fa-close"></i></div>
<div class="col-md-2 col-categories" v-show="!isMobile || _d.onlyShowCategory">
<div class="row row-head no-gutters">
<div class="col">
<div class="controls">
<div v-if="_d.addingNewCategory">
<input type="text" class="form-control" v-model="_d.currentEditingCategoryText" @click.prevent.stop @keypress.enter="onAddCategory">
<button class="btn btn-sm btn-success" @click.prevent.stop="onAddCategory"><i class="fa fa-check"></i></button>
<button class="btn btn-sm btn-outline-secondary" @click.prevent.stop="onCancelCategoryAdding"><i class="fa fa-close"></i></button>
</div>
<div v-if="!_d.addingNewCategory">
<button class="btn btn-success btn-sm" @click="_d.addingNewCategory=true"><i class="fa fa-plus"></i></button>
<button class="btn btn-primary btn-sm" @click="onEditCategory(_d.currentCategory)"><i class="fa fa-edit"></i></button>
<button class="btn btn-danger btn-sm" @click="onDeleteCategory(_d.currentCategory)"><i class="fa fa-close"></i></button>
<button class="btn btn-outline-secondary btn-sm return" v-if="isMobile" @click="_d.onlyShowCategory=false"><i class="fa fa-arrow-right"></i></button>
</div>
</div>
</div>
</div>
<div class="row row-body no-gutters">
<div class="col">
<div class="category-list-wrapper">
<div class="message">
<div class="alert alert-success" v-if="_d.categorySuccessMessage">{{ _d.categorySuccessMessage }}</div>
<div class="alert alert-danger" v-if="_d.categoryErrorMessage">
{{ _d.categoryErrorMessage }}
<button type="button" class="close" aria-label="Close" @click="_d.categoryErrorMessage=null">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
<div class="spinner-border" role="status" v-if="_d.categoryFetching">
<span class="sr-only">Loading...</span>
</div>
<nav class="nav flex-column category-list" :class="{dragging: this._d.currentDraggingCategoryId}" ref="categoryList">
<a class="nav-link category"
href="#"
:draggable="!isUneditableCategory(category.id)"
:data-id="category.id"
:class="{current: _d.currentCategory == category.id, editing: isCategoryEditing(category.id)}"
@dragenter.capture.prevent.stop="onDragEnterCategory($event, category.id)"
@dragleave.capture.prevent.stop="onDragLeaveCategory($event)"
@drop="onDropOnCategory($event, category.id)"
@dragstart.stop="onDragCategory(category.id)"
@dragover.prevent.stop
@dragend.capture.prevent.stop="onDropCategory"
@click.prevent="switchCategory(category.id)"
v-for="category in _d.categories">
<span v-if="!isCategoryEditing(category.id)">
<span class="name">{{ category.name }}</span>
<span class="badge badge-secondary">{{ category.count }}</span>
<button class="btn btn-sm edit-category" v-if="!isUneditableCategory(category.id)" @click.prevent.stop="onEditCategory(category.id)">
<i class="fa fa-edit"></i>
</button>
<button class="btn btn-sm remove-category" v-if="!isUneditableCategory(category.id)" @click.prevent.stop="onDeleteCategory(category.id)">
<i class="fa fa-close"></i>
</button>
</span>
<span v-if="isCategoryEditing(category.id)">
<input type="text" class="form-control" v-model="_d.currentEditingCategoryText" @click.prevent.stop>
<button class="btn btn-sm btn-primary" @click.prevent.stop="onUpdateCategoryName()"><i class="fa fa-check"></i></button>
<button class="btn btn-sm btn-outline-secondary" @click.prevent.stop="cancelCategoryEditing"><i class="fa fa-close"></i></button>
</span>
</a>
</nav>
</div>
</div>
</div>
<div class="row row-footer no-gutters">
<div class="col"></div>
</div>
</div>
<div class="col col-images" v-show="!isMobile || (isMobile && !_d.onlyShowCategory && !_d.onlyShowMediaInfo)">
<div class="row row-head no-gutters">
<div class="col">
<ul class="nav nav-tabs tabs">
<li class="nav-item" v-if="isMobile">
<a href="#" class="nav-link" @click="_d.onlyShowCategory=true">
{{ _d.text.tabCategory }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @drag.prevent @click="_d.isUploadingPage=true"
v-bind:class="{active: _d.isUploadingPage}">{{ _d.text.tabUpload }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @drag.prevent @click="_d.isUploadingPage=false"
v-bind:class="{active: !_d.isUploadingPage}">{{ _d.text.tabBrowse }}</a>
</li>
</ul>
</div>
</div>
<div class="row row-body no-gutters">
<div class="col">
<div class="alert alert-success image-message" v-if="_d.imageSuccessMessage">
{{ _d.imageSuccessMessage }}
</div>
<div class="alert alert-danger image-message" v-if="_d.imageErrorMessage">
{{ _d.imageErrorMessage }}
<button type="button" class="close" aria-label="Close" @click="_d.imageErrorMessage=null">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="tab-page upload" v-show="_d.isUploadingPage">
<form class="upload-zone" v-bind:action="_d.formAction" ref="uploadZone">
<input type="hidden" name="_token" v-bind:value="_d.csrfToken">
<div class="fallback">
<input name="media_file" type="file" multiple />
</div>
<div class="upload-tip"> {{ _d.text.dragOrClickToUpload }} </div>
</form>
</div>
<div class="tab-page browse" v-bind:class="{dragging : !isCurrentDraggingMediaIdsEmpty }" v-show="!_d.isUploadingPage" @scroll="onScrollBrowsing($event)">
<div v-for="media in _d.medias"
:key="media.id"
@dragend="onDropMedia"
@dragstart="onDragMedia(media.id)"
draggable="true"
class="img-wrapper"
v-bind:class="{ selected: isMediaSelected(media)}"
@click="onClickMedia($event, media)"
@click.ctrl="onCtrlClickMedia($event, media)">
<div class="img" :style="{ 'background-image': 'url(' + media.url + ')' }">
</div>
</div>
<div class="spinner-border" role="status" v-if="_d.fetchingLock">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</div>
<div class="row row-footer no-gutters">
<div class="col"></div>
</div>
</div>
<div class="col-md-2 col-media-info" v-show="!isMobile || _d.onlyShowMediaInfo">
<div class="row row-head" v-if="isMobile">
<div class="col">
<button class="btn btn-outline-secondary btn-sm return" @click="_d.onlyShowMediaInfo=false"><i class="fa fa-arrow-left"></i></button>
</div>
</div>
<div class="row row-body no-gutters">
<div class="col">
<div class="media-info-wrapper" :class="{'select-mode': _d.selectMode}">
<div class="message">
<div class="alert alert-danger" v-if="_d.infoErrorMessage">
{{ _d.infoErrorMessage }}
<button type="button" class="close" aria-label="Close" @click="_d.infoErrorMessage=null">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="alert alert-success" v-if="_d.infoSuccessMessage">{{ _d.infoSuccessMessage }}</div>
</div>
<div class="info" :class="{hidden: !_d.currentSelectedMedia}">
<div class="url">
<span class="label font-weight-bold">{{ _d.text.url }} :</span>
<div class="content">
<a v-bind:href="_d.currentSelectedMedia.url" target="_blank">{{ _d.currentSelectedMedia.url }}</a>
</div>
</div>
<div class="size">
<span class="label font-weight-bold">{{ _d.text.fileSize }}:</span>
<span class="content">{{ fileSize }}</span>
</div>
<div class="date">
<span class="label font-weight-bold">{{ _d.text.date }}:</span>
<span class="content">{{ _d.currentSelectedMedia.date }}</span>
</div>
<div class="dimension">
<span class="label font-weight-bold">{{ _d.text.size }}:</span>
<span class="content">{{ dimension }}</span>
</div>
<div class="category">
<span class="label font-weight-bold">{{ _d.text.category }}:</span>
<select name="" id="" v-model="_d.currentSelectedMedia.category.id" v-if="_d.currentSelectedMedia.category">
<option :value="category.id" v-for="category in this.mediaSettableCategories">{{ category.name }}</option>
</select>
<button class="btn btn-success btn-sm" @click.prevent="onUpdateMediaCategory()" v-if="_d.currentSelectedMedia">{{ _d.text.update }}</button>
</div>
<div class="description">
<span class="label font-weight-bold">{{ _d.text.description }}</span>
<div class="content">
<textarea class="form-control" v-model="_d.currentSelectedMedia.description"></textarea>
</div>
<button class="btn btn-success btn-sm" @click.prevent="onUpdateMediaDescription($event)">{{ _d.text.update }}</button>
<button class="btn btn-outline-danger btn-sm" @click.prevent="onDeleteMedia($event)">{{ _d.text.delete }}</button>
</div>
<div class="img">
<img v-bind:src="_d.currentSelectedMedia.url" alt="">
</div>
</div>
</div>
</div>
</div>
<div class="row row-footer no-gutters">
<div class="col">
<div class="select-medias" v-if="_d.selectMode">
<button class="btn btn-primary" @click="onDoneSelectedMedias" :disabled="isSelectedMediasEmpty">{{ _d.text.select }}</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "media-library",
props: ['_d'],
computed: {
isMobile() {
let isMobile = this._d.windowWidth < 768;
return isMobile;
},
mediaSettableCategories() {
return this._d.categories.filter(category => !this.isAllCategory(category.id));
},
// 是否為單選模式
isSingleSelectionMode() {
return this._d.single;
},
isSelectedMediasEmpty() {
return this._d.selectedMedias.length ? false : true;
},
selectedMediaIds() {
return this._d.selectedMedias.map(_media => _media.id);
},
isCurrentDraggingMediaIdsEmpty() {
return this._d.currentDraggingMediaIds ? false : true;
},
dimension() {
if(this._d.currentSelectedMedia.width !== null && this._d.currentSelectedMedia.height !== null) {
return this._d.currentSelectedMedia.width + 'x' + this._d.currentSelectedMedia.height;
} else {
return '';
}
},
fileSize() {
if(this._d.currentSelectedMedia.sizeInBytes) {
let size = this._d.currentSelectedMedia.sizeInBytes;
if(size < 1024)
return size + ' Bytes';
let unit = 0;
while(size >= 1024) {
unit++;
size /= 1024;
}
size = parseFloat(size).toFixed(2)
switch(unit) {
case 1:
size += ' KB';
break;
case 2:
size += ' MB';
break;
case 3:
size += ' GB';
break;
}
return size;
}
return '';
}
},
methods: {
// 設定已選擇的媒體
setSelectedMedias(selectedMedias) {
this._d.selectedMedias = selectedMedias;
},
// 清空被選擇的媒體
clearSelectedMedia() {
this.setSelectedMedias([]);
},
// 加入媒體至被選擇的媒體中
addToSelectedMedia(media) {
this._d.selectedMedias.push(media)
},
// 媒體是否已被選擇(by ID)
isMediaIdSelected(id) {
let foundMedias = this._d.selectedMedias.find(_media => _media.id == id);
return foundMedias ? true : false;
},
// 媒體是否已被選擇
isMediaSelected(media) {
return this.isMediaIdSelected(media.id);
},
// 從被選擇的媒體中刪除媒體
deleteFromSelectedMedia(media) {
this.setSelectedMedias(this._d.selectedMedias.filter(_media => _media.id != media.id));
},
// 從被選擇的媒體中刪除或新增媒體
addOrDeleteSelectedMedia(media) {
if(this.isMediaSelected(media)) {
this.unselectMedia(media);
} else {
this.addToSelectedMedia(media);
}
},
// 設定單一個媒體至被選擇的媒體
setSingleSelectedMedia(media) {
this.clearSelectedMedia();
this.addToSelectedMedia(media);
this.setCurrentSelectedMedia(media);
},
// 反選擇媒體
unselectMedia(media) {
this.deleteFromSelectedMedia(media);
this.clearCurrentSelectedMedia();
},
// 清除目前點選的媒體
clearCurrentSelectedMedia() {
this._d.currentSelectedMedia = false
},
// 設定目前點選的媒體
setCurrentSelectedMedia(media) {
this._d.currentSelectedMedia = media;
if(this.isMobile) {
this._d.onlyShowMediaInfo = true;
}
},
// 取得媒體資料
findMedia(id) {
return this._d.medias.find(media => media.id == id);
},
// 分類ID為"所有"
isAllCategory(id) {
return id == 'all';
},
// 分類ID為"未分類"
isUncategorizedCategory(id) {
return id == 'uncategorized';
},
// 無法編輯的分類
isUneditableCategory(id) {
return this.isUncategorizedCategory(id) || this.isAllCategory(id)
},
// 特定ID的分類正在編輯
isCategoryEditing(id) {
return this._d.currentEditingCategory == id;
},
// 取得分類資料
findCategory(id) {
return this._d.categories.find(category => category.id == id);
},
// 取得分類名稱
getCategoryName(id) {
let category = this.findCategory(id);
return category ? category.name : null;
},
// 刪除媒體
removeMediaFromList(mediaId) {
this._d.medias = this._d.medias.filter(media => media.id != mediaId);
if(this._d.currentSelectedMedia.id == mediaId) {
this.clearCurrentSelectedMedia();
}
},
// click事件選擇媒體
onClickMedia(e, media, fromMulti = false) {
if(e.ctrlKey && !fromMulti) {
return;
}
if(
(this.isSingleSelectionMode && this.isMediaSelected(media)) ||
(!this.isSingleSelectionMode && this.isMediaSelected(media) && this._d.selectedMedias.length == 1)
) {
this.unselectMedia(media);
} else {
this.setSingleSelectedMedia(media);
}
},
// click事件多選媒體
onCtrlClickMedia(e, media) {
this.setCurrentSelectedMedia(media);
if(this.isSingleSelectionMode) {
this.onClickMedia(e, media, true);
} else {
this.addOrDeleteSelectedMedia(media);
}
},
// click事件回傳選擇的媒體
onDoneSelectedMedias() {
let data = [], idString = '';
this._d.selectedMedias.forEach(_media => {
data.push({
id: _media.id,
url: _media.url
})
})
idString = data.map(media => media.id).join(',')
this.$root.$emit('media_selected', {
idString: idString,
medias: data
})
this.onCloseWindow();
},
// click事件關閉視窗
onCloseWindow() {
this.$root.$emit('close');
},
onUpdateMediaCategory() {
this.$root.$emit('update_category_for_medias', this._d.currentSelectedMedia.category.id, this._d.currentSelectedMedia.id, false);
},
// click事件更新媒體描述
onUpdateMediaDescription(e) {
this.$root.$emit('update_media_description', e, this._d.currentSelectedMedia)
},
// click事件刪除媒體
onDeleteMedia(e) {
this.$root.$emit('delete_media', e, this._d.currentSelectedMedia)
},
// 更新媒體的分類名稱
updateCategoryForMedia(mediaId, categoryId) {
let media = this._d.medias.find(media => media.id == mediaId);
if(media) {
let category = this.findCategory(categoryId);
if(category) {
media.category.name = category.name;
media.category.id = categoryId;
}
}
},
// 設定目前正在拖曳的媒體ID
setCurrentDraggingMediaIds(ids) {
this._d.currentDraggingMediaIds = ids;
},
// 清除目前正在拖曳的媒體ID
clearCurrentDraggingMediaIds() {
this._d.currentDraggingMediaIds = null;
},
// click事件刪除所有選擇的媒體
deleteSelectedMedias() {
//TODO 批次刪除媒體
},
// scroll事件載入更多媒體
onScrollBrowsing(e) {
if(this._d.fetchingEnd) return;
let ele = e.target;
if(ele.scrollTop >= ele.scrollHeight - ele.clientHeight - 10) {
if(!this._d.fetchingLock) {
this.$root.$emit('fetch_medias', 14);
}
}
},
clearInfoMessages() {
this._d.infoSuccessMessage = '';
this._d.infoErrorMessage = '';
},
showInfoSuccessMessage(message) {
this._d.infoSuccessMessage = message;
setTimeout(() => {
this._d.infoSuccessMessage = '';
}, 5000);
},
showInfoErrorMessage(message) {
this._d.infoErrorMessage = message;
},
clearCategoryMessages() {
this._d.categorySuccessMessage = '';
this._d.categoryErrorMessage = '';
},
showCategorySuccessMessage(message) {
this._d.categorySuccessMessage = message;
setTimeout(() => {
this._d.categorySuccessMessage = '';
}, 1000);
},
showCategoryErrorMessage(message) {
this._d.categoryErrorMessage = message;
},
clearImageMessages() {
this._d.imageSuccessMessage = '';
this._d.imageErrorMessage = '';
},
showImageSuccessMessage(message) {
this._d.imageSuccessMessage = message;
setTimeout(() => {
this._d.imageSuccessMessage = '';
}, 1000);
},
showImageErrorMessage(message) {
this._d.imageErrorMessage = message;
},
// drag事件拖曳媒體
onDragMedia(id) {
if(!this.isSelectedMediasEmpty && this.isMediaIdSelected(id)) {
this.setCurrentDraggingMediaIds(this.selectedMediaIds);
} else {
this.setCurrentDraggingMediaIds(id);
}
},
// dragenter事件拖曳媒體或分類至分類上
onDragEnterCategory(e, id) {
if(this._d.currentDraggingCategoryId) {
if(this.isUneditableCategory(id)) return;
if(!this.isCurrentDraggingMediaIdsEmpty) return;
let element = this.findCategoryElementById(id);
this._d.currentDraggingOverCategoryId = id;
if(this.isCategoryDragSourceBefore(this._d.currentDraggingCategoryId, id)) {
this.insertAfter(this._d.categoryDraggingIndicator, element)
} else {
this.insertBefore(this._d.categoryDraggingIndicator, element)
}
} else if(!this.isCurrentDraggingMediaIdsEmpty) {
if(!this.isAllCategory(id) && this._d.currentCategory != id) {
e.target.classList.add('on-drag');
}
}
},
// dragleave事件拖曳媒體出分類
onDragLeaveCategory(e) {
e.target.classList.remove('on-drag');
},
// dragend事件放開拖曳媒體
onDropMedia() {
this.clearCurrentDraggingMediaIds();
},
// drop事件拖曳媒體並drop至分類上
onDropOnCategory(e, id) {
e.target.classList.remove('on-drag');
if(!this.isAllCategory(id) && this._d.currentCategory != id && !this.isCurrentDraggingMediaIdsEmpty) {
this.$root.$emit('update_category_for_medias', id, this._d.currentDraggingMediaIds);
}
},
// 切換分類
switchCategory(id) {
if(this._d.currentEditingCategory) {
this.cancelCategoryEditing();
}
if(!this._d.fetchingLock && !this._d.currentEditingCategory) {
this.clearSelectedMedia();
this.clearCurrentSelectedMedia();
this._d.medias = [];
this._d.currentCategory = id;
this._d.lastFetchedMediaId = null;
this._d.fetchingEnd = false;
this.$root.$emit('fetch_medias', 35);
this.$root.$emit('remove_all_uploaded_files');
}
},
// 減少分類的媒體數量
substractCategoryCount(id, number, substractFromAll = true) {
let category = this.findCategory(id);
if(category) {
category.count -= number;
}
if(substractFromAll) {
let category = this.findCategory('all');
if(category) {
category.count -= number;
}
}
},
// 增加分類的媒體數量
addCategoryCount(id, number, addToAll = true) {
let category = this.findCategory(id);
if(category) {
category.count += number;
}
if(addToAll) {
let category = this.findCategory('all');
if(category) {
category.count += number;
}
}
},
// click事件編輯分類
onEditCategory(id) {
if(!this.isUneditableCategory(id)) {
this._d.currentEditingCategory = id;
this._d.currentEditingCategoryText = this.getCategoryName(id);
}
},
// 從列表刪除分類
deleteCategoryFromList(id) {
this._d.categories = this._d.categories.filter(category => category.id != id);
if(id == this._d.currentCategory) {
this._d.medias = [];
this._d.currentCategory = null;
}
},
// click事件刪除分類
onDeleteCategory(id) {
if(!this.isUneditableCategory(id) && id) {
if(confirm(this._d.text.deleteConfirmation)) {
this.$root.$emit('delete_category', id);
}
}
},
// 取消分類編輯
cancelCategoryEditing() {
this._d.currentEditingCategory = null;
this._d.currentEditingCategoryText = null;
},
// click事件更新分類名稱
onUpdateCategoryName() {
this.$root.$emit('update_category_name', this._d.currentEditingCategory, this._d.currentEditingCategoryText)
},
// 更新分類名稱
updateListCategoryName(id, name) {
let category = this.findCategory(id);
if(category) {
category.name = name;
}
},
// click事件取消新增分類
onCancelCategoryAdding() {
this._d.addingNewCategory = false;
},
// click事件新增分類
onAddCategory() {
this.$root.$emit('add_category', this._d.currentEditingCategoryText);
},
// 新增分類至列表開頭
addCategoryToList(id, name) {
let _categories = [];
_categories = _categories.concat(this._d.categories.filter(category => this.isUneditableCategory(category.id)))
_categories.push({
id: id,
name: name,
count: 0,
})
_categories = _categories.concat(this._d.categories.filter(category => !this.isUneditableCategory(category.id)))
this._d.categories = _categories;
},
// 從分類列表中找尋分類的element
findCategoryElementById(id) {
let element = document.querySelector('.category[data-id="' + id + '"]');
return element;
},
// insert after
insertAfter(newNode, referenceNode) {
referenceNode.parentNode.appendChild(newNode);
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
},
// insert before
insertBefore(newNode, referenceNode) {
referenceNode.parentNode.appendChild(newNode);
referenceNode.parentNode.insertBefore(newNode, referenceNode);
},
// 拖曳的分類比目的分類還前面
isCategoryDragSourceBefore(sourceId, targetId) {
let dragSourceIndex = 0, dragTargetIndex = 0;
this._d.categories.forEach((category, index) => {
if(category.id == sourceId) {
dragSourceIndex = index;
}
if(category.id == targetId) {
dragTargetIndex = index;
}
});
return dragSourceIndex < dragTargetIndex;
},
// dragstart事件拖曳分類
onDragCategory(id) {
this._d.currentDraggingCategoryId = id;
let indicatorElement = document.createElement('span')
indicatorElement.className = 'dragging-indicator';
this._d.categoryDraggingIndicator = indicatorElement;
},
// 交換分類的順序
switchCategoryOrder(draggingSourceId, draggingTargetId) {
if(draggingSourceId == draggingTargetId) return;
let _categories = [];
let category = this.findCategory(draggingSourceId);
let insertBefore = this.isCategoryDragSourceBefore(draggingSourceId, draggingTargetId);
if(!insertBefore) {
this._d.categories.reverse();
}
this._d.categories.forEach((_category, index) => {
if(_category.id != category.id) {
_categories.push(_category)
}
if(_category.id == draggingTargetId) {
_categories.push(category);
}
})
if(!insertBefore) {
_categories.reverse();
}
this._d.categories = _categories
this.$root.$emit('update_category_order',
_categories
.filter(category => !this.isUneditableCategory(category.id))
.map(category => category.id)
.join(',')
);
},
// dragend事件拖曳分類並放置分類上
onDropCategory() {
let indicator = document.querySelector('.dragging-indicator')
indicator.parentNode.removeChild(indicator);
this.switchCategoryOrder(this._d.currentDraggingCategoryId, this._d.currentDraggingOverCategoryId);
this._d.currentDraggingOverCategoryId = null;
this._d.currentDraggingCategoryId = null;
}
}
}
</script>
<style scoped>
</style>