diff --git a/src/coffee/constants.coffee b/src/coffee/constants.coffee index c3a28ea18..7dcf4a3bb 100644 --- a/src/coffee/constants.coffee +++ b/src/coffee/constants.coffee @@ -66,7 +66,7 @@ Event.route = 'route' Event.sort = 'sort' Event.sync = 'sync' # model, response Event.transitionEnd = (-> - return unless document? + return 'transitionend' unless document? transitions = 'WebkitTransition': 'webkitTransitionEnd' 'MozTransition': 'transitionend' @@ -88,6 +88,10 @@ Key.Escape = 27 Key.UpArrow = 38 Key.DownArrow = 40 +exports.GitHub = GitHub = {} +GitHub.file = {} +GitHub.file.itemDescription = _.template "/data/<%= modSlug %>/items/<%= itemSlug %>/item.cg" + exports.Login = Login = {} Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" + "?client_id=<%= clientId %>&scope=public_repo&state=<%= state %>" diff --git a/src/coffee/controllers/item_page_controller.coffee b/src/coffee/controllers/item_page_controller.coffee index e8cac6e6f..2426c5af8 100644 --- a/src/coffee/controllers/item_page_controller.coffee +++ b/src/coffee/controllers/item_page_controller.coffee @@ -16,8 +16,10 @@ MarkdownSectionController = require './markdown_section_controller' PageController = require './page_controller' VideoController = require './video_controller' _ = require 'underscore' +w = require 'when' {Duration} = require '../constants' {Event} = require '../constants' +{GitHub} = require '../constants' {Text} = require '../constants' {Url} = require '../constants' @@ -26,6 +28,7 @@ _ = require 'underscore' module.exports = class ItemPageController extends PageController constructor: (options={})-> + if not options.client? then throw new Error 'options.client is required' if not options.itemSlug? then throw new Error 'options.itemSlug is required' if not options.imageLoader? then throw new Error 'options.imageLoader is required' if not options.modPack? then throw new Error 'options.modPack is required' @@ -35,9 +38,12 @@ module.exports = class ItemPageController extends PageController super options + @client = options.client @imageLoader = options.imageLoader @modPack = options.modPack - @_itemSlug = options.itemSlug + + @_editingFile = null + @_itemSlug = options.itemSlug @modPack.on Event.change, => @tryRefresh() @@ -58,12 +64,17 @@ module.exports = class ItemPageController extends PageController onDidRender: -> @adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper' - options = editable:true, imageLoader:@imageLoader, modPack:@modPack, show:false - @_descriptionController = @addChild MarkdownSectionController, '.section.description', options + options = imageLoader:@imageLoader, modPack:@modPack, show:false @_similarItemsController = @addChild ItemGroupController, '.view__item_group.similar', options @_usedAsToolToMakeController = @addChild ItemGroupController, '.view__item_group.usedAsToolToMake', options @_usedToMakeController = @addChild ItemGroupController, '.view__item_group.usedToMake', options + @_descriptionController = @addChild MarkdownSectionController, '.section.description', + editable: true + modPack: @modPack + beginEditing: => @_beginEditingDescription() + endEditing: => @_endEditingDescription() + @$byline = @$('.byline') @$bylineLink = @$('.byline a') @$descriptionSection = @$('.description') @@ -126,6 +137,23 @@ module.exports = class ItemPageController extends PageController # Private Methods ############################################################################## + _beginEditingDescription: -> + if not global.router.user? + global.router.login() + return w.reject new Error 'must be logged in to edit' + + if not @model.item? + return w.reject new Error 'must have an item' + + @client.fetchFile file:GitHub.file.itemDescription modSlug:@_itemSlug.mod, itemSlug:@_itemSlug.item + .then (fileRecord)=> + @_editingFile = fileRecord + @model.item.parse fileRecord.content + @_descriptionController.model = @model.item.description + + _endEditingDescription: -> + return w(true).delay(1000) + _refreshByline: -> mod = @model.item?.modVersion?.mod if mod?.name?.length > 0 diff --git a/src/coffee/controllers/markdown_section_controller.coffee b/src/coffee/controllers/markdown_section_controller.coffee index b2bc48aac..7dd2a59d6 100644 --- a/src/coffee/controllers/markdown_section_controller.coffee +++ b/src/coffee/controllers/markdown_section_controller.coffee @@ -10,94 +10,118 @@ convertMarkdown = require 'marked' _ = require 'underscore' {Event} = require '../constants' {Url} = require '../constants' +w = require 'when' ######################################################################################################################## module.exports = class MarkdownSectionController extends BaseController @State = State = - viewing: 'viewing' - editing: 'editing' - previewing: 'previewing' - error: 'error' + appologizing: 'applogizing' + confirming: 'confirming' + editing: 'editing' + previewing: 'previewing' + viewing: 'viewing' + waiting: 'waiting' constructor: (options={})-> + @_state = State.waiting + if not options.modPack? then throw new Error 'options.modPack is required' - options.editable ?= false - options.imageBase ?= '' - options.model ?= '' - options.title ?= 'Description' - options.templateName = 'markdown_section' + options.model ?= null + options.templateName = 'markdown_section' super options - @imageBase = options.imageBase - @modPack = options.modPack - @title = options.title + @confirmactionMessage = options.confirmationMessage + @confirmDuration = options.confirmationDuration ?= 5000 + @imageBase = options.imageBase ?= '' + @modPack = options.modPack + @title = options.title ?= 'Description' - @_editable = options.editable - @_state = State.viewing + # @_beginEditing must be a function returning a promise which resolves when all actions necessary prior to an + # editing session have been completed (e.g., loading the latest content from a remote server). The promise must + # reject if an error occured which should prevent editing. + @_beginEditing = options.beginEditing + + # @_endEditing must be a function returning a promise which resolves when the model of this controller has been + # saved however is appropriate. The promise must reject if saving failed for some reason. + @_endEditing = options.endEditing # Event Methods ################################################################################ onCancelClicked: (event)-> event.preventDefault() - @_state = State.viewing - @_updateStateVisibility() + @model = @_originalModel + @state = State.viewing onEditClicked: (event)-> event.preventDefault() + return unless @editable - if global.router.user? - @_state = State.editing - @_updateStateVisibility() - else - global.router.login() + @_originalModel = "#{@model}" + @state = State.waiting + @_beginEditing() + .then => + @state = State.editing + .catch (e)=> + logger.warning "cannot begin editing: #{e.stack}" + @state = State.appologizing + w(true).delay(@confirmDuration).then => @state = State.viewing onPreviewClicked: (event)-> event.preventDefault() - @_state = State.preview - @_updateStateVisibility() + @state = State.previewing onReturnClicked: (event)-> event.preventDefault() - @_state = State.editing - @_updateStateVisibility() + @state = State.editing onSaveClicked: (event)-> - # TODO: implement logic to actually save changes - @onCancelClicked event + event.preventDefault() + + nextState = State.viewing + @state = State.waiting + @_endEditing() + .then => + @state = State.confirming + .catch (e)=> + logger.error "failed to end editing: #{e}" + @state = State.appologizing + nextState = State.editing + .delay @confirmDuration + .then => + @state = nextState onTextChanged: (event)-> event.preventDefault() - @_updatePreview() - @_updateSizer() + @model = @$textarea.val() # Property Methods ############################################################################# isEditable: -> - return @_editable + return _.isFunction(@_beginEditing) and _.isFunction(@_endEditing) - setEditable: (editable)-> - return if @_editable is editable - @_editable = editable - @tryRefresh() + getState: -> + return @_state + + setState: (newState)-> + oldState = @_state + return if oldState is newState + @_state = newState + + logger.verbose => "MarkdownSectionController.#{@cid} changed state from #{oldState} to #{newState}" + @trigger Event.change + ':state', this, oldState, newState + @_updateStateVisibility() Object.defineProperties @prototype, - editable: {get:@prototype.isEditable, set:@prototype.setEditable} + editable: {get:@prototype.isEditable} + state: {get:@prototype.getState, set:@prototype.setState} # BaseController Overrides ##################################################################### onDidRender: -> - @$buttonPanel = @$('.buttons') - @$cancelButton = @$('button.cancel') - @$editButton = @$('button.edit') - @$editorPanel = @$('.editor') - @$errorPanel = @$('.error') @$errorText = @$('.error p') @$markdownPanel = @$('.markdown') - @$previewButton = @$('button.preview') - @$returnButton = @$('button.return') - @$saveButton = @$('button.save') @$sizer = @$('.sizer') @$textarea = @$('textarea') @$title = @$('h2') @@ -107,17 +131,23 @@ module.exports = class MarkdownSectionController extends BaseController refresh: -> @$title.html @title - @$textarea.val @model - - if @editable - @show @$buttonPanel - else - @hide @$buttonPanel - @_updateSizer() @_updatePreview() + @_updateStateVisibility() super + onWillChangeModel: (oldModel, newModel)-> + result = super oldModel, newModel + + if @state is State.waiting and newModel? + @state = State.viewing + else if not newModel? + @state = State.waiting + + @$textarea.val newModel + + return result + # Backbone.View Overrides ###################################################################### events: -> @@ -147,35 +177,66 @@ module.exports = class MarkdownSectionController extends BaseController return result _updateSizer: -> - text = @$textarea.val() - text = text.replace /\n/g, '
' + text = @model + if @model? + text = text.replace /\n/g, '
' + @$sizer.html text _updatePreview: -> - text = @$textarea.val() - text = @_convertWikiLinks text - text = @_convertImageLinks text + text = @model + if @model? + text = @_convertWikiLinks text + text = @_convertImageLinks text + text = convertMarkdown text - @$markdownPanel.html convertMarkdown text + @$markdownPanel.html text _updateStateVisibility: -> - toHide = [] - toShow = [] + return if @_lastUpdatedState is @state + @_lastUpdatedState = @state + + elements = + appologizingPanel: @$('.appologizing') + buttonPanel: @$('.buttons') + cancelButton: @$('button.cancel') + confirmingPanel: @$('.confirming') + editButton: @$('button.edit') + editorPanel: @$('.editor') + errorPanel: @$('.error') + markdownPanel: @$markdownPanel + previewButton: @$('button.preview') + returnButton: @$('button.return') + saveButton: @$('button.save') + waitingPanel: @$('.waiting') + + visible = {} errorText = '' - if @_state is State.viewing - toHide = [@$cancelButton, @$editorPanel, @$errorPanel, @$previewButton, @$returnButton, @$saveButton] - toShow = [@$editButton, @$markdownPanel] - else if @_state is State.editing - toHide = [@$editButton, @$errorPanel, @$markdownPanel, @$previewButton, @$returnButton] - toShow = [@$cancelButton, @$editorPanel, @$previewButton, @$saveButton] - else if @_state is State.preview - toHide = [@$cancelButton, @$editButton, @$editorPanel, @$previewButton, @$saveButton] - toShow = [@$errorPanel, @$markdownPanel, @$returnButton] + if @state is State.appologizing + visible = appologizingPanel:true + else if @state is State.confirming + visible = confirmingPanel:true + else if @state is State.editing + visible = buttonPanel:true, cancelButton:true, editorPanel:true, previewButton:true, saveButton:true + else if @state is State.previewing + visible = buttonPanel:true, errorPanel:true, markdownPanel:true, returnButton:true errorText = "remember: your changes aren't saved yet!" - # else if @_state is State.error - # TODO: Implement change to error state + else if @state is State.viewing + visible = markdownPanel:true + if @editable then _.extend visible, {buttonPanel:true, editButton:true} + else # assume any unknown state is the same as "waiting" + visible = waitingPanel:true + + toHide = ($el for name, $el of elements when not visible[name]) + toShow = ($el for name, $el of elements when visible[name]) + + if toHide.length > 0 + @hide $el for $el in toHide + if toShow.length > 0 + @once Event.animate.hide.finish, => + @show $el for $el in toShow + else if toShow.length > 0 + @show $el for $el in toShow - @hide $el for $el in toHide - @once Event.animate.hide.finish, => @show $el for $el in toShow @$errorText.html errorText diff --git a/src/jade/templates/markdown_section.jade b/src/jade/templates/markdown_section.jade index 82eed7a1d..44e83fb57 100644 --- a/src/jade/templates/markdown_section.jade +++ b/src/jade/templates/markdown_section.jade @@ -8,11 +8,11 @@ .view__markdown_section.section h2 .panel - .markdown.hideable + .waiting.hideable: img(src='/images/wait.gif') + .markdown.hideable.hidden .editor.hideable.hidden textarea .sizer - .images.hideable.hidden .buttons.hideable.hidden .error.hideable.hidden: p button.return.hideable.hidden return @@ -20,3 +20,9 @@ button.preview.hideable.hidden preview button.save.hideable.hidden save button.edit.hideable edit + .confirming.hideable.hidden + p Thanks for your help! + p Your changes will appear on the site in a few minutes. + .appologizing.hideable.hidden + p Sorry, but something went wrong. + p Please try again after a few moments. diff --git a/src/scss/templates/markdown_section.scss b/src/scss/templates/markdown_section.scss index 7b3459d23..33ad6d612 100644 --- a/src/scss/templates/markdown_section.scss +++ b/src/scss/templates/markdown_section.scss @@ -7,6 +7,51 @@ All rights reserved. .view__markdown_section { + .appologizing, .confirming { + width: 100%; + + p { + width: 100%; + + margin: 1em auto; + text-align: center; + + &:first-child { + font-size: $font-size-x-large; + margin-top: 2em; + } + + &:last-child { + margin-bottom: 4em; + } + } + } + + .buttons { + position: relative; width: 100%; + + text-align: right; + + button { + margin: 0.66em 0 0 0.66em; + min-width: 6.66em; + padding: 0.25em 0.5em; + } + + .error { + + height: 100%; + + background: none; + display: inline-block; + + p { + color: $color-error-new; + font-weight: bold; + } + } + } + .editor { width: 100%; @@ -40,28 +85,12 @@ All rights reserved. } } - .buttons { - position: relative; width: 100%; + .waiting { + position: relative; min-height: 20em; width: 100%; - text-align: right; - - button { - margin: 0.66em 0 0 0.66em; - min-width: 6.66em; - padding: 0.25em 0.5em; - } - - .error { - - height: 100%; - - background: none; - display: inline-block; - - p { - color: $color-error-new; - font-weight: bold; - } + img { + position: absolute; top: 50%; left: 50%; + @include transform(translate(-50%, -50%)); } } } diff --git a/static/images/wait.gif b/static/images/wait.gif new file mode 100644 index 000000000..27f20ed91 Binary files /dev/null and b/static/images/wait.gif differ