From 8b94f432b06e86fff792651b679d000225ad6909 Mon Sep 17 00:00:00 2001 From: Andrew Miner Date: Thu, 7 May 2015 11:19:36 -0700 Subject: [PATCH] Update MarkdownSectionController with support for editing This allows the MarkdownSectionController and ItemPageController to work together to begin an editing session for an item description. --- src/coffee/constants.coffee | 6 +- .../controllers/item_page_controller.coffee | 34 ++- .../markdown_section_controller.coffee | 203 ++++++++++++------ src/jade/templates/markdown_section.jade | 10 +- src/scss/templates/markdown_section.scss | 71 ++++-- static/images/wait.gif | Bin 0 -> 9331 bytes 6 files changed, 226 insertions(+), 98 deletions(-) create mode 100644 static/images/wait.gif 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 0000000000000000000000000000000000000000..27f20ed91a1438cc3d4eaf5675850d6057e506ad GIT binary patch literal 9331 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 literal 0 HcmV?d00001