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.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 %>"
@@ -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
@@ -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, '<br>'
text = @model
if @model?
text = text.replace /\n/g, '<br>'
@$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
+8 -2
View File
@@ -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.
+50 -21
View File
@@ -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%));
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB