Update MarkdownSectionController with support for editing

This allows the MarkdownSectionController and ItemPageController to
work together to begin an editing session for an item description.
This commit is contained in:
Andrew Miner
2015-05-07 11:19:36 -07:00
parent 54194d3f0a
commit 8b94f432b0
6 changed files with 226 additions and 98 deletions
+5 -1
View File
@@ -66,7 +66,7 @@ Event.route = 'route'
Event.sort = 'sort' Event.sort = 'sort'
Event.sync = 'sync' # model, response Event.sync = 'sync' # model, response
Event.transitionEnd = (-> Event.transitionEnd = (->
return unless document? return 'transitionend' unless document?
transitions = transitions =
'WebkitTransition': 'webkitTransitionEnd' 'WebkitTransition': 'webkitTransitionEnd'
'MozTransition': 'transitionend' 'MozTransition': 'transitionend'
@@ -88,6 +88,10 @@ Key.Escape = 27
Key.UpArrow = 38 Key.UpArrow = 38
Key.DownArrow = 40 Key.DownArrow = 40
exports.GitHub = GitHub = {}
GitHub.file = {}
GitHub.file.itemDescription = _.template "/data/<%= modSlug %>/items/<%= itemSlug %>/item.cg"
exports.Login = Login = {} exports.Login = Login = {}
Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" + Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" +
"?client_id=<%= clientId %>&scope=public_repo&state=<%= state %>" "?client_id=<%= clientId %>&scope=public_repo&state=<%= state %>"
@@ -16,8 +16,10 @@ MarkdownSectionController = require './markdown_section_controller'
PageController = require './page_controller' PageController = require './page_controller'
VideoController = require './video_controller' VideoController = require './video_controller'
_ = require 'underscore' _ = require 'underscore'
w = require 'when'
{Duration} = require '../constants' {Duration} = require '../constants'
{Event} = require '../constants' {Event} = require '../constants'
{GitHub} = require '../constants'
{Text} = require '../constants' {Text} = require '../constants'
{Url} = require '../constants' {Url} = require '../constants'
@@ -26,6 +28,7 @@ _ = require 'underscore'
module.exports = class ItemPageController extends PageController module.exports = class ItemPageController extends PageController
constructor: (options={})-> 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.itemSlug? then throw new Error 'options.itemSlug is required'
if not options.imageLoader? then throw new Error 'options.imageLoader 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' if not options.modPack? then throw new Error 'options.modPack is required'
@@ -35,9 +38,12 @@ module.exports = class ItemPageController extends PageController
super options super options
@client = options.client
@imageLoader = options.imageLoader @imageLoader = options.imageLoader
@modPack = options.modPack @modPack = options.modPack
@_itemSlug = options.itemSlug
@_editingFile = null
@_itemSlug = options.itemSlug
@modPack.on Event.change, => @tryRefresh() @modPack.on Event.change, => @tryRefresh()
@@ -58,12 +64,17 @@ module.exports = class ItemPageController extends PageController
onDidRender: -> onDidRender: ->
@adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper' @adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper'
options = editable:true, imageLoader:@imageLoader, modPack:@modPack, show:false options = imageLoader:@imageLoader, modPack:@modPack, show:false
@_descriptionController = @addChild MarkdownSectionController, '.section.description', options
@_similarItemsController = @addChild ItemGroupController, '.view__item_group.similar', options @_similarItemsController = @addChild ItemGroupController, '.view__item_group.similar', options
@_usedAsToolToMakeController = @addChild ItemGroupController, '.view__item_group.usedAsToolToMake', options @_usedAsToolToMakeController = @addChild ItemGroupController, '.view__item_group.usedAsToolToMake', options
@_usedToMakeController = @addChild ItemGroupController, '.view__item_group.usedToMake', options @_usedToMakeController = @addChild ItemGroupController, '.view__item_group.usedToMake', options
@_descriptionController = @addChild MarkdownSectionController, '.section.description',
editable: true
modPack: @modPack
beginEditing: => @_beginEditingDescription()
endEditing: => @_endEditingDescription()
@$byline = @$('.byline') @$byline = @$('.byline')
@$bylineLink = @$('.byline a') @$bylineLink = @$('.byline a')
@$descriptionSection = @$('.description') @$descriptionSection = @$('.description')
@@ -126,6 +137,23 @@ module.exports = class ItemPageController extends PageController
# Private Methods ############################################################################## # 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: -> _refreshByline: ->
mod = @model.item?.modVersion?.mod mod = @model.item?.modVersion?.mod
if mod?.name?.length > 0 if mod?.name?.length > 0
@@ -10,94 +10,118 @@ convertMarkdown = require 'marked'
_ = require 'underscore' _ = require 'underscore'
{Event} = require '../constants' {Event} = require '../constants'
{Url} = require '../constants' {Url} = require '../constants'
w = require 'when'
######################################################################################################################## ########################################################################################################################
module.exports = class MarkdownSectionController extends BaseController module.exports = class MarkdownSectionController extends BaseController
@State = State = @State = State =
viewing: 'viewing' appologizing: 'applogizing'
editing: 'editing' confirming: 'confirming'
previewing: 'previewing' editing: 'editing'
error: 'error' previewing: 'previewing'
viewing: 'viewing'
waiting: 'waiting'
constructor: (options={})-> constructor: (options={})->
@_state = State.waiting
if not options.modPack? then throw new Error 'options.modPack is required' if not options.modPack? then throw new Error 'options.modPack is required'
options.editable ?= false options.model ?= null
options.imageBase ?= '' options.templateName = 'markdown_section'
options.model ?= ''
options.title ?= 'Description'
options.templateName = 'markdown_section'
super options super options
@imageBase = options.imageBase @confirmactionMessage = options.confirmationMessage
@modPack = options.modPack @confirmDuration = options.confirmationDuration ?= 5000
@title = options.title @imageBase = options.imageBase ?= ''
@modPack = options.modPack
@title = options.title ?= 'Description'
@_editable = options.editable # @_beginEditing must be a function returning a promise which resolves when all actions necessary prior to an
@_state = State.viewing # 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 ################################################################################ # Event Methods ################################################################################
onCancelClicked: (event)-> onCancelClicked: (event)->
event.preventDefault() event.preventDefault()
@_state = State.viewing @model = @_originalModel
@_updateStateVisibility() @state = State.viewing
onEditClicked: (event)-> onEditClicked: (event)->
event.preventDefault() event.preventDefault()
return unless @editable
if global.router.user? @_originalModel = "#{@model}"
@_state = State.editing @state = State.waiting
@_updateStateVisibility() @_beginEditing()
else .then =>
global.router.login() @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)-> onPreviewClicked: (event)->
event.preventDefault() event.preventDefault()
@_state = State.preview @state = State.previewing
@_updateStateVisibility()
onReturnClicked: (event)-> onReturnClicked: (event)->
event.preventDefault() event.preventDefault()
@_state = State.editing @state = State.editing
@_updateStateVisibility()
onSaveClicked: (event)-> onSaveClicked: (event)->
# TODO: implement logic to actually save changes event.preventDefault()
@onCancelClicked event
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)-> onTextChanged: (event)->
event.preventDefault() event.preventDefault()
@_updatePreview() @model = @$textarea.val()
@_updateSizer()
# Property Methods ############################################################################# # Property Methods #############################################################################
isEditable: -> isEditable: ->
return @_editable return _.isFunction(@_beginEditing) and _.isFunction(@_endEditing)
setEditable: (editable)-> getState: ->
return if @_editable is editable return @_state
@_editable = editable
@tryRefresh() 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, Object.defineProperties @prototype,
editable: {get:@prototype.isEditable, set:@prototype.setEditable} editable: {get:@prototype.isEditable}
state: {get:@prototype.getState, set:@prototype.setState}
# BaseController Overrides ##################################################################### # BaseController Overrides #####################################################################
onDidRender: -> onDidRender: ->
@$buttonPanel = @$('.buttons')
@$cancelButton = @$('button.cancel')
@$editButton = @$('button.edit')
@$editorPanel = @$('.editor')
@$errorPanel = @$('.error')
@$errorText = @$('.error p') @$errorText = @$('.error p')
@$markdownPanel = @$('.markdown') @$markdownPanel = @$('.markdown')
@$previewButton = @$('button.preview')
@$returnButton = @$('button.return')
@$saveButton = @$('button.save')
@$sizer = @$('.sizer') @$sizer = @$('.sizer')
@$textarea = @$('textarea') @$textarea = @$('textarea')
@$title = @$('h2') @$title = @$('h2')
@@ -107,17 +131,23 @@ module.exports = class MarkdownSectionController extends BaseController
refresh: -> refresh: ->
@$title.html @title @$title.html @title
@$textarea.val @model
if @editable
@show @$buttonPanel
else
@hide @$buttonPanel
@_updateSizer() @_updateSizer()
@_updatePreview() @_updatePreview()
@_updateStateVisibility()
super 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 ###################################################################### # Backbone.View Overrides ######################################################################
events: -> events: ->
@@ -147,35 +177,66 @@ module.exports = class MarkdownSectionController extends BaseController
return result return result
_updateSizer: -> _updateSizer: ->
text = @$textarea.val() text = @model
text = text.replace /\n/g, '<br>' if @model?
text = text.replace /\n/g, '<br>'
@$sizer.html text @$sizer.html text
_updatePreview: -> _updatePreview: ->
text = @$textarea.val() text = @model
text = @_convertWikiLinks text if @model?
text = @_convertImageLinks text text = @_convertWikiLinks text
text = @_convertImageLinks text
text = convertMarkdown text
@$markdownPanel.html convertMarkdown text @$markdownPanel.html text
_updateStateVisibility: -> _updateStateVisibility: ->
toHide = [] return if @_lastUpdatedState is @state
toShow = [] @_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 = '' errorText = ''
if @_state is State.viewing if @state is State.appologizing
toHide = [@$cancelButton, @$editorPanel, @$errorPanel, @$previewButton, @$returnButton, @$saveButton] visible = appologizingPanel:true
toShow = [@$editButton, @$markdownPanel] else if @state is State.confirming
else if @_state is State.editing visible = confirmingPanel:true
toHide = [@$editButton, @$errorPanel, @$markdownPanel, @$previewButton, @$returnButton] else if @state is State.editing
toShow = [@$cancelButton, @$editorPanel, @$previewButton, @$saveButton] visible = buttonPanel:true, cancelButton:true, editorPanel:true, previewButton:true, saveButton:true
else if @_state is State.preview else if @state is State.previewing
toHide = [@$cancelButton, @$editButton, @$editorPanel, @$previewButton, @$saveButton] visible = buttonPanel:true, errorPanel:true, markdownPanel:true, returnButton:true
toShow = [@$errorPanel, @$markdownPanel, @$returnButton]
errorText = "remember: your changes aren't saved yet!" errorText = "remember: your changes aren't saved yet!"
# else if @_state is State.error else if @state is State.viewing
# TODO: Implement change to error state 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 @$errorText.html errorText
+8 -2
View File
@@ -8,11 +8,11 @@
.view__markdown_section.section .view__markdown_section.section
h2 h2
.panel .panel
.markdown.hideable .waiting.hideable: img(src='/images/wait.gif')
.markdown.hideable.hidden
.editor.hideable.hidden .editor.hideable.hidden
textarea textarea
.sizer .sizer
.images.hideable.hidden
.buttons.hideable.hidden .buttons.hideable.hidden
.error.hideable.hidden: p .error.hideable.hidden: p
button.return.hideable.hidden return button.return.hideable.hidden return
@@ -20,3 +20,9 @@
button.preview.hideable.hidden preview button.preview.hideable.hidden preview
button.save.hideable.hidden save button.save.hideable.hidden save
button.edit.hideable edit 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.
+50 -21
View File
@@ -7,6 +7,51 @@ All rights reserved.
.view__markdown_section { .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 { .editor {
width: 100%; width: 100%;
@@ -40,28 +85,12 @@ All rights reserved.
} }
} }
.buttons { .waiting {
position: relative; width: 100%; position: relative; min-height: 20em; width: 100%;
text-align: right; img {
position: absolute; top: 50%; left: 50%;
button { @include transform(translate(-50%, -50%));
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;
}
} }
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB