diff --git a/client/css/post-upload.styl b/client/css/post-upload.styl index 30f6e22c..7bf9fbe7 100644 --- a/client/css/post-upload.styl +++ b/client/css/post-upload.styl @@ -1,5 +1,7 @@ @import colors +$cancel-button-color = tomato + #post-upload form width: 100% @@ -19,6 +21,17 @@ input[type=submit] margin-top: 1em + &[disabled] + display: none + + .cancel + margin-top: 1em + background: $cancel-button-color + border-color: $cancel-button-color + &[disabled] + display: none + &:focus + border: 2px solid $text-color .messages margin-top: 1em diff --git a/client/html/post_upload.tpl b/client/html/post_upload.tpl index faf5f86e..1dcfba40 100644 --- a/client/html/post_upload.tpl +++ b/client/html/post_upload.tpl @@ -3,6 +3,7 @@
+ diff --git a/client/js/api.js b/client/js/api.js index 535b8809..fe0f4ed8 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -64,11 +64,13 @@ class Api extends events.EventTarget { _process(url, requestFactory, data, files, options) { options = options || {}; const [fullUrl, query] = this._getFullUrl(url); - return new Promise((resolve, reject) => { - if (!options.noProgress) { - nprogress.start(); - } + + let abortFunction = null; + + let promise = new Promise((resolve, reject) => { let req = requestFactory(fullUrl); + + req.set('Accept', 'application/json'); if (query) { req.query(query); } @@ -94,18 +96,35 @@ class Api extends events.EventTarget { title: 'Authentication error', description: 'Malformed credentials'}); } - req.set('Accept', 'application/json') - .end((error, response) => { - nprogress.done(); - if (error) { - reject(response && response.body ? response.body : { - title: 'Networking error', - description: error.message}); - } else { - resolve(response.body); - } - }); + + if (!options.noProgress) { + nprogress.start(); + } + + abortFunction = () => { + req.abort(); // does *NOT* call the callback passed in .end() + nprogress.done(); + reject({ + title: 'Cancelled', + description: + 'The request was aborted due to user cancel.'}); + }; + + req.end((error, response) => { + nprogress.done(); + if (error) { + reject(response && response.body ? response.body : { + title: 'Networking error', + description: error.message}); + } else { + resolve(response.body); + } + }); }); + + promise.abort = () => abortFunction(); + + return promise; } hasPrivilege(lookup) { diff --git a/client/js/controllers/post_upload_controller.js b/client/js/controllers/post_upload_controller.js index c9da651d..4defd5ee 100644 --- a/client/js/controllers/post_upload_controller.js +++ b/client/js/controllers/post_upload_controller.js @@ -10,6 +10,8 @@ const EmptyView = require('../views/empty_view.js'); class PostUploadController { constructor() { + this._lastPromise = null; + if (!api.hasPrivilege('posts:create')) { this._view = new EmptyView(); this._view.showError('You don\'t have privileges to upload posts.'); @@ -23,6 +25,7 @@ class PostUploadController { }); this._view.addEventListener('change', e => this._evtChange(e)); this._view.addEventListener('submit', e => this._evtSubmit(e)); + this._view.addEventListener('cancel', e => this._evtCancel(e)); } _evtChange(e) { @@ -34,6 +37,12 @@ class PostUploadController { this._view.clearMessages(); } + _evtCancel(e) { + if (this._lastPromise) { + this._lastPromise.abort(); + } + } + _evtSubmit(e) { this._view.disableForm(); this._view.clearMessages(); @@ -48,7 +57,9 @@ class PostUploadController { } else { post.newContent = uploadable.file; } - return post.save(uploadable.anonymous) + let modelPromise = post.save(uploadable.anonymous); + this._lastPromise = modelPromise; + return modelPromise .then(() => { this._view.removeUploadable(uploadable); return Promise.resolve(); diff --git a/client/js/models/post.js b/client/js/models/post.js index 7e321438..f5f8f00e 100644 --- a/client/js/models/post.js +++ b/client/js/models/post.js @@ -135,11 +135,11 @@ class Post extends events.EventTarget { files.thumbnail = this._newThumbnail; } - let promise = this._id ? + let apiPromise = this._id ? api.put('/post/' + this._id, detail, files) : api.post('/posts', detail, files); - return promise.then(response => { + let returnedPromise = apiPromise.then(response => { this._updateFromResponse(response); this.dispatchEvent( new CustomEvent('change', {detail: {post: this}})); @@ -159,6 +159,12 @@ class Post extends events.EventTarget { } return Promise.reject(response.description); }); + + returnedPromise.abort = () => { + apiPromise.abort(); + }; + + return returnedPromise; } feature() { diff --git a/client/js/views/post_upload_view.js b/client/js/views/post_upload_view.js index c40a626f..98b80215 100644 --- a/client/js/views/post_upload_view.js +++ b/client/js/views/post_upload_view.js @@ -133,10 +133,13 @@ class PostUploadView extends events.EventTarget { super(); this._ctx = ctx; this._hostNode = document.getElementById('content-holder'); - this._enabled = true; + views.replaceContent(this._hostNode, template()); views.syncScrollPosition(); + this._enabled = true; + this._cancelButtonNode.disabled = true; + this._uploadables = new Map(); this._contentFileDropper = new FileDropperControl( this._contentInputNode, @@ -150,18 +153,22 @@ class PostUploadView extends events.EventTarget { this._contentFileDropper.addEventListener( 'urladd', e => this._evtUrlsAdded(e)); + this._cancelButtonNode.addEventListener( + 'click', e => this._evtCancelButtonClick(e)); this._formNode.addEventListener('submit', e => this._evtFormSubmit(e)); this._formNode.classList.add('inactive'); } enableForm() { - this._enabled = true; views.enableForm(this._formNode); + this._cancelButtonNode.disabled = true; + this._enabled = true; } disableForm() { - this._enabled = false; views.disableForm(this._formNode); + this._cancelButtonNode.disabled = false; + this._enabled = false; } clearMessages() { @@ -226,6 +233,11 @@ class PostUploadView extends events.EventTarget { this.addUploadables(e.detail.urls.map(url => new Url(url))); } + _evtCancelButtonClick(e) { + e.preventDefault(); + this._emit('cancel'); + } + _evtFormSubmit(e) { e.preventDefault(); this._emit('submit'); @@ -342,6 +354,10 @@ class PostUploadView extends events.EventTarget { return this._hostNode.querySelector('form [type=submit]'); } + get _cancelButtonNode() { + return this._hostNode.querySelector('form .cancel'); + } + get _contentInputNode() { return this._formNode.querySelector('.dropper-container'); }