Merge branch 'andrewminer-item-edit'
This commit is contained in:
+8
-3
@@ -133,17 +133,18 @@ module.exports = (grunt)->
|
|||||||
tasks: ['browserify:dev', 'exorcise']
|
tasks: ['browserify:dev', 'exorcise']
|
||||||
jade:
|
jade:
|
||||||
files: ['./src/**/*.jade']
|
files: ['./src/**/*.jade']
|
||||||
tasks: ['jade:pages', 'jade:templates', 'browserify:dev', 'exorcise']
|
tasks: ['jade:pages', 'jade:templates']
|
||||||
sass:
|
sass:
|
||||||
files: ['./src/**/*.scss']
|
files: ['./src/**/*.scss']
|
||||||
tasks: ['sass', 'copy:styles']
|
tasks: ['sass', 'copy:styles']
|
||||||
|
test:
|
||||||
|
files: ['./src/**/*.coffee', './src/**/*.js', './test/**/*.coffee']
|
||||||
|
tasks: ['test']
|
||||||
|
|
||||||
grunt.registerTask 'default', 'build'
|
grunt.registerTask 'default', 'build'
|
||||||
|
|
||||||
grunt.registerTask 'build', ['rsync', 'sass:build', 'jade', 'copy', 'browserify:dev', 'exorcise']
|
grunt.registerTask 'build', ['rsync', 'sass:build', 'jade', 'copy', 'browserify:dev', 'exorcise']
|
||||||
|
|
||||||
grunt.registerTask 'clean-watch', ['clean', 'build', 'watch']
|
|
||||||
|
|
||||||
grunt.registerTask 'dist', ['rsync', 'sass:dist', 'jade', 'copy', 'browserify:prod', 'uglify']
|
grunt.registerTask 'dist', ['rsync', 'sass:dist', 'jade', 'copy', 'browserify:prod', 'uglify']
|
||||||
|
|
||||||
grunt.registerTask 'prepublish', ['clean', 'coffee']
|
grunt.registerTask 'prepublish', ['clean', 'coffee']
|
||||||
@@ -155,6 +156,10 @@ module.exports = (grunt)->
|
|||||||
|
|
||||||
grunt.registerTask 'test', ['mochaTest']
|
grunt.registerTask 'test', ['mochaTest']
|
||||||
|
|
||||||
|
grunt.registerTask 'watch-dev', ['clean', 'build', 'watch']
|
||||||
|
|
||||||
|
grunt.registerTask 'watch-test', ['clean', 'build', 'watch:test']
|
||||||
|
|
||||||
args = process.argv[..]
|
args = process.argv[..]
|
||||||
while args.length > 0
|
while args.length > 0
|
||||||
switch args[0]
|
switch args[0]
|
||||||
|
|||||||
Binary file not shown.
@@ -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 %>"
|
||||||
|
|||||||
@@ -49,14 +49,17 @@ module.exports = class BaseController extends backbone.View
|
|||||||
|
|
||||||
if not ($el.hasClass('hiding') or $el.hasClass('hidden'))
|
if not ($el.hasClass('hiding') or $el.hasClass('hidden'))
|
||||||
logger.verbose => "#{this} is hiding \"#{$el.selector}\""
|
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'
|
$el.addClass 'hiding'
|
||||||
@trigger Event.animate.hide.start, this, $el
|
@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: ->
|
refresh: ->
|
||||||
logger.verbose => "#{this} refreshing"
|
logger.verbose => "#{this} refreshing"
|
||||||
@@ -86,20 +89,27 @@ module.exports = class BaseController extends backbone.View
|
|||||||
|
|
||||||
show: ($el)->
|
show: ($el)->
|
||||||
$el ?= @$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}\""
|
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'
|
$el.removeClass 'hidden'
|
||||||
_.defer =>
|
$el.addClass 'hiding'
|
||||||
$el.removeClass 'hiding'
|
@trigger Event.animate.show.start, this, $el
|
||||||
@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: ->
|
unrender: ->
|
||||||
@undelegateEvents()
|
@undelegateEvents()
|
||||||
@@ -153,9 +163,9 @@ module.exports = class BaseController extends backbone.View
|
|||||||
@tryRefresh()
|
@tryRefresh()
|
||||||
|
|
||||||
Object.defineProperties @prototype,
|
Object.defineProperties @prototype,
|
||||||
model: {get:@prototype.getModel, set:@prototype.setModel}
|
model: {get:@prototype.getModel, set:@prototype.setModel}
|
||||||
rendered: {get:@prototype.isRendered }
|
rendered: {get:@prototype.isRendered }
|
||||||
user: {get:@prototype.user, set:@prototype.setUser}
|
user: {get:@prototype.getUser, set:@prototype.setUser}
|
||||||
|
|
||||||
# Backbone.View Overrides ######################################################################
|
# Backbone.View Overrides ######################################################################
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@@ -59,11 +65,16 @@ module.exports = class ItemPageController extends PageController
|
|||||||
@adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper'
|
@adsenseController = @addChild AdsenseController, '.view__adsense', model:'sidebar_skyscraper'
|
||||||
|
|
||||||
options = 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')
|
||||||
@@ -111,17 +122,54 @@ module.exports = class ItemPageController extends PageController
|
|||||||
|
|
||||||
super
|
super
|
||||||
|
|
||||||
|
setUser: (user)->
|
||||||
|
super user
|
||||||
|
@_descriptionController.user = user if @_descriptionController?
|
||||||
|
|
||||||
# Backbone.View Overrides ######################################################################
|
# Backbone.View Overrides ######################################################################
|
||||||
|
|
||||||
events: ->
|
events: ->
|
||||||
return _.extend super,
|
return _.extend super,
|
||||||
'click a.craftingPlan': 'routeLinkClick'
|
'click a.craftingPlan': 'routeLinkClick'
|
||||||
'click .byline a': 'routeLinkClick'
|
'click .byline a': 'routeLinkClick'
|
||||||
'click .markdown a': 'routeLinkClick'
|
'click .markdown a': 'routeLinkClick'
|
||||||
'click button': 'craftingPlanButtonClicked'
|
'click button.craftingPlan': 'craftingPlanButtonClicked'
|
||||||
|
|
||||||
# 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 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: ->
|
_refreshByline: ->
|
||||||
mod = @model.item?.modVersion?.mod
|
mod = @model.item?.modVersion?.mod
|
||||||
if mod?.name?.length > 0
|
if mod?.name?.length > 0
|
||||||
@@ -136,6 +184,8 @@ module.exports = class ItemPageController extends PageController
|
|||||||
if @model.item?.description?.length > 0
|
if @model.item?.description?.length > 0
|
||||||
@_descriptionController.model = @model.item.description
|
@_descriptionController.model = @model.item.description
|
||||||
@show @$descriptionSection
|
@show @$descriptionSection
|
||||||
|
else if @user?
|
||||||
|
@show @$descriptionSection
|
||||||
else
|
else
|
||||||
@hide @$descriptionSection
|
@hide @$descriptionSection
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ module.exports = class LoginPageController extends PageController
|
|||||||
attributes = response.json.data.user
|
attributes = response.json.data.user
|
||||||
if attributes?
|
if attributes?
|
||||||
router.user = new User attributes
|
router.user = new User attributes
|
||||||
router.navigate Url.root(), trigger:true
|
router.resumeAfterLogin()
|
||||||
.catch (error)->
|
.catch (error)->
|
||||||
logger.error "Failed to get access token: #{error}"
|
logger.error "Failed to get access token: #{error}"
|
||||||
@_accessTokenLookupFailed = true
|
@_accessTokenLookupFailed = true
|
||||||
@@ -120,9 +120,9 @@ module.exports = class LoginPageController extends PageController
|
|||||||
@client.completeGitHubLogin code:code
|
@client.completeGitHubLogin code:code
|
||||||
.then (response)=>
|
.then (response)=>
|
||||||
router.user = new User response.json.user
|
router.user = new User response.json.user
|
||||||
.catch (error)->
|
.catch (error)=>
|
||||||
@_accessTokenLookupFailed = true
|
@_accessTokenLookupFailed = true
|
||||||
@refresh
|
@refresh()
|
||||||
.done()
|
.done()
|
||||||
|
|
||||||
_redirectToGitHub: ->
|
_redirectToGitHub: ->
|
||||||
|
|||||||
@@ -8,48 +8,153 @@ All rights reserved.
|
|||||||
BaseController = require './base_controller'
|
BaseController = require './base_controller'
|
||||||
convertMarkdown = require 'marked'
|
convertMarkdown = require 'marked'
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
|
{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 =
|
||||||
|
appologizing: 'applogizing'
|
||||||
|
confirming: 'confirming'
|
||||||
|
creating: 'creating'
|
||||||
|
editing: 'editing'
|
||||||
|
previewing: 'previewing'
|
||||||
|
viewing: 'viewing'
|
||||||
|
waiting: 'waiting'
|
||||||
|
|
||||||
constructor: (options={})->
|
constructor: (options={})->
|
||||||
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.imageBase ?= ''
|
options.model ?= null
|
||||||
options.model ?= ''
|
|
||||||
options.title ?= 'Description'
|
|
||||||
options.templateName = 'markdown_section'
|
options.templateName = 'markdown_section'
|
||||||
|
@_state = State.waiting
|
||||||
super options
|
super options
|
||||||
|
|
||||||
@imageBase = options.imageBase
|
@on Event.animate.hide.finish, => @_updateStateVisibility()
|
||||||
@modPack = options.modPack
|
@on Event.animate.show.finish, => @_updateStateVisibility()
|
||||||
@title = options.title
|
|
||||||
|
@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 #####################################################################
|
# BaseController Overrides #####################################################################
|
||||||
|
|
||||||
onDidRender: ->
|
onDidRender: ->
|
||||||
@$title = @$('h2')
|
@$errorText = @$('.error p')
|
||||||
@$markdownPanel = @$('.markdown')
|
@$markdownPanel = @$('.markdown')
|
||||||
|
@$sizer = @$('.sizer')
|
||||||
|
@$textarea = @$('textarea')
|
||||||
|
@$title = @$('h2')
|
||||||
|
|
||||||
|
@_resetToDefaultState()
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|
||||||
refresh: ->
|
refresh: ->
|
||||||
@$title.html @title
|
@$title.html @title
|
||||||
|
|
||||||
text = @model
|
@_updateSizer()
|
||||||
text = @_convertWikiLinks text
|
@_updatePreview()
|
||||||
text = @_convertImageLinks text
|
|
||||||
|
|
||||||
@$markdownPanel.html convertMarkdown text
|
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|
||||||
|
onWillChangeModel: (oldModel, newModel)->
|
||||||
|
result = super oldModel, newModel
|
||||||
|
@$textarea.val newModel
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
# Backbone.View Overrides ######################################################################
|
# Backbone.View Overrides ######################################################################
|
||||||
|
|
||||||
events: ->
|
events: ->
|
||||||
return _.extend super,
|
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 ##############################################################################
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
@@ -66,3 +171,81 @@ module.exports = class MarkdownSectionController extends BaseController
|
|||||||
result = "[#{name}](#{display.itemUrl})"
|
result = "[#{name}](#{display.itemUrl})"
|
||||||
|
|
||||||
return result
|
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
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ backbone = require 'backbone'
|
|||||||
module.exports = class CraftingGuideRouter extends backbone.Router
|
module.exports = class CraftingGuideRouter extends backbone.Router
|
||||||
|
|
||||||
constructor: (options={})->
|
constructor: (options={})->
|
||||||
|
@_lastReported = null
|
||||||
@_page = null
|
@_page = null
|
||||||
@_pageControllers = {}
|
@_pageControllers = {}
|
||||||
@_lastReported = null
|
@_postLoginUrl = Url.root
|
||||||
@_user = null
|
@_user = null
|
||||||
super options
|
super options
|
||||||
|
|
||||||
@@ -80,6 +81,16 @@ module.exports = class CraftingGuideRouter extends backbone.Router
|
|||||||
logger.info "User is not logged in"
|
logger.info "User is not logged in"
|
||||||
.done()
|
.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 #############################################################################
|
# Property Methods #############################################################################
|
||||||
|
|
||||||
getUser: ->
|
getUser: ->
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ module.exports = class Item extends BaseModel
|
|||||||
return if this.name < that.name then -1 else +1
|
return if this.name < that.name then -1 else +1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
unparse: ->
|
||||||
|
ItemParser = require './item_parser' # to avoid require cycles
|
||||||
|
@_parser ?= new ItemParser model:this
|
||||||
|
return @_parser.unparse()
|
||||||
|
|
||||||
# Property Methods #############################################################################
|
# Property Methods #############################################################################
|
||||||
|
|
||||||
getIsCraftable: ->
|
getIsCraftable: ->
|
||||||
|
|||||||
@@ -17,17 +17,23 @@ module.exports = class ItemParserV1 extends CommandParserVersionBase
|
|||||||
@_buildItem rawData, model
|
@_buildItem rawData, model
|
||||||
|
|
||||||
_unparseModel: (builder, model)->
|
_unparseModel: (builder, model)->
|
||||||
|
builder.line 'schema: ', 1
|
||||||
|
builder.line()
|
||||||
|
|
||||||
@_unparseItem builder, model
|
@_unparseItem builder, model
|
||||||
|
|
||||||
# Command Methods ##############################################################################
|
# Command Methods ##############################################################################
|
||||||
|
|
||||||
_command_description: (textParts...)->
|
_command_description: (textParts...)->
|
||||||
@_rawData.description ?= ''
|
if not @_rawData.description?
|
||||||
|
@_rawData.description = ''
|
||||||
|
else
|
||||||
|
@_rawData.description += '\n'
|
||||||
@_rawData.description += textParts.join ', '
|
@_rawData.description += textParts.join ', '
|
||||||
|
|
||||||
_command_officialUrl: (officialUrl)->
|
_command_officialUrl: (officialUrl)->
|
||||||
if @_rawData.officialUrl? then throw new Error 'duplicate declaration of "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
|
@_rawData.officialUrl = officialUrl
|
||||||
|
|
||||||
_command_video: (youTubeId, nameParts...)->
|
_command_video: (youTubeId, nameParts...)->
|
||||||
@@ -41,6 +47,26 @@ module.exports = class ItemParserV1 extends CommandParserVersionBase
|
|||||||
# Object Building Methods ######################################################################
|
# Object Building Methods ######################################################################
|
||||||
|
|
||||||
_buildItem: (rawData, model)->
|
_buildItem: (rawData, model)->
|
||||||
model.description = rawData.description if rawData.description
|
model.description = rawData.description if rawData.description?
|
||||||
model.officialUrl = rawData.officialUrl if rawData.officialUrl
|
model.officialUrl = rawData.officialUrl if rawData.officialUrl?
|
||||||
model.videos = rawData.videos if rawData.videos
|
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()
|
||||||
|
|||||||
@@ -7,4 +7,24 @@
|
|||||||
|
|
||||||
.view__markdown_section.section
|
.view__markdown_section.section
|
||||||
h2
|
h2
|
||||||
.panel.markdown
|
.panel
|
||||||
|
.waiting.hideable: img(src='/images/wait.gif')
|
||||||
|
.markdown.hideable.hidden
|
||||||
|
.editor.hideable.hidden
|
||||||
|
textarea
|
||||||
|
.sizer
|
||||||
|
.creating.hideable.hidden
|
||||||
|
p Please help by adding a description!
|
||||||
|
.buttons.hideable.hidden
|
||||||
|
.error.hideable.hidden: p
|
||||||
|
button.return.hideable.hidden return
|
||||||
|
button.cancel.hideable.hidden cancel
|
||||||
|
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.
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ All rights reserved.
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error { background: $color-error !important; }
|
.error { background: $color-error; }
|
||||||
|
|
||||||
.error-new { background: $color-error-new !important; }
|
.error-new { background: $color-error-new; }
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
|
|||||||
@@ -6,7 +6,102 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.creating {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $color-gray-light;
|
||||||
|
font-size: $font-size-x-large;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
min-height: 10em;
|
||||||
|
|
||||||
|
textarea, .sizer {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid $color-gray-light;
|
||||||
|
font-family: $font-family-input;
|
||||||
|
font-size: $font-size-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
position: absolute; height: 100%; width: 100%;
|
||||||
|
|
||||||
|
background: $color-gray-faint;
|
||||||
|
resize: none;
|
||||||
|
z-index: $layer-normal;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: $color-background-panel;
|
||||||
|
border: 1px solid $color-gray-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sizer {
|
||||||
|
padding-bottom: 1.5em;
|
||||||
|
visibility: hidden;
|
||||||
|
z-index: $layer-normal - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting {
|
||||||
|
position: relative; min-height: 20em; width: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute; top: 50%; left: 50%;
|
||||||
|
@include transform(translate(-50%, -50%));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ All rights reserved.
|
|||||||
position: absolute; bottom: 5em; height: 3.2em; width: 100%;
|
position: absolute; bottom: 5em; height: 3.2em; width: 100%;
|
||||||
|
|
||||||
.login {
|
.login {
|
||||||
display: none; // Just until there's a feature which needs it.
|
|
||||||
|
|
||||||
position: absolute; top: 50%; right: 2em;
|
position: absolute; top: 50%; right: 2em;
|
||||||
@include transform(translateY(-50%));
|
@include transform(translateY(-50%));
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
@@ -0,0 +1,100 @@
|
|||||||
|
###
|
||||||
|
Crafting Guide - item_parser_v1.test.coffee
|
||||||
|
|
||||||
|
Copyright (c) 2015 by Redwood Labs
|
||||||
|
All rights reserved.
|
||||||
|
###
|
||||||
|
|
||||||
|
Item = require '../../src/coffee/models/item'
|
||||||
|
ItemParserV1 = require '../../src/coffee/models/parser_versions/item_parser_v1'
|
||||||
|
_ = require 'underscore'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
baseText = item = parser = null
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'item_parser_v1.coffee', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
item = new Item name:'alpha'
|
||||||
|
parser = new ItemParserV1 model:item
|
||||||
|
|
||||||
|
describe 'officialUrl', ->
|
||||||
|
|
||||||
|
it 'may be omitted', ->
|
||||||
|
parser.parse 'schema: 1\nvideo: youtubeid, Video Alpha\ndescription: Bravo, Charlie'
|
||||||
|
expect(item.officialUrl).to.be.null
|
||||||
|
|
||||||
|
it 'is assigned properly when given', ->
|
||||||
|
parser.parse 'schema: 1\nofficialUrl: http://testurl.com'
|
||||||
|
item.officialUrl.should.equal 'http://testurl.com'
|
||||||
|
|
||||||
|
it 'does not allow duplicate declarations', ->
|
||||||
|
func = -> parser.parse 'schema: 1\nofficialUrl: http://testurl.com\nofficialUrl: http://testurl2.com'
|
||||||
|
expect(func).to.throw Error, 'duplicate'
|
||||||
|
|
||||||
|
it 'does not allow an empty value if given', ->
|
||||||
|
func = -> parser.parse 'schema: 1\nofficialUrl:'
|
||||||
|
expect(func).to.throw Error, 'empty'
|
||||||
|
|
||||||
|
describe 'description', ->
|
||||||
|
|
||||||
|
it 'may be omitted', ->
|
||||||
|
parser.parse 'schema: 1\nvideo: youTubeId, Video Alpha\nofficialUrl: http://testurl.com'
|
||||||
|
expect(item.description).to.be.null
|
||||||
|
|
||||||
|
it 'is assigned properly when given', ->
|
||||||
|
parser.parse 'schema: 1\ndescription: Alpha Bravo Charlie'
|
||||||
|
item.description.should.equal 'Alpha Bravo Charlie'
|
||||||
|
|
||||||
|
it 'can be a heredoc', ->
|
||||||
|
parser.parse 'schema: 1\ndescription: <<-END\nAlpha\nBravo\nCharlie\nEND'
|
||||||
|
item.description.should.equal 'Alpha\nBravo\nCharlie'
|
||||||
|
|
||||||
|
it 'concatenates multiple declarations', ->
|
||||||
|
parser.parse 'schema: 1\ndescription: Alpha\ndescription: Bravo'
|
||||||
|
item.description.should.equal 'Alpha\nBravo'
|
||||||
|
|
||||||
|
describe 'video', ->
|
||||||
|
|
||||||
|
it 'may be omitted', ->
|
||||||
|
parser.parse 'schema: 1\nofficialUrl: http://testurl.com\ndescription: Alpha Bravo Charlie'
|
||||||
|
item.videos.should.eql []
|
||||||
|
|
||||||
|
it 'is assigned properly when given', ->
|
||||||
|
parser.parse 'schema: 1\nvideo: youtubeid, Alpha Bravo'
|
||||||
|
item.videos[0].should.eql youTubeId:'youtubeid', name:'Alpha Bravo'
|
||||||
|
item.videos.length.should.equal 1
|
||||||
|
|
||||||
|
it 'may be included multiple times', ->
|
||||||
|
parser.parse 'schema: 1\nvideo: youtubeid1, Alpha\nvideo: youtubeid2, Bravo'
|
||||||
|
item.videos[0].should.eql youTubeId:'youtubeid1', name:'Alpha'
|
||||||
|
item.videos[1].should.eql youTubeId:'youtubeid2', name:'Bravo'
|
||||||
|
item.videos.length.should.equal 2
|
||||||
|
|
||||||
|
it 'requires a YouTubeId and name', ->
|
||||||
|
func = -> parser.parse 'schema: 1\nvideo: alpha'
|
||||||
|
expect(func).to.throw Error, 'requires a name'
|
||||||
|
|
||||||
|
describe 'unparsing', ->
|
||||||
|
|
||||||
|
it 'can round-trip a fully described item', ->
|
||||||
|
text = """
|
||||||
|
schema: 1
|
||||||
|
|
||||||
|
officialUrl: http://testurl.com
|
||||||
|
|
||||||
|
description: <<-END
|
||||||
|
Alpha
|
||||||
|
Bravo
|
||||||
|
END
|
||||||
|
|
||||||
|
video: youtubeid1, Alpha Bravo
|
||||||
|
video: youtubeid2, Charlie Delta
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
parser.parse text
|
||||||
|
parser.unparse().should.equal text
|
||||||
Reference in New Issue
Block a user