mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
WebSurfaceView now parsing player code through virtual DOM into iframe
This commit is contained in:
parent
5b16da099a
commit
ed320a8d9e
14 changed files with 207 additions and 22 deletions
45
app/assets/javascripts/web-dev-listener.js
Normal file
45
app/assets/javascripts/web-dev-listener.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
// TODO: don't serve this script from codecombat.com; serve it from a harmless extra domain we don't have yet.
|
||||
|
||||
window.addEventListener("message", receiveMessage, false);
|
||||
|
||||
var concreteDOM;
|
||||
var virtualDOM;
|
||||
|
||||
function receiveMessage(event) {
|
||||
var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
|
||||
if (origin != 'https://codecombat.com' && origin != 'http://localhost:3000') {
|
||||
console.log("Bad origin:", origin);
|
||||
}
|
||||
//console.log(event);
|
||||
switch (event.data.type) {
|
||||
case 'create':
|
||||
case 'update':
|
||||
if (virtualDOM)
|
||||
update(event.data.dom);
|
||||
else
|
||||
create(event.data.dom);
|
||||
break;
|
||||
case 'log':
|
||||
console.log(event.data.text);
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown message type:', event.data.type);
|
||||
}
|
||||
|
||||
//event.source.postMessage("hi there yourself! the secret response is: rheeeeet!", event.origin);
|
||||
}
|
||||
|
||||
function create(dom) {
|
||||
concreteDOM = deku.dom.create(event.data.dom);
|
||||
virtualDOM = event.data.dom;
|
||||
// TODO: target the actual HTML tag and combine our initial structure for styles/scripts/tags with theirs
|
||||
$('body').empty().append(concreteDOM);
|
||||
}
|
||||
|
||||
function update(dom) {
|
||||
function dispatch() {} // Might want to do something here in the future
|
||||
var context = {}; // Might want to use this to send shared state to every component
|
||||
var changes = deku.diff.diffNode(virtualDOM, event.data.dom);
|
||||
changes.reduce(deku.dom.update(dispatch, context), concreteDOM) // Rerender
|
||||
virtualDOM = event.data.dom;
|
||||
}
|
28
app/assets/web-dev-iframe.html
Normal file
28
app/assets/web-dev-iframe.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
||||
<title>My CodeCombat Website</title>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="/javascripts/web-dev-listener.js"></script>
|
||||
|
||||
<script src="/javascripts/app/vendor/aether-html.js"></script>
|
||||
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||
|
||||
<!-- Optional theme -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
||||
|
||||
<!-- Latest compiled and minified JavaScript -->
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Loading...</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -146,3 +146,6 @@ module.exports =
|
|||
lineOffsetPx: {type: ['number', 'undefined']}
|
||||
'tome:hide-problem-alert': c.object {title: 'Hide Problem Alert'}
|
||||
'tome:jiggle-problem-alert': c.object {title: 'Jiggle Problem Alert'}
|
||||
|
||||
'tome:html-updated': c.object {title: 'HTML Updated', required: ['html']},
|
||||
html: {type: 'string'}
|
||||
|
|
53
app/styles/play/level/web-surface-view.sass
Normal file
53
app/styles/play/level/web-surface-view.sass
Normal file
|
@ -0,0 +1,53 @@
|
|||
#web-surface-view
|
||||
background-color: white
|
||||
|
||||
iframe
|
||||
width: 100%
|
||||
height: 100%
|
||||
//
|
||||
// body
|
||||
// background-color: initial
|
||||
// color: initial
|
||||
//
|
||||
// html, body, div, span, applet, object, iframe,
|
||||
// h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
// a, abbr, acronym, address, big, cite, code,
|
||||
// del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
// small, strike, strong, sub, sup, tt, var,
|
||||
// b, u, i, center,
|
||||
// dl, dt, dd, ol, ul, li,
|
||||
// fieldset, form, label, legend,
|
||||
// table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
// article, aside, canvas, details, embed,
|
||||
// figure, figcaption, footer, header, hgroup,
|
||||
// menu, nav, output, ruby, section, summary,
|
||||
// time, mark, audio, video
|
||||
// margin: initial
|
||||
// padding: initial
|
||||
// border: initial
|
||||
// font-size: innitial
|
||||
// font: initial
|
||||
// vertical-align: baseline
|
||||
//
|
||||
// // HTML5 display-role reset for older browsers
|
||||
// article, aside, details, figcaption, figure,
|
||||
// footer, header, hgroup, menu, nav, section
|
||||
// display: block
|
||||
//
|
||||
// body
|
||||
// line-height: 1
|
||||
//
|
||||
// ol, ul
|
||||
// list-style: none
|
||||
//
|
||||
// blockquote, q
|
||||
// quotes: none
|
||||
//
|
||||
// blockquote:before, blockquote:after, q:before, q:after
|
||||
// content: ''
|
||||
// content: none
|
||||
//
|
||||
// table
|
||||
// border-collapse: collapse
|
||||
// border-spacing: 0
|
||||
//
|
|
@ -288,9 +288,12 @@ $level-resize-transition-time: 0.5s
|
|||
#canvas-wrapper canvas
|
||||
display: none
|
||||
|
||||
#web-surface
|
||||
width: 100%
|
||||
height: 100%
|
||||
#web-surface-view
|
||||
position: absolute
|
||||
top: 0
|
||||
right: 0
|
||||
left: 0
|
||||
bottom: 0
|
||||
|
||||
html.fullscreen-editor
|
||||
#level-view
|
||||
|
|
1
app/templates/play/level/web-surface-view.jade
Normal file
1
app/templates/play/level/web-surface-view.jade
Normal file
|
@ -0,0 +1 @@
|
|||
iframe(src="/web-dev-iframe.html")
|
|
@ -25,7 +25,7 @@ if view.showAds()
|
|||
canvas(width=924, height=589)#webgl-surface
|
||||
canvas(width=924, height=589)#normal-surface
|
||||
|
||||
#web-surface
|
||||
#web-surface-view
|
||||
#ascii-surface
|
||||
#canvas-left-gradient.gradient
|
||||
#canvas-top-gradient.gradient
|
||||
|
|
|
@ -37,6 +37,7 @@ require 'vendor/aether-python'
|
|||
require 'vendor/aether-coffeescript'
|
||||
require 'vendor/aether-lua'
|
||||
require 'vendor/aether-java'
|
||||
require 'vendor/aether-html'
|
||||
|
||||
module.exports = class LevelEditView extends RootView
|
||||
id: 'editor-level-view'
|
||||
|
|
|
@ -44,6 +44,7 @@ LevelSetupManager = require 'lib/LevelSetupManager'
|
|||
ContactModal = require 'views/core/ContactModal'
|
||||
HintsView = require './HintsView'
|
||||
HintsState = require './HintsState'
|
||||
WebSurfaceView = require './WebSurfaceView'
|
||||
|
||||
PROFILE_ME = false
|
||||
|
||||
|
@ -268,6 +269,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
@insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.isType('hero-ladder', 'course-ladder')
|
||||
@insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, courseID: @courseID, courseInstanceID: @courseInstanceID}
|
||||
@insertSubView @hintsView = new HintsView({ @session, @level, @hintsState }), @$('.hints-view')
|
||||
@insertSubView @webSurface = new WebSurfaceView level: @level if @level.isType('web-dev')
|
||||
#_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick'
|
||||
|
||||
initVolume: ->
|
||||
|
@ -324,7 +326,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
@levelLoader.destroy()
|
||||
@levelLoader = null
|
||||
if @level.isType('web-dev')
|
||||
@initWebSurface()
|
||||
Backbone.Mediator.publish 'level:started', {}
|
||||
else
|
||||
@initSurface()
|
||||
|
||||
|
@ -369,7 +371,6 @@ module.exports = class PlayLevelView extends RootView
|
|||
if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal
|
||||
return Backbone.Mediator.subscribeOnce 'modal:closed', @onLevelStarted, @
|
||||
@surface?.showLevel()
|
||||
@webSurface?.showLevel()
|
||||
Backbone.Mediator.publish 'level:set-time', time: 0
|
||||
if (@isEditorPreview or @observing) and not @getQueryVariable('intro')
|
||||
@loadingView.startUnveiling()
|
||||
|
@ -671,11 +672,3 @@ module.exports = class PlayLevelView extends RootView
|
|||
@setupManager?.destroy()
|
||||
@setupManager = new LevelSetupManager({supermodel: @supermodel, level: @level, levelID: @levelID, parent: @, session: @session, hadEverChosenHero: true})
|
||||
@setupManager.open()
|
||||
|
||||
|
||||
# web-dev levels
|
||||
initWebSurface: ->
|
||||
@webSurface = showLevel: =>
|
||||
# TODO: make a real WebSurface class
|
||||
@$('#web-surface').css('background-color', 'red')
|
||||
Backbone.Mediator.publish 'level:started', {}
|
||||
|
|
43
app/views/play/level/WebSurfaceView.coffee
Normal file
43
app/views/play/level/WebSurfaceView.coffee
Normal file
|
@ -0,0 +1,43 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
State = require 'models/State'
|
||||
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) ->
|
||||
@state = new State
|
||||
blah: 'blah'
|
||||
super(options)
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@iframe = @$('iframe')[0]
|
||||
$(@iframe).on 'load', (e) =>
|
||||
@iframe.contentWindow.postMessage {type: 'log', text: 'Player HTML iframe is ready.'}, "*"
|
||||
@iframeLoaded = true
|
||||
@onIframeLoaded?()
|
||||
@onIframeLoaded = null
|
||||
|
||||
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 = @dekuify html
|
||||
messageType = if @virtualDOM then 'update' else 'create'
|
||||
@iframe.contentWindow.postMessage {type: messageType, dom: 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
|
||||
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 ? []))
|
|
@ -70,6 +70,14 @@ module.exports = class Spell
|
|||
@originalSource = @languages[@language] ? @languages.javascript
|
||||
@originalSource = @addPicoCTFProblem() if window.serverConfig.picoCTF
|
||||
|
||||
if @level.isType('web-dev')
|
||||
playerCode = @originalSource.match(/<playercode>\n([\s\S]*)\n *<\/playercode>/)[1]
|
||||
playerCodeLines = playerCode.split('\n')
|
||||
indentation = playerCodeLines[0].length - playerCodeLines[0].trim().length
|
||||
playerCode = (line.substr(indentation) for line in playerCodeLines).join('\n')
|
||||
@wrapperCode = @originalSource.replace /<playercode>[\s\S]*<\/playercode>/, '☃'
|
||||
@originalSource = playerCode
|
||||
|
||||
# Translate comments chosen spoken language.
|
||||
return unless @commentContext
|
||||
context = $.extend true, {}, @commentContext
|
||||
|
@ -95,6 +103,9 @@ module.exports = class Spell
|
|||
when 'coffeescript' then @originalSource
|
||||
else @originalSource
|
||||
|
||||
constructHTML: (source) ->
|
||||
@wrapperCode.replace '☃', source
|
||||
|
||||
addPicoCTFProblem: ->
|
||||
return @originalSource unless problem = @level.picoCTFProblem
|
||||
description = """
|
||||
|
|
|
@ -539,9 +539,10 @@ module.exports = class SpellView extends CocoView
|
|||
Backbone.Mediator.publish 'tome:spell-loaded', spell: @spell
|
||||
@eventsSuppressed = false # Now that the initial change is in, we can start running any changed code
|
||||
@createToolbarView()
|
||||
@updateHTML() if @options.level.isType('web-dev')
|
||||
|
||||
createDebugView: ->
|
||||
return if @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') # We'll turn this on later, maybe, but not yet.
|
||||
return if @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') # We'll turn this on later, maybe, but not yet.
|
||||
@debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell
|
||||
@$el.append @debugView.render().$el.hide()
|
||||
|
||||
|
@ -712,6 +713,8 @@ module.exports = class SpellView extends CocoView
|
|||
]
|
||||
onSignificantChange.push _.debounce @checkRequiredCode, 750 if @options.level.get 'requiredCode'
|
||||
onSignificantChange.push _.debounce @checkSuspectCode, 750 if @options.level.get 'suspectCode'
|
||||
onAnyChange.push _.throttle @updateHTML, 10 if @options.level.isType('web-dev')
|
||||
|
||||
@onCodeChangeMetaHandler = =>
|
||||
return if @eventsSuppressed
|
||||
#@playSound 'code-change', volume: 0.5 # Currently not using this sound.
|
||||
|
@ -724,6 +727,9 @@ module.exports = class SpellView extends CocoView
|
|||
|
||||
onCursorActivity: => # Used to refresh autocast delay; doesn't do anything at the moment.
|
||||
|
||||
updateHTML: =>
|
||||
Backbone.Mediator.publish 'tome:html-updated', html: @spell.constructHTML @getSource()
|
||||
|
||||
# Design for a simpler system?
|
||||
# * Keep Aether linting, debounced, on any significant change
|
||||
# - All problems just vanish when you make any change to the code
|
||||
|
@ -865,9 +871,7 @@ module.exports = class SpellView extends CocoView
|
|||
beginningOfLine = not currentLine.substr(0, cursorPosition.column).trim().length # uncommenting code, for example
|
||||
incompleteThis = /^(s|se|sel|self|t|th|thi|this)$/.test currentLine.trim()
|
||||
#console.log "finished=#{valid and (endOfLine or beginningOfLine) and not incompleteThis}", valid, endOfLine, beginningOfLine, incompleteThis, cursorPosition, currentLine.length, aether, new Date() - 0, currentLine
|
||||
if @options.level.isType('web-dev') and valid
|
||||
console.log 'Update it!'
|
||||
else if valid and (endOfLine or beginningOfLine) and not incompleteThis
|
||||
if valid and (endOfLine or beginningOfLine) and not incompleteThis
|
||||
@preload()
|
||||
|
||||
singleLineCommentRegex: ->
|
||||
|
@ -896,6 +900,7 @@ module.exports = class SpellView extends CocoView
|
|||
return if @spell.source.indexOf('while') isnt -1 # If they're working with while-loops, it's more likely to be an incomplete infinite loop, so don't preload.
|
||||
return if @spell.source.length > 500 # Only preload on really short methods
|
||||
return if @spellThang?.castAether?.metrics?.statementsExecuted > 2000 # Don't preload if they are running significant amounts of user code
|
||||
return if @options.level.isType('web-dev')
|
||||
oldSource = @spell.source
|
||||
oldSpellThangAethers = {}
|
||||
for thangID, spellThang of @spell.thangs
|
||||
|
|
|
@ -107,13 +107,12 @@
|
|||
"aether": {
|
||||
"main": [
|
||||
"build/aether.js",
|
||||
"build/clojure.js",
|
||||
"build/coffeescript.js",
|
||||
"build/io.js",
|
||||
"build/javascript.js",
|
||||
"build/lua.js",
|
||||
"build/python.js",
|
||||
"build/java.js"
|
||||
"build/java.js",
|
||||
"build/html.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -115,7 +115,7 @@ exports.config =
|
|||
'javascripts/app/vendor/aether-lua.js': 'bower_components/aether/build/lua.js'
|
||||
'javascripts/app/vendor/aether-java.js': 'bower_components/aether/build/java.js'
|
||||
'javascripts/app/vendor/aether-python.js': 'bower_components/aether/build/python.js'
|
||||
'javascripts/app/vendor/aether-java.js': 'bower_components/aether/build/java.js'
|
||||
'javascripts/app/vendor/aether-html.js': 'bower_components/aether/build/html.js'
|
||||
|
||||
# Any vendor libraries we don't want the client to load immediately
|
||||
'javascripts/app/vendor/d3.js': regJoin('^bower_components/d3')
|
||||
|
|
Loading…
Reference in a new issue