Implement New Files Preview
This is the first commit of preview functionality into New Files it isn't polished, but should be a good starting point for polish. Tickets are being created to account for any remaining tasks. closes CNVS-14727 Test Plan: (sorry kinda big) - Enable "Better File Browsing" for a course - Go to course files (/courses/##/files) - Make sure you have some images uploaded... since that's all that is really supported. - Click on a file (but not on the file name itself) - It should highlight - Click the "View" option in the toolbar that should appear - An overlay should cover the screen - You should be able to tab to each of the interactive elements on this page - Clicking Download should download the file. - Clicking Info should open the information panel. - Clicking the show button at the bottom should open up the preview thumbnails - The arrow keys (and the buttons on the screeen) should navigate left/right - A non-image file should show you a nice message telling you it can't be previewed yet. - Clicking close should, you know... close the panel. - Now select multiple files and click "view" again. - Perform all the same tests, but now you should be limited to only the items that you selected. Change-Id: I8f48a133f739b9bddda5e340f5156269691c7870 Reviewed-on: https://gerrit.instructure.com/41451 QA-Review: Jahnavi Yetukuri <jyetukuri@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Product-Review: Cosme Salazar <cosme@instructure.com>
This commit is contained in:
parent
df6d8b969b
commit
9a4f37a4bc
|
@ -0,0 +1,230 @@
|
||||||
|
define [
|
||||||
|
'underscore',
|
||||||
|
'react'
|
||||||
|
'react-router',
|
||||||
|
'bower/react-modal/dist/react-modal'
|
||||||
|
'i18n!file_preview'
|
||||||
|
'./FriendlyDatetime'
|
||||||
|
'compiled/models/Folder',
|
||||||
|
'compiled/react/shared/utils/withReactDOM'
|
||||||
|
'../utils/collectionHandler'
|
||||||
|
], (_, React, ReactRouter, ReactModal, I18n, FriendlyDatetime, Folder, withReactDOM, collectionHandler) ->
|
||||||
|
FilePreview = React.createClass
|
||||||
|
|
||||||
|
|
||||||
|
displayName: 'FilePreview'
|
||||||
|
|
||||||
|
mixins: [React.addons.PureRenderMixin]
|
||||||
|
|
||||||
|
propTypes:
|
||||||
|
initialItem: React.PropTypes.object
|
||||||
|
otherItems: React.PropTypes.array
|
||||||
|
otherItemsString: React.PropTypes.string
|
||||||
|
|
||||||
|
getInitialState: ->
|
||||||
|
showInfoPanel: false
|
||||||
|
showFooter: false
|
||||||
|
showFooterBtn: true
|
||||||
|
displayedItem: @props.initialItem
|
||||||
|
user: @props.initialItem.get 'user'
|
||||||
|
otherItemsIsBackBoneCollection: @props.otherItems instanceof Backbone.Collection
|
||||||
|
|
||||||
|
componentWillMount: ->
|
||||||
|
ReactModal.setAppElement(@props.appElement)
|
||||||
|
|
||||||
|
componentDidMount: ->
|
||||||
|
$('.ReactModal__Overlay').on 'keydown', @handleKeyboardNavigation
|
||||||
|
|
||||||
|
componentWillUnmount: ->
|
||||||
|
$('.ReactModal__Overlay').off 'keydown', @handleKeyboardNavigation
|
||||||
|
|
||||||
|
componentWillReceiveProps: (newProps) ->
|
||||||
|
@setState(
|
||||||
|
displayedItem: newProps.initialItem
|
||||||
|
user: newProps.initialItem.get 'user'
|
||||||
|
otherItemPreviewString: @setUpOtherItemsQuery(newProps.otherItems)
|
||||||
|
)
|
||||||
|
|
||||||
|
setUpOtherItemsQuery: (otherItems) ->
|
||||||
|
otherItems.map((item) ->
|
||||||
|
item.id
|
||||||
|
).join(',')
|
||||||
|
|
||||||
|
openInfoPanel: (event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
@setState({showInfoPanel: !@state.showInfoPanel});
|
||||||
|
|
||||||
|
toggleFooter: (event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
@setState({showFooter: !@state.showFooter});
|
||||||
|
|
||||||
|
handleKeyboardNavigation: (event) ->
|
||||||
|
return null unless (event.keyCode is 37 or event.keyCode is 39)
|
||||||
|
# left arrow
|
||||||
|
if (event.keyCode is 37)
|
||||||
|
nextItem = collectionHandler.getPreviousInRelationTo(@props.otherItems, @state.displayedItem)
|
||||||
|
|
||||||
|
# right arrow
|
||||||
|
if (event.keyCode is 39)
|
||||||
|
nextItem = collectionHandler.getNextInRelationTo(@props.otherItems, @state.displayedItem)
|
||||||
|
|
||||||
|
if (@props.otherItemsString)
|
||||||
|
ReactRouter.transitionTo((if @props.params.splat then 'folder' else 'rootFolder'), @props.params, {preview: nextItem.id, only_preview: @props.otherItemsString})
|
||||||
|
else
|
||||||
|
ReactRouter.transitionTo((if @props.params.splat then 'folder' else 'rootFolder'), @props.params, {preview: nextItem.id})
|
||||||
|
|
||||||
|
getStatusMessage: ->
|
||||||
|
'A nice status message ;) ' #TODO: Actually do this..
|
||||||
|
|
||||||
|
renderPreview: ->
|
||||||
|
fileNameParts = @state.displayedItem.displayName().split('.')
|
||||||
|
fileExt = fileNameParts[fileNameParts.length - 1].toUpperCase()
|
||||||
|
contentType = @state.displayedItem.get('content-type')
|
||||||
|
div {className: if @state.showInfoPanel then 'ef-file-preview-item full-height col-xs-6' else 'ef-file-preview-item full-height col-xs-10'},
|
||||||
|
if contentType.substring(0, contentType.indexOf('/')) is 'image'
|
||||||
|
img {className: 'ef-file-preview-image' ,src: @state.displayedItem.get('url')}
|
||||||
|
else
|
||||||
|
h1 {className: 'ef-file-preview-not-available'},
|
||||||
|
"Previewing a #{fileExt} file is not yet available."
|
||||||
|
|
||||||
|
renderArrowLink: (direction) ->
|
||||||
|
# TODO: Refactor this to use the collectionHandler
|
||||||
|
# Get the current position in the collection
|
||||||
|
curItemIndex = @props.otherItems.indexOf(@state.displayedItem)
|
||||||
|
switch direction
|
||||||
|
when 'left'
|
||||||
|
goToItemIndex = curItemIndex - 1;
|
||||||
|
if goToItemIndex < 0
|
||||||
|
goToItemIndex = @props.otherItems.length - 1
|
||||||
|
when 'right'
|
||||||
|
goToItemIndex = curItemIndex + 1;
|
||||||
|
if goToItemIndex > @props.otherItems.length - 1
|
||||||
|
goToItemIndex = 0
|
||||||
|
goToItem = if @state.otherItemsIsBackBoneCollection then @props.otherItems.at(goToItemIndex) else @props.otherItems[goToItemIndex]
|
||||||
|
if (@props.otherItemsString)
|
||||||
|
@props.params.only_preview = @props.otherItemsString
|
||||||
|
switch direction
|
||||||
|
when 'left'
|
||||||
|
div {className: 'col-xs-1 full-height'},
|
||||||
|
ReactRouter.Link _.defaults({to: (if @props.params.splat then 'folder' else 'rootFolder'), query: {preview: goToItem.id}, className: 'ef-file-preview-arrow-link'}, @props.params),
|
||||||
|
div {className: 'ef-file-preview-arrow'},
|
||||||
|
i {className: 'icon-arrow-open-left'}
|
||||||
|
when 'right'
|
||||||
|
div {className: 'col-xs-1 full-height'},
|
||||||
|
ReactRouter.Link _.defaults({to: (if @props.params.splat then 'folder' else 'rootFolder'), query: {preview: goToItem.id}, className: 'ef-file-preview-arrow-link'}, @props.params),
|
||||||
|
div {className: 'ef-file-preview-arrow'},
|
||||||
|
i {className: 'icon-arrow-open-right'}
|
||||||
|
|
||||||
|
|
||||||
|
scrollLeft: (event) ->
|
||||||
|
width = $('.ef-file-preview-footer-list').width();
|
||||||
|
console.log("left scroll");
|
||||||
|
$('.ef-file-preview-footer-list').animate({
|
||||||
|
scrollLeft: '-=' + width
|
||||||
|
}, 300, 'easeOutQuad')
|
||||||
|
|
||||||
|
scrollRight: (event) ->
|
||||||
|
width = $('.ef-file-preview-footer-list').width();
|
||||||
|
console.log("right scroll");
|
||||||
|
$('.ef-file-preview-footer-list').animate({
|
||||||
|
scrollLeft: '+=' + width
|
||||||
|
}, 300, 'easeOutQuad')
|
||||||
|
|
||||||
|
closeModal: ->
|
||||||
|
ReactRouter.transitionTo((if @props.params.splat then 'folder' else 'rootFolder'), @props.params)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
render: withReactDOM ->
|
||||||
|
ReactModal {isOpen: true, onRequestClose: @closeModal, closeTimeoutMS: 10},
|
||||||
|
div {className: 'ef-file-preview-overlay'},
|
||||||
|
div {className: 'ef-file-preview-container'},
|
||||||
|
div {className: 'ef-file-preview-header grid-row middle-xs'},
|
||||||
|
div {className: 'col-xs'},
|
||||||
|
div {className: 'ef-file-preview-header-filename-container'},
|
||||||
|
h1 {className: 'ef-file-preview-header-filename'},
|
||||||
|
@props.initialItem.displayName()
|
||||||
|
div {className: 'col-xs end-xs'},
|
||||||
|
div {className: 'ef-file-preview-header-buttons'},
|
||||||
|
a {className: 'ef-file-preview-header-download ef-file-preview-button', href: @state.displayedItem.get('url')},
|
||||||
|
i {className: 'icon-download'} #Replace with actual icon
|
||||||
|
I18n.t('file_preview_headerbutton_download', ' Download')
|
||||||
|
a {className: 'ef-file-preview-header-info ef-file-preview-button', href: '#', onClick: @openInfoPanel},
|
||||||
|
i {className: 'icon-info'}
|
||||||
|
I18n.t('file_preview_headerbutton_info', ' Info')
|
||||||
|
ReactRouter.Link _.defaults({to: (if @props.params.splat then 'folder' else 'rootFolder'), query: '', className: 'ef-file-preview-header-close ef-file-preview-button'}, @props.params),
|
||||||
|
i {className: 'icon-end'}
|
||||||
|
I18n.t('file_preview_headerbutton_close', ' Close')
|
||||||
|
div {className: 'ef-file-preview-preview grid-row middle-xs'},
|
||||||
|
# We need to render out the left/right arrows
|
||||||
|
@renderArrowLink('left') if @props.otherItems.length > 0
|
||||||
|
@renderPreview()
|
||||||
|
@renderArrowLink('right') if @props.otherItems.length > 0
|
||||||
|
if @state.showInfoPanel
|
||||||
|
div {className: 'col-xs-4 full-height ef-file-preview-information'},
|
||||||
|
table {className: 'ef-file-preview-infotable'},
|
||||||
|
tbody {},
|
||||||
|
tr {},
|
||||||
|
th {scope: 'row'},
|
||||||
|
I18n.t('file_preview_infotable_name', 'Name')
|
||||||
|
td {},
|
||||||
|
@state.displayedItem.displayName()
|
||||||
|
tr {},
|
||||||
|
th {scope: 'row'},
|
||||||
|
I18n.t('file_preview_infotable_status', 'Status')
|
||||||
|
td {},
|
||||||
|
@getStatusMessage();
|
||||||
|
tr {},
|
||||||
|
th {scope: 'row'},
|
||||||
|
I18n.t('file_preview_infotable_kind', 'Kind')
|
||||||
|
td {},
|
||||||
|
@state.displayedItem.get 'content-type'
|
||||||
|
tr {},
|
||||||
|
th {scope: 'row'},
|
||||||
|
I18n.t('file_preview_infotable_size', 'Size')
|
||||||
|
td {},
|
||||||
|
@state.displayedItem.get('size') + ' Kb'
|
||||||
|
tr {},
|
||||||
|
th {scope: 'row'},
|
||||||
|
I18n.t('file_preview_infotable_datemodified', 'Date Modified')
|
||||||
|
td {},
|
||||||
|
FriendlyDatetime datetime: @state.displayedItem.get('updated_at')
|
||||||
|
tr {},
|
||||||
|
th {scope: 'row'},
|
||||||
|
I18n.t('file_preview_infotable_modifiedby', 'Modified By')
|
||||||
|
td {},
|
||||||
|
img {className: 'avatar', src: @state.user?.avatar_image_url }
|
||||||
|
a {href: @state.user?.html_url},
|
||||||
|
@state.user?.display_name
|
||||||
|
tr {},
|
||||||
|
th {scope: 'row'},
|
||||||
|
I18n.t('file_preview_infotable_datecreated', 'Date Created')
|
||||||
|
td {},
|
||||||
|
FriendlyDatetime datetime: @state.displayedItem.get('created_at')
|
||||||
|
div {className: 'ef-file-preview-toggle-row grid-row middle-xs'},
|
||||||
|
if @state.showFooterBtn
|
||||||
|
a {className: 'ef-file-preview-toggle col-xs-1 off-xs-1', href: '#', onClick: @toggleFooter, role: 'button', style: {bottom: '21%'} if @state.showFooter},
|
||||||
|
if @state.showFooter
|
||||||
|
I18n.t('file_preview_hide', 'Hide')
|
||||||
|
else
|
||||||
|
I18n.t('file_preview_show', 'Show')
|
||||||
|
if @state.showFooter
|
||||||
|
div {className: 'ef-file-preview-footer grid-row'},
|
||||||
|
div {className: 'col-xs-1', onClick: @scrollLeft},
|
||||||
|
div {className: 'ef-file-preview-footer-arrow'},
|
||||||
|
i {className: 'icon-arrow-open-left'}
|
||||||
|
div {className: 'col-xs-10'},
|
||||||
|
ul {className: 'ef-file-preview-footer-list'},
|
||||||
|
@props.otherItems.map (file) =>
|
||||||
|
li {className: 'ef-file-preview-footer-list-item', key: file.id},
|
||||||
|
figure {className: 'ef-file-preview-footer-item'},
|
||||||
|
ReactRouter.Link _.defaults({to: (if @props.params.splat then 'folder' else 'rootFolder'), query: {preview: file.id}, className: ''}, @props.params),
|
||||||
|
div {
|
||||||
|
className: if file.displayName() is @state.displayedItem.displayName() then 'ef-file-preview-footer-image ef-file-preview-footer-active' else 'ef-file-preview-footer-image'
|
||||||
|
style: {'background-image': 'url(' + file.get('thumbnail_url') + ')'}
|
||||||
|
}
|
||||||
|
figcaption {},
|
||||||
|
file.displayName()
|
||||||
|
div {className: 'col-xs-1', onClick: @scrollRight},
|
||||||
|
div {className: 'ef-file-preview-footer-arrow'},
|
||||||
|
i {className: 'icon-arrow-open-right'}
|
|
@ -11,14 +11,16 @@ define [
|
||||||
'../utils/updateAPIQuerySortParams'
|
'../utils/updateAPIQuerySortParams'
|
||||||
'compiled/models/Folder'
|
'compiled/models/Folder'
|
||||||
'./CurrentUploads'
|
'./CurrentUploads'
|
||||||
|
'./FilePreview'
|
||||||
'./UploadDropZone'
|
'./UploadDropZone'
|
||||||
], (_, React, I18n, withReactDOM, filesEnv, ColumnHeaders, LoadingIndicator, FolderChild, getAllPages, updateAPIQuerySortParams, Folder, CurrentUploads, UploadDropZone) ->
|
], (_, React, I18n, withReactDOM, filesEnv, ColumnHeaders, LoadingIndicator, FolderChild, getAllPages, updateAPIQuerySortParams, Folder, CurrentUploads, FilePreview, UploadDropZone) ->
|
||||||
|
|
||||||
LEADING_SLASH_TILL_BUT_NOT_INCLUDING_NEXT_SLASH = /^\/[^\/]*/
|
LEADING_SLASH_TILL_BUT_NOT_INCLUDING_NEXT_SLASH = /^\/[^\/]*/
|
||||||
|
|
||||||
ShowFolder = React.createClass
|
ShowFolder = React.createClass
|
||||||
displayName: 'ShowFolder'
|
displayName: 'ShowFolder'
|
||||||
|
|
||||||
|
|
||||||
debouncedForceUpdate: _.debounce ->
|
debouncedForceUpdate: _.debounce ->
|
||||||
@forceUpdate() if @isMounted()
|
@forceUpdate() if @isMounted()
|
||||||
, 0
|
, 0
|
||||||
|
@ -66,6 +68,7 @@ define [
|
||||||
setTimeout =>
|
setTimeout =>
|
||||||
@props.onResolvePath({currentFolder:undefined, rootTillCurrentFolder:undefined})
|
@props.onResolvePath({currentFolder:undefined, rootTillCurrentFolder:undefined})
|
||||||
|
|
||||||
|
|
||||||
componentWillReceiveProps: (newProps) ->
|
componentWillReceiveProps: (newProps) ->
|
||||||
@unregisterListeners()
|
@unregisterListeners()
|
||||||
return unless newProps.currentFolder
|
return unless newProps.currentFolder
|
||||||
|
@ -99,6 +102,28 @@ define [
|
||||||
|
|
||||||
LoadingIndicator isLoading: @props.currentFolder.folders.fetchingNextPage || @props.currentFolder.files.fetchingNextPage
|
LoadingIndicator isLoading: @props.currentFolder.folders.fetchingNextPage || @props.currentFolder.files.fetchingNextPage
|
||||||
|
|
||||||
|
# Prepare and render the FilePreview if needed.
|
||||||
|
# As long as ?preview is present in the url.
|
||||||
|
if @props.query.preview?
|
||||||
|
# Sets up our collection that we will be using.
|
||||||
|
onlyIdsToPreview = @props.query.only_preview?.split(',')
|
||||||
|
otherItems = if onlyIdsToPreview # expects this to be [1,2,34,9] (ids of files to preview)
|
||||||
|
@props.currentFolder.files.filter (file) ->
|
||||||
|
file.id in onlyIdsToPreview
|
||||||
|
else
|
||||||
|
@props.currentFolder.files
|
||||||
|
# If preview contains data (i.e. ?preview=4)
|
||||||
|
if @props.query.preview
|
||||||
|
# We go back to the folder to pull this data.
|
||||||
|
initialItem = @props.currentFolder.files.get(@props.query.preview)
|
||||||
|
# If preview doesn't contain data (i.e. ?preview)
|
||||||
|
# we'll just use the first one in our otherItems collection.
|
||||||
|
else
|
||||||
|
# Because otherItems may (or may not be) a Backbone collection (FilesCollection) we change up our method.
|
||||||
|
initialItem = if otherItems instanceof Backbone.Collection then otherItems.first() else _.first(otherItems)
|
||||||
|
# Makes sure other items has something before sending it to the preview.
|
||||||
|
if otherItems?.length
|
||||||
|
if @props.query.only_preview
|
||||||
|
FilePreview {initialItem: initialItem, otherItems: otherItems, params: @props.params, appElement: document.getElementById('content'), otherItemsString: @props.query.only_preview}
|
||||||
|
else
|
||||||
|
FilePreview {initialItem: initialItem, otherItems: otherItems, params: @props.params, appElement: document.getElementById('content')}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
define [
|
define [
|
||||||
|
'underscore'
|
||||||
'i18n!react_files'
|
'i18n!react_files'
|
||||||
'react'
|
'react'
|
||||||
'react-router'
|
'react-router'
|
||||||
|
@ -10,7 +11,7 @@ define [
|
||||||
'./RestrictedDialogForm'
|
'./RestrictedDialogForm'
|
||||||
'jquery'
|
'jquery'
|
||||||
'compiled/jquery.rails_flash_notifications'
|
'compiled/jquery.rails_flash_notifications'
|
||||||
], (I18n, React, Router, withReactDOM, UploadButton, openMoveDialog, downloadStuffAsAZip, customPropTypes, RestrictedDialogForm, $) ->
|
], (_, I18n, React, Router, withReactDOM, UploadButton, openMoveDialog, downloadStuffAsAZip, customPropTypes, RestrictedDialogForm, $) ->
|
||||||
|
|
||||||
Toolbar = React.createClass
|
Toolbar = React.createClass
|
||||||
displayName: 'Toolbar'
|
displayName: 'Toolbar'
|
||||||
|
@ -20,6 +21,7 @@ define [
|
||||||
contextType: customPropTypes.contextType.isRequired
|
contextType: customPropTypes.contextType.isRequired
|
||||||
contextId: customPropTypes.contextId.isRequired
|
contextId: customPropTypes.contextId.isRequired
|
||||||
|
|
||||||
|
|
||||||
onSubmitSearch: (event) ->
|
onSubmitSearch: (event) ->
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
query = {search_term: @refs.searchTerm.getDOMNode().value}
|
query = {search_term: @refs.searchTerm.getDOMNode().value}
|
||||||
|
@ -50,6 +52,17 @@ define [
|
||||||
$.flashMessage I18n.t('deleted_items_successfully', '%{count} items deleted successfully', {count})
|
$.flashMessage I18n.t('deleted_items_successfully', '%{count} items deleted successfully', {count})
|
||||||
@props.clearSelectedItems()
|
@props.clearSelectedItems()
|
||||||
|
|
||||||
|
getPreviewQuery: ->
|
||||||
|
return unless @props.selectedItems.length
|
||||||
|
if @props.selectedItems.length is 1
|
||||||
|
{preview: @props.selectedItems[0].id}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
preview: @props.selectedItems[0].id
|
||||||
|
only_preview: @props.selectedItems.map((item) -> item.id).join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Function Summary
|
# Function Summary
|
||||||
# Create a blank dialog window via jQuery, then dump the RestrictedDialogForm into that
|
# Create a blank dialog window via jQuery, then dump the RestrictedDialogForm into that
|
||||||
# dialog window. This allows us to do react things inside of this all ready rendered
|
# dialog window. This allows us to do react things inside of this all ready rendered
|
||||||
|
@ -96,14 +109,14 @@ define [
|
||||||
|
|
||||||
div className: "ui-buttonset col-xs #{'screenreader-only' unless showingButtons}",
|
div className: "ui-buttonset col-xs #{'screenreader-only' unless showingButtons}",
|
||||||
|
|
||||||
button {
|
Router.Link {
|
||||||
disabled: !showingButtons
|
to: (if @props.currentFolder?.urlPath() then 'folder' else 'rootFolder'),
|
||||||
className: 'ui-button btn-view'
|
query: @getPreviewQuery()
|
||||||
onClick: alert.bind(null, 'TODO: handle CNVS-14727 actually implement previewing of files')
|
splat: @props.currentFolder?.urlPath()
|
||||||
title: I18n.t('view', 'View')
|
className: 'ui-button btn-view'
|
||||||
'aria-label': I18n.t('view', 'View')
|
title: I18n.t('view', 'View')
|
||||||
'data-tooltip': ''
|
'data-tooltip': ''
|
||||||
},
|
},
|
||||||
i className: 'icon-search'
|
i className: 'icon-search'
|
||||||
|
|
||||||
if @props.userCanManageFilesForContext
|
if @props.userCanManageFilesForContext
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
#
|
||||||
|
# Handles navigation through a collection.
|
||||||
|
#
|
||||||
|
define [], ->
|
||||||
|
|
||||||
|
CollectionHandler =
|
||||||
|
|
||||||
|
isBackboneCollection: (collection) ->
|
||||||
|
return collection instanceof Backbone.Collection
|
||||||
|
|
||||||
|
# Get the previous item in a collection.
|
||||||
|
getPreviousInRelationTo : (collection, collectionItem) ->
|
||||||
|
|
||||||
|
isBackbone = @isBackboneCollection(collection)
|
||||||
|
|
||||||
|
itemIndex = collection.indexOf(collectionItem)
|
||||||
|
|
||||||
|
# Return null if the item wasn't found.
|
||||||
|
return null unless itemIndex >= 0
|
||||||
|
|
||||||
|
nextIndex = itemIndex - 1
|
||||||
|
|
||||||
|
# Return the last item if we were at the first.
|
||||||
|
if nextIndex < 0
|
||||||
|
if isBackbone
|
||||||
|
return collection.at(collection.length - 1)
|
||||||
|
else
|
||||||
|
return collection[collection.length - 1]
|
||||||
|
|
||||||
|
# Otherwise let's just return the previous item.
|
||||||
|
if isBackbone then collection.at(nextIndex) else collection[nextIndex]
|
||||||
|
|
||||||
|
# Get the next item in a collection.
|
||||||
|
getNextInRelationTo : (collection, collectionItem) ->
|
||||||
|
|
||||||
|
isBackbone = @isBackboneCollection(collection)
|
||||||
|
|
||||||
|
itemIndex = collection.indexOf(collectionItem)
|
||||||
|
|
||||||
|
# Return null if the item wasn't found.
|
||||||
|
return null unless itemIndex >= 0
|
||||||
|
|
||||||
|
nextIndex = itemIndex + 1
|
||||||
|
|
||||||
|
# Return the first item if we were at the last.
|
||||||
|
if nextIndex > collection.length - 1
|
||||||
|
if isBackbone
|
||||||
|
return collection.at(0)
|
||||||
|
else
|
||||||
|
return collection[0]
|
||||||
|
# Otherwise let's just return the next item.
|
||||||
|
if isBackbone then collection.at(nextIndex) else collection[nextIndex]
|
|
@ -329,3 +329,207 @@ i[class*=UploadDropZone__instructions--icon-upload] {
|
||||||
// Hack, Hack, Hack!
|
// Hack, Hack, Hack!
|
||||||
// to make sure that there is space for the ItemCog menu to appear below the bottom thing in the list of files
|
// to make sure that there is space for the ItemCog menu to appear below the bottom thing in the list of files
|
||||||
#footer { min-height: 80px }
|
#footer { min-height: 80px }
|
||||||
|
|
||||||
|
// Make sure that ReactModal gets above everything else
|
||||||
|
.ReactModal__Overlay {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-overlay {
|
||||||
|
position: fixed;
|
||||||
|
left: 0; top: 0;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.7);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-header {
|
||||||
|
// position: relative;
|
||||||
|
// top: 0;
|
||||||
|
// left: 0;
|
||||||
|
// right: 0;
|
||||||
|
//height: 70px;
|
||||||
|
flex: 0 0 70px;
|
||||||
|
background-color: #000;
|
||||||
|
opacity: 1;
|
||||||
|
border-bottom: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-preview {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
&.full-height {
|
||||||
|
flex: 0 0 400px;
|
||||||
|
}
|
||||||
|
// height: 100vh;
|
||||||
|
// position: absolute;
|
||||||
|
// top: 70px;
|
||||||
|
// left: 0;
|
||||||
|
// right: 0;
|
||||||
|
// height: 100%;
|
||||||
|
// width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-arrow-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-arrow-link {
|
||||||
|
background-color: #000;
|
||||||
|
border: 1px solid #666;
|
||||||
|
text-align: center;
|
||||||
|
height: 100px;
|
||||||
|
padding-top: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-arrow {
|
||||||
|
|
||||||
|
|
||||||
|
// background-color: #000;
|
||||||
|
// height: 100px;
|
||||||
|
// width: 50px;
|
||||||
|
// i {
|
||||||
|
// margin-top: 45px;
|
||||||
|
// margin-left: 13px;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-header-buttons {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-button {
|
||||||
|
color: rgba($canvas-light, 0.8);
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover, &:focus { color: $canvas-light; }
|
||||||
|
@if $use_high_contrast == false {
|
||||||
|
&:hover, &:focus { text-decoration: none; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-header-filename-container {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-header-filename {
|
||||||
|
color: $canvas-light;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ef-file-preview-information {
|
||||||
|
// width: 46%;
|
||||||
|
background-color: #333;
|
||||||
|
align-self: flex-start;
|
||||||
|
// position: absolute;
|
||||||
|
// right: 0;
|
||||||
|
// float: right;
|
||||||
|
// top: 70px;
|
||||||
|
// bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-infotable {
|
||||||
|
th {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-toggle {
|
||||||
|
color: #666;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #000;
|
||||||
|
// width: 100px;
|
||||||
|
// margin-left: 25px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid #666;
|
||||||
|
border-left: 1px solid #666;
|
||||||
|
border-right: 1px solid #666;
|
||||||
|
// position: absolute;
|
||||||
|
// bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-footer {
|
||||||
|
// width: 100%;
|
||||||
|
// height: 20%;
|
||||||
|
// position: absolute;
|
||||||
|
// bottom: 0;
|
||||||
|
background-color: black;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-footer-item
|
||||||
|
{
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
// float: right;
|
||||||
|
color: white;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-image {
|
||||||
|
width: 500px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-item {
|
||||||
|
//background-color: red;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-toggle-row {
|
||||||
|
flex: 0 0 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-footer-list {
|
||||||
|
list-style-type: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-footer-list-item {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-footer-arrow {
|
||||||
|
margin-top: 35%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-footer-active {
|
||||||
|
border: solid $canvas-primary 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-footer-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ef-file-preview-not-available {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #000;
|
||||||
|
padding: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,6 +109,9 @@ module Canvas
|
||||||
deps: ['react'],
|
deps: ['react'],
|
||||||
exports: 'ReactRouter'
|
exports: 'ReactRouter'
|
||||||
},
|
},
|
||||||
|
'bower/react-modal/dist/react-modal': {
|
||||||
|
deps: ['react']
|
||||||
|
},
|
||||||
'bower/ember/ember': {
|
'bower/ember/ember': {
|
||||||
deps: ['jquery', 'handlebars'],
|
deps: ['jquery', 'handlebars'],
|
||||||
exports: 'Ember'
|
exports: 'Ember'
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
define [
|
||||||
|
'react'
|
||||||
|
'react-router'
|
||||||
|
'compiled/react_files/components/FilePreview'
|
||||||
|
'compiled/models/Folder'
|
||||||
|
'compiled/models/File'
|
||||||
|
'compiled/collections/FilesCollection'
|
||||||
|
'compiled/react_files/components/FolderChild'
|
||||||
|
], (React, Router, FilePreview, Folder, File, FilesCollection, FolderChild) ->
|
||||||
|
|
||||||
|
Simulate = React.addons.TestUtils.Simulate
|
||||||
|
|
||||||
|
module 'File Preview Rendering',
|
||||||
|
setup: ->
|
||||||
|
|
||||||
|
#window.React = React
|
||||||
|
|
||||||
|
sinon.stub(Router, 'Link').returns('some link')
|
||||||
|
sinon.stub(Folder, 'resolvePath').returns($.Deferred())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize a few things to view in the preview.
|
||||||
|
@currentFolder = new FilesCollection()
|
||||||
|
|
||||||
|
@file1 = new File({
|
||||||
|
cid: '1'
|
||||||
|
name:'Test File.file1'
|
||||||
|
'content-type': 'unknown/unknown'
|
||||||
|
}, {preflightUrl: ''})
|
||||||
|
@file2 = new File({
|
||||||
|
cid: '2'
|
||||||
|
name:'Test File.file2'
|
||||||
|
'content-type': 'unknown/unknown'
|
||||||
|
}, {preflightUrl: ''})
|
||||||
|
@file3 = new File({
|
||||||
|
cid: '3'
|
||||||
|
name:'Test File.file3'
|
||||||
|
'content-type': 'image/png',
|
||||||
|
'url': 'test/test/test.png'
|
||||||
|
}, {preflightUrl: ''})
|
||||||
|
|
||||||
|
@currentFolder.add(@file1)
|
||||||
|
@currentFolder.add(@file2)
|
||||||
|
@currentFolder.add(@file3)
|
||||||
|
|
||||||
|
properties =
|
||||||
|
initialItem: @file2
|
||||||
|
otherItems: @currentFolder
|
||||||
|
params: {splat: "test/test/test/"}
|
||||||
|
appElement: $('#fixtures').get(0)
|
||||||
|
# onResolvePath: ->
|
||||||
|
# currentFolder: @currentFolder
|
||||||
|
# query: {preview: '2'}
|
||||||
|
# toggleItemSelected: ->
|
||||||
|
# selectedItems: []
|
||||||
|
# areAllItemsSelected: -> false
|
||||||
|
|
||||||
|
|
||||||
|
@filePreview = React.renderComponent(FilePreview(properties), $('#fixtures')[0])
|
||||||
|
|
||||||
|
|
||||||
|
teardown: ->
|
||||||
|
Router.Link.restore()
|
||||||
|
Folder.resolvePath.restore()
|
||||||
|
React.unmountComponentAtNode($('#fixtures')[0])
|
||||||
|
|
||||||
|
|
||||||
|
test 'clicking the info button should render out the info panel', ->
|
||||||
|
infoButton = $('.ef-file-preview-header-info').get(0)
|
||||||
|
Simulate.click(infoButton)
|
||||||
|
ok $('.ef-file-preview-information').length, 'The info panel did not show'
|
||||||
|
|
||||||
|
test 'clicking the Show button should render out the footer', ->
|
||||||
|
showButton = $('.ef-file-preview-toggle').get(0)
|
||||||
|
Simulate.click(showButton)
|
||||||
|
ok $('.ef-file-preview-footer').length, 'The footer did not show'
|
||||||
|
|
||||||
|
test 'clicking the Show button should change the text to Hide', ->
|
||||||
|
showButton = $('.ef-file-preview-toggle').get(0)
|
||||||
|
Simulate.click(showButton)
|
||||||
|
ok $('.ef-file-preview-toggle').text().trim() is "Hide", 'The button text did not become Hide'
|
||||||
|
|
||||||
|
|
||||||
|
#####
|
||||||
|
## The next tests should be fixed once Simulate.keyDown is working properly.
|
||||||
|
#####
|
||||||
|
|
||||||
|
# test 'pressing the left arrow should navigate to the previous item', ->
|
||||||
|
# modal = $('.ReactModal__Overlay').get(0)
|
||||||
|
# Simulate.keyDown(modal, {keyCode: 37})
|
||||||
|
# expected = @file1.get 'name'
|
||||||
|
# actual = $('.ef-file-preview-header-filename').text()
|
||||||
|
# ok actual is expected, 'The previous item did not load'
|
||||||
|
|
||||||
|
|
||||||
|
# test 'pressing the left arrow should navigate to the last item if you are at the beginning', ->
|
||||||
|
# ok false
|
||||||
|
|
||||||
|
# test 'pressing the right arrow should navigate to the next item', ->
|
||||||
|
# ok false
|
||||||
|
|
||||||
|
# test 'pressing the right arrow should navigate to the first item if you are at the end.', ->
|
||||||
|
# ok false
|
||||||
|
|
||||||
|
test 'an image should be previewed if the content type matches', ->
|
||||||
|
@filePreview.setState(displayedItem: @file3)
|
||||||
|
ok $('.ef-file-preview-image').length, 'The image was not displayed'
|
||||||
|
|
||||||
|
test 'files that are not images should display a message indicating they are not able to be viewed currently', ->
|
||||||
|
# TODO: Remove this test when the rest of the preview stuff is enabled.
|
||||||
|
@filePreview.setState(displayedItem: @file1)
|
||||||
|
ok $('.ef-file-preview-not-available').length, 'The not available message was not shown.'
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,16 @@ define [
|
||||||
'compiled/collections/FilesCollection'
|
'compiled/collections/FilesCollection'
|
||||||
], (React, Router, SearchResults, FilesCollection) ->
|
], (React, Router, SearchResults, FilesCollection) ->
|
||||||
module 'SearchResults#render',
|
module 'SearchResults#render',
|
||||||
|
setup: ->
|
||||||
|
sinon.stub(Router, 'Link').returns('link')
|
||||||
|
sinon.stub($, 'ajax').returns($.Deferred().resolve())
|
||||||
|
|
||||||
|
teardown: ->
|
||||||
|
Router.Link.restore()
|
||||||
|
$.ajax.restore()
|
||||||
|
|
||||||
test 'when collection is loaded and empty display no matches found', ->
|
test 'when collection is loaded and empty display no matches found', ->
|
||||||
sinon.stub(Router, 'Link').returns('link')
|
|
||||||
sinon.stub($, 'ajax').returns($.Deferred().resolve())
|
|
||||||
props =
|
props =
|
||||||
params: {}
|
params: {}
|
||||||
query: {}
|
query: {}
|
||||||
|
@ -28,5 +35,4 @@ define [
|
||||||
ok @searchResults.refs.noResultsFound, 'Displays the no results text'
|
ok @searchResults.refs.noResultsFound, 'Displays the no results text'
|
||||||
|
|
||||||
React.unmountComponentAtNode($('#fixtures')[0])
|
React.unmountComponentAtNode($('#fixtures')[0])
|
||||||
Router.Link.restore()
|
|
||||||
$.ajax.restore()
|
|
||||||
|
|
|
@ -3,12 +3,14 @@ define [
|
||||||
'react'
|
'react'
|
||||||
'react-router'
|
'react-router'
|
||||||
'compiled/react_files/components/Toolbar'
|
'compiled/react_files/components/Toolbar'
|
||||||
], ($, React, Router, Toolbar) ->
|
'compiled/react_files/routes'
|
||||||
|
], ($, React, Router, Toolbar, routes) ->
|
||||||
|
|
||||||
Simulate = React.addons.TestUtils.Simulate
|
Simulate = React.addons.TestUtils.Simulate
|
||||||
|
|
||||||
module 'Toolbar',
|
module 'Toolbar',
|
||||||
setup: ->
|
setup: ->
|
||||||
|
@routes = React.addons.TestUtils.renderIntoDocument(routes)
|
||||||
@toolbar = React.renderComponent(Toolbar({params: 'foo', query:'', selectedItems: '', contextId: "1", contextType: "courses"}), $('<div>').appendTo('body')[0])
|
@toolbar = React.renderComponent(Toolbar({params: 'foo', query:'', selectedItems: '', contextId: "1", contextType: "courses"}), $('<div>').appendTo('body')[0])
|
||||||
teardown: ->
|
teardown: ->
|
||||||
React.unmountComponentAtNode(@toolbar.getDOMNode().parentNode)
|
React.unmountComponentAtNode(@toolbar.getDOMNode().parentNode)
|
||||||
|
|
Loading…
Reference in New Issue