Merge branch 'andrewminer-item-edit'

This commit is contained in:
Andrew Miner
2015-05-11 09:43:25 -07:00
16 changed files with 564 additions and 57 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 %>"
+29 -19
View File
@@ -49,14 +49,17 @@ module.exports = class BaseController extends backbone.View
if not ($el.hasClass('hiding') or $el.hasClass('hidden'))
logger.verbose => "#{this} is hiding \"#{$el.selector}\""
@_onAnimationComplete $el, Duration.normal, =>
$el.addClass 'hidden'
$el.removeClass 'hiding'
logger.verbose => "#{this} is finished hiding #{$el.selector}"
@trigger Event.animate.hide.finish, this, $el
$el.addClass 'hiding'
@trigger Event.animate.hide.start, this, $el
@_onAnimationComplete $el, Duration.normal, => @hide $el
return
if $el.hasClass('hiding') and not $el.hasClass('hidden')
$el.removeClass 'hiding'
$el.addClass 'hidden'
logger.verbose => "#{this} is finished hiding #{$el.selector}"
@trigger Event.animate.hide.finish, this, $el
refresh: ->
logger.verbose => "#{this} refreshing"
@@ -86,20 +89,27 @@ module.exports = class BaseController extends backbone.View
show: ($el)->
$el ?= @$el
$el.addClass 'hideable'
if $el.hasClass('hidden') or $el.hasClass('hiding')
if not $el.hasClass 'hideable'
$el.addClass 'hideable'
_.defer => @show $el
return
if $el.hasClass 'hidden'
logger.verbose => "#{this} is showing \"#{$el.selector}\""
@_onAnimationComplete $el, Duration.normal, =>
logger.verbose => "#{this} is finished showing #{$el.selector}"
@trigger Event.animate.show.finish, this, $el
$el.addClass 'hiding'
$el.removeClass 'hidden'
_.defer =>
$el.removeClass 'hiding'
@trigger Event.animate.show.start, this, $el
$el.addClass 'hiding'
@trigger Event.animate.show.start, this, $el
_.defer => @show $el
return
if $el.hasClass 'hiding'
$el.removeClass 'hiding'
@_onAnimationComplete $el, Duration.normal, => @show $el
return
logger.verbose => "#{this} is finished showing #{$el.selector}"
@trigger Event.animate.show.finish, this, $el
unrender: ->
@undelegateEvents()
@@ -153,9 +163,9 @@ module.exports = class BaseController extends backbone.View
@tryRefresh()
Object.defineProperties @prototype,
model: {get:@prototype.getModel, set:@prototype.setModel}
model: {get:@prototype.getModel, set:@prototype.setModel}
rendered: {get:@prototype.isRendered }
user: {get:@prototype.user, set:@prototype.setUser}
user: {get:@prototype.getUser, set:@prototype.setUser}
# Backbone.View Overrides ######################################################################
@@ -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()
@@ -59,11 +65,16 @@ module.exports = class ItemPageController extends PageController
@adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper'
options = imageLoader:@imageLoader, modPack:@modPack, show:false
@_descriptionController = @addChild MarkdownSectionController, '.section.description', options
@_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')
@@ -111,17 +122,54 @@ module.exports = class ItemPageController extends PageController
super
setUser: (user)->
super user
@_descriptionController.user = user if @_descriptionController?
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click a.craftingPlan': 'routeLinkClick'
'click .byline a': 'routeLinkClick'
'click .markdown a': 'routeLinkClick'
'click button': 'craftingPlanButtonClicked'
'click a.craftingPlan': 'routeLinkClick'
'click .byline a': 'routeLinkClick'
'click .markdown a': 'routeLinkClick'
'click button.craftingPlan': 'craftingPlanButtonClicked'
# 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 path:GitHub.file.itemDescription modSlug:@_itemSlug.mod, itemSlug:@_itemSlug.item
.then (response)=>
@_editingFile = response.json.data
if @_editingFile.content.length > 0
@model.item.parse @_editingFile.content
else
@model.item.description = ''
@_descriptionController.model = @model.item.description
_endEditingDescription: ->
oldDescription = @model.item.description
@model.item.description = @_descriptionController.model
args =
content: @model.item.unparse()
message: "User-submitted text for #{@model.item.name}"
path: GitHub.file.itemDescription modSlug:@_itemSlug.mod, itemSlug:@_itemSlug.item
sha: @_editingFile.sha
@client.updateFile args
.catch (e)=>
@model.item.description = oldDescription
throw e
_refreshByline: ->
mod = @model.item?.modVersion?.mod
if mod?.name?.length > 0
@@ -136,6 +184,8 @@ module.exports = class ItemPageController extends PageController
if @model.item?.description?.length > 0
@_descriptionController.model = @model.item.description
@show @$descriptionSection
else if @user?
@show @$descriptionSection
else
@hide @$descriptionSection
@@ -77,7 +77,7 @@ module.exports = class LoginPageController extends PageController
attributes = response.json.data.user
if attributes?
router.user = new User attributes
router.navigate Url.root(), trigger:true
router.resumeAfterLogin()
.catch (error)->
logger.error "Failed to get access token: #{error}"
@_accessTokenLookupFailed = true
@@ -120,9 +120,9 @@ module.exports = class LoginPageController extends PageController
@client.completeGitHubLogin code:code
.then (response)=>
router.user = new User response.json.user
.catch (error)->
.catch (error)=>
@_accessTokenLookupFailed = true
@refresh
@refresh()
.done()
_redirectToGitHub: ->
@@ -8,48 +8,153 @@ All rights reserved.
BaseController = require './base_controller'
convertMarkdown = require 'marked'
_ = require 'underscore'
{Event} = require '../constants'
{Url} = require '../constants'
w = require 'when'
########################################################################################################################
module.exports = class MarkdownSectionController extends BaseController
@State = State =
appologizing: 'applogizing'
confirming: 'confirming'
creating: 'creating'
editing: 'editing'
previewing: 'previewing'
viewing: 'viewing'
waiting: 'waiting'
constructor: (options={})->
if not options.modPack? then throw new Error 'options.modPack is required'
options.imageBase ?= ''
options.model ?= ''
options.title ?= 'Description'
options.model ?= null
options.templateName = 'markdown_section'
@_state = State.waiting
super options
@imageBase = options.imageBase
@modPack = options.modPack
@title = options.title
@on Event.animate.hide.finish, => @_updateStateVisibility()
@on Event.animate.show.finish, => @_updateStateVisibility()
@confirmactionMessage = options.confirmationMessage
@confirmDuration = options.confirmationDuration ?= 5000
@imageBase = options.imageBase ?= ''
@modPack = options.modPack
@title = options.title ?= 'Description'
# @_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()
@model = @_originalModel
@_resetToDefaultState()
onEditClicked: (event)->
event.preventDefault()
return unless @editable
@_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 => @_resetToDefaultState()
onPreviewClicked: (event)->
event.preventDefault()
@state = State.previewing
onReturnClicked: (event)->
event.preventDefault()
@state = State.editing
onSaveClicked: (event)->
event.preventDefault()
@state = State.waiting
@_endEditing()
.then =>
@state = State.confirming
.catch (e)=>
logger.error "failed to end editing: #{e}"
@state = State.appologizing
.delay @confirmDuration
.then =>
@state = State.viewing
onTextChanged: (event)->
event.preventDefault()
@model = @$textarea.val()
# Property Methods #############################################################################
isEditable: ->
return _.isFunction(@_beginEditing) and _.isFunction(@_endEditing)
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}
state: {get:@prototype.getState, set:@prototype.setState}
# BaseController Overrides #####################################################################
onDidRender: ->
@$title = @$('h2')
@$errorText = @$('.error p')
@$markdownPanel = @$('.markdown')
@$sizer = @$('.sizer')
@$textarea = @$('textarea')
@$title = @$('h2')
@_resetToDefaultState()
super
refresh: ->
@$title.html @title
text = @model
text = @_convertWikiLinks text
text = @_convertImageLinks text
@$markdownPanel.html convertMarkdown text
@_updateSizer()
@_updatePreview()
super
onWillChangeModel: (oldModel, newModel)->
result = super oldModel, newModel
@$textarea.val newModel
return result
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click .markdown a': 'routeLinkClick'
'click .markdown a': 'routeLinkClick'
'click button.cancel': 'onCancelClicked'
'click button.edit': 'onEditClicked'
'click button.preview': 'onPreviewClicked'
'click button.return': 'onReturnClicked'
'click button.save': 'onSaveClicked'
'input textarea': 'onTextChanged'
# Private Methods ##############################################################################
@@ -66,3 +171,81 @@ module.exports = class MarkdownSectionController extends BaseController
result = "[#{name}](#{display.itemUrl})"
return result
_updateSizer: ->
text = @model
if @model?
text = text.replace /\n/g, '<br>'
@$sizer.html text
_updatePreview: ->
text = @model
if @model?
text = @_convertWikiLinks text
text = @_convertImageLinks text
text = convertMarkdown text
@$markdownPanel.html text
_resetToDefaultState: ->
if @model?
@state = State.viewing
else
if @editable?
@state = State.creating
else
@state = State.waiting
_updateStateVisibility: ->
return if @_lastUpdatedState is @state
@_lastUpdatedState = @state
elements =
appologizingPanel: @$('.appologizing')
buttonPanel: @$('.buttons')
cancelButton: @$('button.cancel')
confirmingPanel: @$('.confirming')
creatingPanel: @$('.creating')
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.appologizing
visible = appologizingPanel:true
else if @state is State.confirming
visible = confirmingPanel:true
else if @state is State.creating
visible = buttonPanel:true, creatingPanel:true, editButton: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.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
@$errorText.html errorText
+12 -1
View File
@@ -36,9 +36,10 @@ backbone = require 'backbone'
module.exports = class CraftingGuideRouter extends backbone.Router
constructor: (options={})->
@_lastReported = null
@_page = null
@_pageControllers = {}
@_lastReported = null
@_postLoginUrl = Url.root
@_user = null
super options
@@ -80,6 +81,16 @@ module.exports = class CraftingGuideRouter extends backbone.Router
logger.info "User is not logged in"
.done()
login: ->
@storage.store 'post-login-url', window.location.pathname
@navigate Url.login(), trigger:true
resumeAfterLogin: ->
url = @storage.load 'post-login-url'
url ?= Url.root()
@navigate url, trigger:true
@storage.store 'post-login-url', null
# Property Methods #############################################################################
getUser: ->
+5
View File
@@ -45,6 +45,11 @@ module.exports = class Item extends BaseModel
return if this.name < that.name then -1 else +1
return 0
unparse: ->
ItemParser = require './item_parser' # to avoid require cycles
@_parser ?= new ItemParser model:this
return @_parser.unparse()
# Property Methods #############################################################################
getIsCraftable: ->
@@ -17,17 +17,23 @@ module.exports = class ItemParserV1 extends CommandParserVersionBase
@_buildItem rawData, model
_unparseModel: (builder, model)->
builder.line 'schema: ', 1
builder.line()
@_unparseItem builder, model
# Command Methods ##############################################################################
_command_description: (textParts...)->
@_rawData.description ?= ''
if not @_rawData.description?
@_rawData.description = ''
else
@_rawData.description += '\n'
@_rawData.description += textParts.join ', '
_command_officialUrl: (officialUrl)->
if @_rawData.officialUrl? then throw new Error 'duplicate declaration of "officialUrl"'
if officialUrl.length is 0 then throw new Error 'officialUrl cannot be empty'
if not officialUrl? or (officialUrl.length is 0) then throw new Error 'officialUrl cannot be empty'
@_rawData.officialUrl = officialUrl
_command_video: (youTubeId, nameParts...)->
@@ -41,6 +47,26 @@ module.exports = class ItemParserV1 extends CommandParserVersionBase
# Object Building Methods ######################################################################
_buildItem: (rawData, model)->
model.description = rawData.description if rawData.description
model.officialUrl = rawData.officialUrl if rawData.officialUrl
model.videos = rawData.videos if rawData.videos
model.description = rawData.description if rawData.description?
model.officialUrl = rawData.officialUrl if rawData.officialUrl?
model.videos = rawData.videos if rawData.videos?
# Un-parsing Methods ###########################################################################
_unparseItem: (builder, model)->
if model.officialUrl?
builder.line 'officialUrl: ', model.officialUrl
builder.line()
if model.description?
if model.description.indexOf('\n') isnt -1
builder.line 'description: <<-END'
builder.line model.description
builder.line 'END'
else
builder.line 'description: ', model.description
builder.line()
for video in model.videos
builder.line 'video: ', video.youTubeId, ', ', video.name
builder.line()