mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-04 21:01:06 -05:00
663c220eaf
This heavily refactors SpellView and adds infrastructure for receiving and reporting Errors raised by the web-dev iFrame. The web-dev error system, the Aether error system, and the Ace html-worker avoid disturbing each others' errors/annotations (though currently Aether+web-dev errors won't coexist), and they clear/update their own asynchronously. Show web-dev iFrame errors as Ace annotations Add functional error banners (with poor messages) Improve error banners, don't allow duplicate Problems Refactor setAnnotations override Convert all constructor calls for Problems Add comments, clean up Clean up Don't clear things unnecessarily Clean up error message sending from iFrame Add web-dev:error schema Clarify error message attributes Refactor displaying AetherProblems Refactor displaying user problem banners Refactor onWebDevError Set ace styles on updating @problems Clean up, fix off-by-1 error Add comment Show stale web-dev errors differently Some web-dev errors are generated by "stale" code — code that's still running in the iFrame but doesn't have the player's recent changes. This shows those errors differently than if they weren't "stale", and suggests they re-run their code. Hook up web-dev event schema Destroy ignored duplicate problems Functionalize a bit of stuff Fix ProblemAlertView never loading
99 lines
4.4 KiB
CoffeeScript
99 lines
4.4 KiB
CoffeeScript
CocoView = require 'views/core/CocoView'
|
|
template = require 'templates/play/level/web-surface-view'
|
|
|
|
module.exports = class WebSurfaceView extends CocoView
|
|
id: 'web-surface-view'
|
|
template: template
|
|
|
|
subscriptions:
|
|
'tome:html-updated': 'onHTMLUpdated'
|
|
|
|
initialize: (options) ->
|
|
@goals = (goal for goal in options.goalManager?.goals ? [] when goal.html)
|
|
# Consider https://www.npmjs.com/package/css-select to do this on virtualDom instead of in iframe on concreteDOM
|
|
super(options)
|
|
|
|
afterRender: ->
|
|
super()
|
|
@iframe = @$('iframe')[0]
|
|
$(@iframe).on 'load', (e) =>
|
|
window.addEventListener 'message', @onIframeMessage
|
|
@iframeLoaded = true
|
|
@onIframeLoaded?()
|
|
@onIframeLoaded = null
|
|
|
|
# TODO: make clicking Run actually trigger a 'create' update here (for resetting scripts)
|
|
|
|
onHTMLUpdated: (e) ->
|
|
unless @iframeLoaded
|
|
return @onIframeLoaded = => @onHTMLUpdated e unless @destroyed
|
|
dom = htmlparser2.parseDOM e.html, {}
|
|
body = _.find(dom, name: 'body') ? {name: 'body', attribs: null, children: dom}
|
|
html = _.find(dom, name: 'html') ? {name: 'html', attribs: null, children: [body]}
|
|
# TODO: pull out the actual scripts, styles, and body/elements they are doing so we can merge them with our initial structure on the other side
|
|
{ virtualDom, styles, scripts } = @extractStylesAndScripts(@dekuify html)
|
|
messageType = if e.create or not @virtualDom then 'create' else 'update'
|
|
@iframe.contentWindow.postMessage {type: messageType, dom: virtualDom, styles, scripts, goals: @goals}, '*'
|
|
@virtualDom = virtualDom
|
|
|
|
dekuify: (elem) ->
|
|
return elem.data if elem.type is 'text'
|
|
return null if elem.type is 'comment' # TODO: figure out how to make a comment in virtual dom
|
|
elem.attribs = _.omit elem.attribs, (val, attr) -> attr.indexOf('<') > -1 # Deku chokes on `<thing <p></p>`
|
|
unless elem.name
|
|
console.log("Failed to dekuify", elem)
|
|
return elem.type
|
|
deku.element(elem.name, elem.attribs, (@dekuify(c) for c in elem.children ? []))
|
|
|
|
extractStylesAndScripts: (dekuTree) ->
|
|
recurse = (dekuTree) ->
|
|
#base case
|
|
if dekuTree.type is '#text'
|
|
return { virtualDom: dekuTree, styles: [], scripts: [] }
|
|
if dekuTree.type is 'style'
|
|
return { styles: [dekuTree], scripts: [] }
|
|
if dekuTree.type is 'script'
|
|
return { styles: [], scripts: [dekuTree] }
|
|
# recurse over children
|
|
childStyles = []
|
|
childScripts = []
|
|
dekuTree.children?.forEach (dekuChild, index) =>
|
|
{ virtualDom, styles, scripts } = recurse(dekuChild)
|
|
dekuTree.children[index] = virtualDom
|
|
childStyles = childStyles.concat(styles)
|
|
childScripts = childScripts.concat(scripts)
|
|
dekuTree.children = _.filter dekuTree.children # Remove the nodes we extracted
|
|
return { virtualDom: dekuTree, scripts: childScripts, styles: childStyles }
|
|
|
|
{ virtualDom, scripts, styles } = recurse(dekuTree)
|
|
wrappedStyles = deku.element('head', {}, styles)
|
|
wrappedScripts = deku.element('head', {}, scripts)
|
|
return { virtualDom, scripts: wrappedScripts, styles: wrappedStyles }
|
|
|
|
combineNodes: (type, nodes) ->
|
|
if _.any(nodes, (node) -> node.type isnt type)
|
|
throw new Error("Can't combine nodes of different types. (Got #{nodes.map (n) -> n.type})")
|
|
children = nodes.map((n) -> n.children).reduce(((a,b) -> a.concat(b)), [])
|
|
if _.isEmpty(children)
|
|
deku.element(type, {})
|
|
else
|
|
deku.element(type, {}, children)
|
|
|
|
onIframeMessage: (event) =>
|
|
origin = event.origin or event.originalEvent.origin
|
|
unless origin is window.location.origin
|
|
return console.log 'Ignoring message from bad origin:', origin
|
|
unless event.source is @iframe.contentWindow
|
|
return console.log 'Ignoring message from somewhere other than our iframe:', event.source
|
|
switch event.data.type
|
|
when 'goals-updated'
|
|
Backbone.Mediator.publish 'god:new-html-goal-states', goalStates: event.data.goalStates, overallStatus: event.data.overallStatus
|
|
when 'error'
|
|
# NOTE: The line number in this is relative to the script tag, not the user code. The offset is added in SpellView.
|
|
Backbone.Mediator.publish 'web-dev:error', _.pick(event.data, ['message', 'line', 'column', 'url'])
|
|
else
|
|
console.warn 'Unknown message type', event.data.type, 'for message', event, 'from origin', origin
|
|
|
|
destroy: ->
|
|
window.removeEventListener 'message', @onIframeMessage
|
|
super()
|