diff options
author | Christopher Speller <crspeller@gmail.com> | 2015-08-18 08:46:14 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2015-08-18 08:46:14 -0400 |
commit | 96d1eb1c800a427e31e63970e57d0824a3bc91e3 (patch) | |
tree | 4bf9b926fa3877de9bafaafcc0831e724e6fc7a3 | |
parent | ab197e98358f4f48b81669182a361b6641132029 (diff) | |
parent | 2c098d7711eda893f903329ab64528a7d387a6e8 (diff) | |
download | chat-96d1eb1c800a427e31e63970e57d0824a3bc91e3.tar.gz chat-96d1eb1c800a427e31e63970e57d0824a3bc91e3.tar.bz2 chat-96d1eb1c800a427e31e63970e57d0824a3bc91e3.zip |
Merge pull request #378 from rgarmsen2295/mm-316c
MM-316 Allows users to drag and drop files from their computer to the center pane or RHS to upload them
-rw-r--r-- | web/react/components/create_comment.jsx | 22 | ||||
-rw-r--r-- | web/react/components/create_post.jsx | 22 | ||||
-rw-r--r-- | web/react/components/file_upload.jsx | 101 | ||||
-rw-r--r-- | web/react/components/file_upload_overlay.jsx | 26 | ||||
-rw-r--r-- | web/react/components/post_right.jsx | 3 | ||||
-rw-r--r-- | web/react/pages/channel.jsx | 7 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_base.scss | 1 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_files.scss | 15 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_post.scss | 26 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_responsive.scss | 6 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_sidebar--left.scss | 1 | ||||
-rw-r--r-- | web/static/js/jquery-dragster/LICENSE | 21 | ||||
-rw-r--r-- | web/static/js/jquery-dragster/README.md | 17 | ||||
-rw-r--r-- | web/static/js/jquery-dragster/jquery.dragster.js | 85 | ||||
-rw-r--r-- | web/templates/channel.html | 1 | ||||
-rw-r--r-- | web/templates/head.html | 2 |
16 files changed, 325 insertions, 31 deletions
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 78e06c532..885efab7a 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -122,16 +122,20 @@ module.exports = React.createClass({ this.setState({uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews']}); }, handleUploadError: function(err, clientId) { - var draft = PostStore.getCommentDraft(this.props.rootId); + if (clientId !== -1) { + var draft = PostStore.getCommentDraft(this.props.rootId); - var index = draft['uploadsInProgress'].indexOf(clientId); - if (index !== -1) { - draft['uploadsInProgress'].splice(index, 1); - } + var index = draft['uploadsInProgress'].indexOf(clientId); + if (index !== -1) { + draft['uploadsInProgress'].splice(index, 1); + } - PostStore.storeCommentDraft(this.props.rootId, draft); + PostStore.storeCommentDraft(this.props.rootId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + } else { + this.setState({serverError: err}); + } }, clearPreviews: function() { this.setState({previews: []}); @@ -222,7 +226,9 @@ module.exports = React.createClass({ getFileCount={this.getFileCount} onUploadStart={this.handleUploadStart} onFileUpload={this.handleFileUploadComplete} - onUploadError={this.handleUploadError} /> + onUploadError={this.handleUploadError} + postType='comment' + channelId={this.props.channelId} /> </div> <MsgTyping channelId={this.props.channelId} parentId={this.props.rootId} /> <div className={postFooterClassName}> diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 9ca1d5388..377e7bd34 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -145,16 +145,20 @@ module.exports = React.createClass({ this.setState({uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews']}); }, handleUploadError: function(err, clientId) { - var draft = PostStore.getDraft(this.state.channelId); + if (clientId !== -1) { + var draft = PostStore.getDraft(this.state.channelId); - var index = draft['uploadsInProgress'].indexOf(clientId); - if (index !== -1) { - draft['uploadsInProgress'].splice(index, 1); - } + var index = draft['uploadsInProgress'].indexOf(clientId); + if (index !== -1) { + draft['uploadsInProgress'].splice(index, 1); + } - PostStore.storeDraft(this.state.channelId, draft); + PostStore.storeDraft(this.state.channelId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + } else { + this.setState({serverError: err}); + } }, removePreview: function(id) { var previews = this.state.previews; @@ -262,7 +266,9 @@ module.exports = React.createClass({ getFileCount={this.getFileCount} onUploadStart={this.handleUploadStart} onFileUpload={this.handleFileUploadComplete} - onUploadError={this.handleUploadError} /> + onUploadError={this.handleUploadError} + postType='post' + channelId='' /> </div> <div className={postFooterClassName}> {postError} diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index c1fab669c..7497ec330 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -12,7 +12,9 @@ module.exports = React.createClass({ onUploadError: React.PropTypes.func, getFileCount: React.PropTypes.func, onFileUpload: React.PropTypes.func, - onUploadStart: React.PropTypes.func + onUploadStart: React.PropTypes.func, + channelId: React.PropTypes.string, + postType: React.PropTypes.string }, getInitialState: function() { return {requests: {}}; @@ -21,7 +23,7 @@ module.exports = React.createClass({ var element = $(this.refs.fileInput.getDOMNode()); var files = element.prop('files'); - var channelId = ChannelStore.getCurrentId(); + var channelId = this.props.channelId || ChannelStore.getCurrentId(); this.props.onUploadError(null); @@ -61,8 +63,8 @@ module.exports = React.createClass({ this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); var requests = this.state.requests; - for (var i = 0; i < parsedData.client_ids.length; i++) { - delete requests[parsedData.client_ids[i]]; + for (var j = 0; j < parsedData.client_ids.length; j++) { + delete requests[parsedData.client_ids[j]]; } this.setState({requests: requests}); }.bind(this), @@ -87,10 +89,94 @@ module.exports = React.createClass({ } } catch(e) {} }, + handleDrop: function(e) { + this.props.onUploadError(null); + + var files = e.originalEvent.dataTransfer.files; + var channelId = this.props.channelId || ChannelStore.getCurrentId(); + + if (typeof files !== 'string' && files.length) { + var numFiles = files.length; + + var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId), numFiles); + + if (numFiles > numToUpload) { + this.props.onUploadError('Uploads limited to ' + Constants.MAX_UPLOAD_FILES + ' files maximum. Please use additional posts for more files.'); + } + + for (var i = 0; i < files.length && i < numToUpload; i++) { + if (files[i].size > Constants.MAX_FILE_SIZE) { + this.props.onUploadError('Files must be no more than ' + Constants.MAX_FILE_SIZE / 1000000 + ' MB'); + continue; + } + + // generate a unique id that can be used by other components to refer back to this file upload + var clientId = utils.generateId(); + + // Prepare data to be uploaded. + var formData = new FormData(); + formData.append('channel_id', channelId); + formData.append('files', files[i], files[i].name); + formData.append('client_ids', clientId); + + var request = client.uploadFile(formData, + function(data) { + var parsedData = $.parseJSON(data); + this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); + + var requests = this.state.requests; + for (var j = 0; j < parsedData.client_ids.length; j++) { + delete requests[parsedData.client_ids[j]]; + } + this.setState({requests: requests}); + }.bind(this), + function(err) { + this.props.onUploadError(err, clientId); + }.bind(this) + ); + + var requests = this.state.requests; + requests[clientId] = request; + this.setState({requests: requests}); + + this.props.onUploadStart([clientId], channelId); + } + } else { + this.props.onUploadError('Invalid file upload', -1); + } + }, componentDidMount: function() { var inputDiv = this.refs.input.getDOMNode(); var self = this; + if (this.props.postType === 'post') { + $('.row.main').dragster({ + enter: function() { + $('.center-file-overlay').removeClass('hidden'); + }, + leave: function() { + $('.center-file-overlay').addClass('hidden'); + }, + drop: function(dragsterEvent, e) { + $('.center-file-overlay').addClass('hidden'); + self.handleDrop(e); + } + }); + } else if (this.props.postType === 'comment') { + $('.post-right__container').dragster({ + enter: function() { + $('.right-file-overlay').removeClass('hidden'); + }, + leave: function() { + $('.right-file-overlay').addClass('hidden'); + }, + drop: function(dragsterEvent, e) { + $('.right-file-overlay').addClass('hidden'); + self.handleDrop(e); + } + }); + } + document.addEventListener('paste', function(e) { var textarea = $(inputDiv.parentNode.parentNode).find('.custom-textarea')[0]; @@ -133,14 +219,13 @@ module.exports = React.createClass({ continue; } - var channelId = ChannelStore.getCurrentId(); + var channelId = this.props.channelId || ChannelStore.getCurrentId(); // generate a unique id that can be used by other components to refer back to this file upload var clientId = utils.generateId(); var formData = new FormData(); formData.append('channel_id', channelId); - var d = new Date(); var hour; if (d.getHours() < 10) { @@ -165,8 +250,8 @@ module.exports = React.createClass({ self.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); var requests = self.state.requests; - for (var i = 0; i < parsedData.client_ids.length; i++) { - delete requests[parsedData.client_ids[i]]; + for (var j = 0; j < parsedData.client_ids.length; j++) { + delete requests[parsedData.client_ids[j]]; } self.setState({requests: requests}); }, diff --git a/web/react/components/file_upload_overlay.jsx b/web/react/components/file_upload_overlay.jsx new file mode 100644 index 000000000..f35556371 --- /dev/null +++ b/web/react/components/file_upload_overlay.jsx @@ -0,0 +1,26 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +module.exports = React.createClass({ + displayName: 'FileUploadOverlay', + propTypes: { + overlayType: React.PropTypes.string + }, + render: function() { + var overlayClass = 'file-overlay hidden'; + if (this.props.overlayType === 'right') { + overlayClass += ' right-file-overlay'; + } else if (this.props.overlayType === 'center') { + overlayClass += ' center-file-overlay'; + } + + return ( + <div className={overlayClass}> + <div> + <i className='fa fa-upload'></i> + <span>Drop a file to upload it.</span> + </div> + </div> + ); + } +}); diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index ad8b54012..e46979ff7 100644 --- a/web/react/components/post_right.jsx +++ b/web/react/components/post_right.jsx @@ -11,6 +11,7 @@ var SearchBox =require('./search_bar.jsx'); var CreateComment = require( './create_comment.jsx' ); var Constants = require('../utils/constants.jsx'); var FileAttachmentList = require('./file_attachment_list.jsx'); +var FileUploadOverlay = require('./file_upload_overlay.jsx'); var ActionTypes = Constants.ActionTypes; RhsHeaderPost = React.createClass({ @@ -296,6 +297,8 @@ module.exports = React.createClass({ return ( <div className="post-right__container"> + <FileUploadOverlay + overlayType='right' /> <div className="search-bar__container sidebar--right__search-header">{searchForm}</div> <div className="sidebar-right__body"> <RhsHeaderPost fromSearch={this.props.fromSearch} isMentionSearch={this.props.isMentionSearch} /> diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 929499715..6e4baa582 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -34,6 +34,7 @@ var ChannelInfoModal = require('../components/channel_info_modal.jsx'); var AccessHistoryModal = require('../components/access_history_modal.jsx'); var ActivityLogModal = require('../components/activity_log_modal.jsx'); var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx') +var FileUploadOverlay = require('../components/file_upload_overlay.jsx'); var AsyncClient = require('../utils/async_client.jsx'); @@ -224,4 +225,10 @@ global.window.setup_channel_page = function(team_name, team_type, team_id, chann document.getElementById('removed_from_channel_modal') ); + React.render( + <FileUploadOverlay + overlayType='center' />, + document.getElementById('file_upload_overlay') + ); + }; diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 78006ff18..5b68b488f 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -24,6 +24,7 @@ body { height: 100%; > .row.main { height: 100%; + position: relative; } } > .container-fluid { diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss index ca06d7def..1375a10e7 100644 --- a/web/sass-files/sass/partials/_files.scss +++ b/web/sass-files/sass/partials/_files.scss @@ -193,11 +193,12 @@ border-right: 1px solid #ddd; vertical-align: center; - // helper to center the image icon in the preview window - .file-details__preview-helper { - height: 100%; - display: inline-block; - vertical-align: middle; - } - } + // helper to center the image icon in the preview window + .file-details__preview-helper { + height: 100%; + display: inline-block; + vertical-align: middle; } + } +} + diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 98b17120d..56b31205b 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -106,6 +106,32 @@ body.ios { } } +.file-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + text-align: center; + color: #FFF; + display: table; + font-size: 1.7em; + font-weight: 600; + z-index: 6; + + > div { + display: table-cell; + vertical-align: middle; + } + + .fa { + display: block; + font-size: 2em; + margin: 0 0 0.3em; + } +} + #post-list { .post-list-holder-by-time { background: #fff; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index f28df1f89..733d81c2b 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -189,6 +189,9 @@ } @media screen and (max-width: 960px) { + .center-file-overlay { + font-size: 1.5em; + } .post { .post-header .post-header-col.post-header__reply { .comment-icon__container__hide { @@ -240,6 +243,9 @@ } } @media screen and (max-width: 768px) { + .center-file-overlay { + font-size: 1.3em; + } .date-separator, .new-separator { &.hovered--after { &:before { diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 5d866715e..2376c9212 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -6,6 +6,7 @@ border-right: $border-gray; padding: 0 0 2em 0; background: #fafafa; + z-index: 5; &.sidebar--padded { padding-top: 44px; } diff --git a/web/static/js/jquery-dragster/LICENSE b/web/static/js/jquery-dragster/LICENSE new file mode 100644 index 000000000..b8b51dc0b --- /dev/null +++ b/web/static/js/jquery-dragster/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jan Martin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/static/js/jquery-dragster/README.md b/web/static/js/jquery-dragster/README.md new file mode 100644 index 000000000..1c28adaf0 --- /dev/null +++ b/web/static/js/jquery-dragster/README.md @@ -0,0 +1,17 @@ +Include [jquery.dragster.js](https://rawgithub.com/catmanjan/jquery-dragster/master/jquery.dragster.js) in page. + +Works in IE. + +```javascript +$('.element').dragster({ + enter: function (dragsterEvent, event) { + $(this).addClass('hover'); + }, + leave: function (dragsterEvent, event) { + $(this).removeClass('hover'); + }, + drop: function (dragsterEvent, event) { + $(this).removeClass('hover'); + } +}); +```
\ No newline at end of file diff --git a/web/static/js/jquery-dragster/jquery.dragster.js b/web/static/js/jquery-dragster/jquery.dragster.js new file mode 100644 index 000000000..db73fe3f0 --- /dev/null +++ b/web/static/js/jquery-dragster/jquery.dragster.js @@ -0,0 +1,85 @@ +// 1.0.3 +/* +The MIT License (MIT) + +Copyright (c) 2015 Jan Martin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +(function ($) { + + $.fn.dragster = function (options) { + var settings = $.extend({ + enter: $.noop, + leave: $.noop, + over: $.noop, + drop: $.noop + }, options); + + return this.each(function () { + var first = false, + second = false, + $this = $(this); + + $this.on({ + dragenter: function (event) { + if (first) { + second = true; + return; + } else { + first = true; + $this.trigger('dragster:enter', event); + } + event.preventDefault(); + }, + dragleave: function (event) { + if (second) { + second = false; + } else if (first) { + first = false; + } + if (!first && !second) { + $this.trigger('dragster:leave', event); + } + event.preventDefault(); + }, + dragover: function (event) { + $this.trigger('dragster:over', event); + event.preventDefault(); + }, + drop: function (event) { + if (second) { + second = false; + } else if (first) { + first = false; + } + if (!first && !second) { + $this.trigger('dragster:drop', event); + } + event.preventDefault(); + }, + 'dragster:enter': settings.enter, + 'dragster:leave': settings.leave, + 'dragster:over': settings.over, + 'dragster:drop': settings.drop + }); + }); + }; + +}(jQuery)); diff --git a/web/templates/channel.html b/web/templates/channel.html index da6fed97d..9bfd1fa35 100644 --- a/web/templates/channel.html +++ b/web/templates/channel.html @@ -14,6 +14,7 @@ <div id="navbar"></div> </div> <div class="row main"> + <div id="file_upload_overlay"></div> <div id="app-content" class="app__content"> <div id="channel-header"></div> <div id="post-list"></div> diff --git a/web/templates/head.html b/web/templates/head.html index 7a7d4fe8e..dd5e9f46e 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -32,6 +32,8 @@ <script src="/static/js/perfect-scrollbar-0.6.3.jquery.js"></script> + <script src="/static/js/jquery-dragster/jquery.dragster.js"></script> + <script type="text/javascript" src="https://www.google.com/jsapi?autoload={'modules':[{'name':'visualization','version':'1','packages':['annotationchart']}]}"></script> <script type="text/javascript" src="https://cloudfront.loggly.com/js/loggly.tracker.js" async></script> |