uppy
Version:
Almost as cute as a Puppy :dog:
420 lines (362 loc) • 12.8 kB
JavaScript
const AuthView = require('./AuthView')
const Browser = require('./Browser')
const LoaderView = require('./Loader')
const Utils = require('../core/Utils')
/**
* Class to easily generate generic views for plugins
*
* This class expects the plugin using to have the following attributes
*
* stateId {String} object key of which the plugin state is stored
*
* This class also expects the plugin instance using it to have the following
* accessor methods.
* Each method takes the item whose property is to be accessed
* as a param
*
* isFolder
* @return {Boolean} for if the item is a folder or not
* getItemData
* @return {Object} that is format ready for uppy upload/download
* getItemIcon
* @return {Object} html instance of the item's icon
* getItemSubList
* @return {Array} sub-items in the item. e.g a folder may contain sub-items
* getItemName
* @return {String} display friendly name of the item
* getMimeType
* @return {String} mime type of the item
* getItemId
* @return {String} unique id of the item
* getItemRequestPath
* @return {String} unique request path of the item when making calls to uppy server
* getItemModifiedDate
* @return {object} or {String} date of when last the item was modified
* getItemThumbnailUrl
* @return {String}
*/
module.exports = class View {
/**
* @param {object} instance of the plugin
*/
constructor (plugin, opts) {
this.plugin = plugin
this.Provider = plugin[plugin.id]
// set default options
const defaultOptions = {
viewType: 'list'
}
// merge default options with the ones set by user
this.opts = Object.assign({}, defaultOptions, opts)
// Logic
this.addFile = this.addFile.bind(this)
this.filterItems = this.filterItems.bind(this)
this.filterQuery = this.filterQuery.bind(this)
this.toggleSearch = this.toggleSearch.bind(this)
this.getFolder = this.getFolder.bind(this)
this.getNextFolder = this.getNextFolder.bind(this)
this.logout = this.logout.bind(this)
this.checkAuth = this.checkAuth.bind(this)
this.handleAuth = this.handleAuth.bind(this)
this.handleDemoAuth = this.handleDemoAuth.bind(this)
this.sortByTitle = this.sortByTitle.bind(this)
this.sortByDate = this.sortByDate.bind(this)
this.isActiveRow = this.isActiveRow.bind(this)
this.handleError = this.handleError.bind(this)
this.handleScroll = this.handleScroll.bind(this)
// Visual
this.render = this.render.bind(this)
}
/**
* Little shorthand to update the state with the plugin's state
*/
updateState (newState) {
let stateId = this.plugin.stateId
const {state} = this.plugin.core
this.plugin.core.setState({[stateId]: Object.assign({}, state[stateId], newState)})
}
_updateFilesAndFolders (res, files, folders) {
this.plugin.getItemSubList(res).forEach((item) => {
if (this.plugin.isFolder(item)) {
folders.push(item)
} else {
files.push(item)
}
})
this.updateState({ folders, files })
}
checkAuth () {
this.updateState({ checkAuthInProgress: true })
this.Provider.checkAuth()
.then((authenticated) => {
this.updateState({ checkAuthInProgress: false })
this.plugin.onAuth(authenticated)
})
.catch((err) => {
this.updateState({ checkAuthInProgress: false })
this.handleError(err)
})
}
/**
* Based on folder ID, fetch a new folder and update it to state
* @param {String} id Folder id
* @return {Promise} Folders/files in folder
*/
getFolder (id, name) {
return this._loaderWrapper(
this.Provider.list(id),
(res) => {
let folders = []
let files = []
let updatedDirectories
const state = this.plugin.core.getState()[this.plugin.stateId]
const index = state.directories.findIndex((dir) => id === dir.id)
if (index !== -1) {
updatedDirectories = state.directories.slice(0, index + 1)
} else {
updatedDirectories = state.directories.concat([{id, title: name || this.plugin.getItemName(res)}])
}
this._updateFilesAndFolders(res, files, folders)
this.updateState({ directories: updatedDirectories })
},
this.handleError)
}
/**
* Fetches new folder
* @param {Object} Folder
* @param {String} title Folder title
*/
getNextFolder (folder) {
let id = this.plugin.getItemRequestPath(folder)
this.getFolder(id, this.plugin.getItemName(folder))
}
addFile (file) {
const tagFile = {
source: this.plugin.id,
data: this.plugin.getItemData(file),
name: this.plugin.getItemName(file) || this.plugin.getItemId(file),
type: this.plugin.getMimeType(file),
isRemote: true,
body: {
fileId: this.plugin.getItemId(file)
},
remote: {
host: this.plugin.opts.host,
url: `${this.Provider.fileUrl(this.plugin.getItemRequestPath(file))}`,
body: {
fileId: this.plugin.getItemId(file)
}
}
}
Utils.getFileType(tagFile).then(fileType => {
if (Utils.isPreviewSupported(fileType[1])) {
tagFile.preview = this.plugin.getItemThumbnailUrl(file)
}
this.plugin.core.log('Adding remote file')
this.plugin.core.addFile(tagFile)
})
}
/**
* Removes session token on client side.
*/
logout () {
this.Provider.logout(location.href)
.then((res) => res.json())
.then((res) => {
if (res.ok) {
const newState = {
authenticated: false,
files: [],
folders: [],
directories: []
}
this.updateState(newState)
}
}).catch(this.handleError)
}
filterQuery (e) {
const state = this.plugin.core.getState()[this.plugin.stateId]
this.updateState(Object.assign({}, state, {
filterInput: e.target.value
}))
}
toggleSearch () {
const state = this.plugin.core.getState()[this.plugin.stateId]
const searchInputEl = document.querySelector('.Browser-searchInput')
this.updateState(Object.assign({}, state, {
isSearchVisible: !state.isSearchVisible,
filterInput: ''
}))
searchInputEl.value = ''
if (!state.isSearchVisible) {
searchInputEl.focus()
}
}
filterItems (items) {
const state = this.plugin.core.getState()[this.plugin.stateId]
return items.filter((folder) => {
return this.plugin.getItemName(folder).toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1
})
}
sortByTitle () {
const state = Object.assign({}, this.plugin.core.getState()[this.plugin.stateId])
const {files, folders, sorting} = state
let sortedFiles = files.sort((fileA, fileB) => {
if (sorting === 'titleDescending') {
return this.plugin.getItemName(fileB).localeCompare(this.plugin.getItemName(fileA))
}
return this.plugin.getItemName(fileA).localeCompare(this.plugin.getItemName(fileB))
})
let sortedFolders = folders.sort((folderA, folderB) => {
if (sorting === 'titleDescending') {
return this.plugin.getItemName(folderB).localeCompare(this.plugin.getItemName(folderA))
}
return this.plugin.getItemName(folderA).localeCompare(this.plugin.getItemName(folderB))
})
this.updateState(Object.assign({}, state, {
files: sortedFiles,
folders: sortedFolders,
sorting: (sorting === 'titleDescending') ? 'titleAscending' : 'titleDescending'
}))
}
sortByDate () {
const state = Object.assign({}, this.plugin.core.getState()[this.plugin.stateId])
const {files, folders, sorting} = state
let sortedFiles = files.sort((fileA, fileB) => {
let a = new Date(this.plugin.getItemModifiedDate(fileA))
let b = new Date(this.plugin.getItemModifiedDate(fileB))
if (sorting === 'dateDescending') {
return a > b ? -1 : a < b ? 1 : 0
}
return a > b ? 1 : a < b ? -1 : 0
})
let sortedFolders = folders.sort((folderA, folderB) => {
let a = new Date(this.plugin.getItemModifiedDate(folderA))
let b = new Date(this.plugin.getItemModifiedDate(folderB))
if (sorting === 'dateDescending') {
return a > b ? -1 : a < b ? 1 : 0
}
return a > b ? 1 : a < b ? -1 : 0
})
this.updateState(Object.assign({}, state, {
files: sortedFiles,
folders: sortedFolders,
sorting: (sorting === 'dateDescending') ? 'dateAscending' : 'dateDescending'
}))
}
sortBySize () {
const state = Object.assign({}, this.plugin.core.getState()[this.plugin.stateId])
const {files, sorting} = state
// check that plugin supports file sizes
if (!files.length || !this.plugin.getItemData(files[0]).size) {
return
}
let sortedFiles = files.sort((fileA, fileB) => {
let a = this.plugin.getItemData(fileA).size
let b = this.plugin.getItemData(fileB).size
if (sorting === 'sizeDescending') {
return a > b ? -1 : a < b ? 1 : 0
}
return a > b ? 1 : a < b ? -1 : 0
})
this.updateState(Object.assign({}, state, {
files: sortedFiles,
sorting: (sorting === 'sizeDescending') ? 'sizeAscending' : 'sizeDescending'
}))
}
isActiveRow (file) {
return this.plugin.core.getState()[this.plugin.stateId].activeRow === this.plugin.getItemId(file)
}
handleDemoAuth () {
const state = this.plugin.core.getState()[this.plugin.stateId]
this.updateState({}, state, {
authenticated: true
})
}
handleAuth () {
const urlId = Math.floor(Math.random() * 999999) + 1
const redirect = `${location.href}${location.search ? '&' : '?'}id=${urlId}`
const authState = btoa(JSON.stringify({ redirect }))
const link = `${this.Provider.authUrl()}?state=${authState}`
const authWindow = window.open(link, '_blank')
const checkAuth = () => {
let authWindowUrl
try {
authWindowUrl = authWindow.location.href
} catch (e) {
if (e instanceof DOMException || e instanceof TypeError) {
return setTimeout(checkAuth, 100)
} else throw e
}
// split url because chrome adds '#' to redirects
if (authWindowUrl && authWindowUrl.split('#')[0] === redirect) {
authWindow.close()
this._loaderWrapper(this.Provider.checkAuth(), this.plugin.onAuth, this.handleError)
} else {
setTimeout(checkAuth, 100)
}
}
checkAuth()
}
handleError (error) {
const core = this.plugin.core
const message = core.i18n('uppyServerError')
core.log(error.toString())
core.info({message: message, details: error.toString()}, 'error', 5000)
}
handleScroll (e) {
const scrollPos = e.target.scrollHeight - (e.target.scrollTop + e.target.offsetHeight)
const path = this.plugin.getNextPagePath ? this.plugin.getNextPagePath() : null
if (scrollPos < 50 && path && !this._isHandlingScroll) {
this.Provider.list(path)
.then((res) => {
const { files, folders } = this.plugin.core.getState()[this.plugin.stateId]
this._updateFilesAndFolders(res, files, folders)
}).catch(this.handleError)
.then(() => { this._isHandlingScroll = false }) // always called
this._isHandlingScroll = true
}
}
// displays loader view while asynchronous request is being made.
_loaderWrapper (promise, then, catch_) {
promise
.then(then).catch(catch_)
.then(() => this.updateState({ loading: false })) // always called.
this.updateState({ loading: true })
}
render (state) {
const { authenticated, checkAuthInProgress, loading } = state[this.plugin.stateId]
if (loading) {
return LoaderView()
}
if (!authenticated) {
return AuthView({
pluginName: this.plugin.title,
demo: this.plugin.opts.demo,
checkAuth: this.checkAuth,
handleAuth: this.handleAuth,
handleDemoAuth: this.handleDemoAuth,
checkAuthInProgress: checkAuthInProgress
})
}
const browserProps = Object.assign({}, state[this.plugin.stateId], {
getNextFolder: this.getNextFolder,
getFolder: this.getFolder,
addFile: this.addFile,
filterItems: this.filterItems,
filterQuery: this.filterQuery,
toggleSearch: this.toggleSearch,
sortByTitle: this.sortByTitle,
sortByDate: this.sortByDate,
logout: this.logout,
demo: this.plugin.opts.demo,
isActiveRow: this.isActiveRow,
getItemName: this.plugin.getItemName,
getItemIcon: this.plugin.getItemIcon,
handleScroll: this.handleScroll,
title: this.plugin.title,
viewType: this.opts.viewType
})
return Browser(browserProps)
}
}