mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 01:25:42 -05:00
Implements the SPADE logger into the SpellView
* Updates spade.js vendor file, adds a sublime-project for developers to use * Moves server logic away from handlers * Moves session update logic to middleware, sets up server schema to autorender IDs as ObjectIDs * Modernizes the supermodel loading scheme and switches from constructor to initalize
This commit is contained in:
parent
e4392019a4
commit
c4652d82e2
21 changed files with 607 additions and 3 deletions
6
app/collections/CodeLogs.coffee
Normal file
6
app/collections/CodeLogs.coffee
Normal file
|
@ -0,0 +1,6 @@
|
|||
CocoCollection = require 'collections/CocoCollection'
|
||||
CodeLog = require 'models/CodeLog'
|
||||
|
||||
module.exports = class CodeLogCollection extends CocoCollection
|
||||
url: '/db/codelogs'
|
||||
model: CodeLog
|
|
@ -37,6 +37,7 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'admin/trial-requests': go('admin/TrialRequestsView')
|
||||
'admin/user-code-problems': go('admin/UserCodeProblemsView')
|
||||
'admin/pending-patches': go('admin/PendingPatchesView')
|
||||
'admin/codelogs': go('admin/CodeLogsView')
|
||||
|
||||
'beta': go('HomeView')
|
||||
|
||||
|
|
6
app/models/CodeLog.coffee
Normal file
6
app/models/CodeLog.coffee
Normal file
|
@ -0,0 +1,6 @@
|
|||
CocoModel = require './CocoModel'
|
||||
|
||||
module.exports = class CodeLog extends CocoModel
|
||||
@className: 'CodeLog'
|
||||
@schema: require 'schemas/models/codelog.schema'
|
||||
urlRoot: '/db/codelogs'
|
20
app/schemas/models/codelog.schema.coffee
Normal file
20
app/schemas/models/codelog.schema.coffee
Normal file
|
@ -0,0 +1,20 @@
|
|||
c = require './../schemas'
|
||||
|
||||
LevelVersionSchema = c.object {required: ['original', 'majorVersion'], links: [{rel: 'db', href: '/db/level/{(original)}/version/{(majorVersion)}'}]},
|
||||
original: c.objectId()
|
||||
majorVersion:
|
||||
type: 'integer'
|
||||
minimum: 0
|
||||
|
||||
|
||||
CodeLogSchema =
|
||||
type: 'object'
|
||||
properties:
|
||||
sessionID: c.objectId()
|
||||
level: LevelVersionSchema
|
||||
levelSlug: {type:'string'}
|
||||
userID: c.objectId()
|
||||
log: {type:'string'}
|
||||
created: c.date()
|
||||
|
||||
module.exports = CodeLogSchema
|
|
@ -150,6 +150,9 @@ _.extend LevelSessionSchema.properties,
|
|||
type: 'string'
|
||||
format: 'code'
|
||||
|
||||
codeLogs:
|
||||
type: 'array'
|
||||
|
||||
codeLanguage:
|
||||
type: 'string'
|
||||
|
||||
|
|
|
@ -173,3 +173,5 @@ module.exports =
|
|||
'level:subscription-required': c.object {}
|
||||
|
||||
'level:course-membership-required': c.object {}
|
||||
|
||||
'level:contact-button-pressed': c.object {title: 'Contact Pressed', description: 'Dispatched when the contact button is pressed in a level.'}
|
||||
|
|
6
app/styles/admin/codelogs-view.sass
Normal file
6
app/styles/admin/codelogs-view.sass
Normal file
|
@ -0,0 +1,6 @@
|
|||
#codelogs-view
|
||||
#codelogs-tooltip
|
||||
z-index: 9999
|
||||
position: absolute
|
||||
width: 512px
|
||||
height: 512px
|
22
app/templates/admin/codelogs-view.jade
Normal file
22
app/templates/admin/codelogs-view.jade
Normal file
|
@ -0,0 +1,22 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
#codelogs-view
|
||||
#codelogtable
|
||||
table.table.table-striped
|
||||
tr
|
||||
th date
|
||||
th userID
|
||||
th levelSlug
|
||||
th
|
||||
if view.codelogs
|
||||
for codelog in view.codelogs.models
|
||||
+codeLogRow(codelog)
|
||||
|
||||
mixin codeLogRow(codelog)
|
||||
tr
|
||||
td= codelog.get('created')
|
||||
td= codelog.get('userID')
|
||||
td= codelog.get('levelSlug')
|
||||
td
|
||||
button.button.playback(data-codelog=codelog.get('log')) Playback
|
0
app/views/admin/CodeLogs.coffee
Normal file
0
app/views/admin/CodeLogs.coffee
Normal file
43
app/views/admin/CodeLogsView.coffee
Normal file
43
app/views/admin/CodeLogsView.coffee
Normal file
|
@ -0,0 +1,43 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/admin/codelogs-view'
|
||||
CodeLogCollection = require 'collections/CodeLogs'
|
||||
CodeLog = require 'models/CodeLog'
|
||||
utils = require 'core/utils'
|
||||
|
||||
module.exports = class CodeLogsView extends RootView
|
||||
template: template
|
||||
id: 'codelogs-view'
|
||||
tooltip: null
|
||||
events:
|
||||
'click .playback': 'onClickPlayback'
|
||||
|
||||
initialize: ->
|
||||
@spade = new Spade()
|
||||
@codelogs = new CodeLogCollection()
|
||||
@supermodel.trackRequest(@codelogs.fetch())
|
||||
|
||||
onClickPlayback: (e) ->
|
||||
@deleteTooltip()
|
||||
events = LZString.decompressFromUTF16($(e.target).data('codelog'))
|
||||
events = @spade.expand(JSON.parse(events))
|
||||
|
||||
@tooltip = $(document.createElement('textarea'))
|
||||
@tooltip.attr('id', "codelogs-tooltip")
|
||||
@tooltip.css({left: e.pageX + 20, top: e.pageY}) # Position near the cursor
|
||||
@tooltip.blur @onBlurTooltip
|
||||
@$('#codelogs-view').append @tooltip
|
||||
@tooltip.focus()
|
||||
@spade.play(events, @tooltip.context)
|
||||
|
||||
deleteTooltip: ->
|
||||
if @tooltip?
|
||||
@tooltip.off 'blur'
|
||||
@tooltip.remove()
|
||||
@tooltip = null
|
||||
|
||||
onBlurTooltip: (e) =>
|
||||
@deleteTooltip()
|
||||
|
||||
destroy: ->
|
||||
@deleteTooltip()
|
||||
super()
|
|
@ -593,6 +593,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
session.save {screenshot: screenshot}, {patch: true, type: 'PUT'}
|
||||
|
||||
onContactClicked: (e) ->
|
||||
Backbone.Mediator.publish 'level:contact-button-pressed', {}
|
||||
@openModalView contactModal = new ContactModal levelID: @level.get('slug') or @level.id, courseID: @courseID, courseInstanceID: @courseInstanceID
|
||||
screenshot = @surface.screenshot(1, 'image/png', 1.0, 1)
|
||||
body =
|
||||
|
|
|
@ -10,6 +10,7 @@ SpellToolbarView = require './SpellToolbarView'
|
|||
LevelComponent = require 'models/LevelComponent'
|
||||
UserCodeProblem = require 'models/UserCodeProblem'
|
||||
utils = require 'core/utils'
|
||||
CodeLog = require 'models/CodeLog'
|
||||
|
||||
module.exports = class SpellView extends CocoView
|
||||
id: 'spell-view'
|
||||
|
@ -47,6 +48,8 @@ module.exports = class SpellView extends CocoView
|
|||
'tome:maximize-toggled': 'onMaximizeToggled'
|
||||
'script:state-changed': 'onScriptStateChange'
|
||||
'playback:ended-changed': 'onPlaybackEndedChanged'
|
||||
'level:contact-button-pressed': 'onContactButtonPressed'
|
||||
'level:show-victory': 'onShowVictory'
|
||||
|
||||
events:
|
||||
'mouseout': 'onMouseOut'
|
||||
|
@ -63,7 +66,6 @@ module.exports = class SpellView extends CocoView
|
|||
@highlightCurrentLine = _.throttle @highlightCurrentLine, 100
|
||||
$(window).on 'resize', @onWindowResize
|
||||
@observing = @session.get('creator') isnt me.id
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@createACE()
|
||||
|
@ -106,6 +108,14 @@ module.exports = class SpellView extends CocoView
|
|||
$(@ace.container).find('.ace_gutter').on 'click', @onGutterClick
|
||||
@initAutocomplete aceConfig.liveCompletion ? true
|
||||
|
||||
return if @session.get('creator') isnt me.id or @session.fake
|
||||
# Create a Spade to 'dig' into Ace.
|
||||
@spade = new Spade()
|
||||
@spade.track(@ace)
|
||||
# If a user is taking longer than 10 minutes, let's log it.
|
||||
saveSpadeDelay = 10 * 60 * 1000
|
||||
@saveSpadeTimeout = setTimeout @saveSpade, saveSpadeDelay
|
||||
|
||||
createACEShortcuts: ->
|
||||
@aceCommands = aceCommands = []
|
||||
ace = @ace
|
||||
|
@ -636,6 +646,9 @@ module.exports = class SpellView extends CocoView
|
|||
onMouseOut: (e) ->
|
||||
@debugView?.onMouseOut e
|
||||
|
||||
onContactButtonPressed: (e) ->
|
||||
@saveSpade()
|
||||
|
||||
getSource: ->
|
||||
@ace.getValue() # could also do @firepad.getText()
|
||||
|
||||
|
@ -705,6 +718,33 @@ module.exports = class SpellView extends CocoView
|
|||
return if @destroyed
|
||||
Backbone.Mediator.publish 'tome:hide-problem-alert', {}
|
||||
|
||||
saveSpade: =>
|
||||
return if @destroyed
|
||||
spadeEvents = @spade.compile()
|
||||
# Uncomment the below line for a debug panel to display inside the level
|
||||
#@spade.debugPlay(spadeEvents)
|
||||
condensedEvents = @spade.condense(spadeEvents)
|
||||
|
||||
return unless condensedEvents.length
|
||||
compressedEvents = LZString.compressToUTF16(JSON.stringify(condensedEvents))
|
||||
|
||||
codeLog = new CodeLog({
|
||||
sessionID: @options.session.id
|
||||
level:
|
||||
original: @options.level.get 'original'
|
||||
majorVersion: (@options.level.get 'version').major
|
||||
levelSlug: @options.level.get 'slug'
|
||||
userID: @options.session.get 'creator'
|
||||
log: compressedEvents
|
||||
})
|
||||
|
||||
codeLog.save()
|
||||
|
||||
onShowVictory: (e) ->
|
||||
if @saveSpadeTimeout?
|
||||
window.clearTimeout @saveSpadeTimeout
|
||||
@saveSpadeTimeout = null
|
||||
|
||||
onManualCast: (e) ->
|
||||
cast = @$el.parent().length
|
||||
@recompile cast, e.realTime
|
||||
|
@ -1299,6 +1339,8 @@ module.exports = class SpellView extends CocoView
|
|||
@toolbarView?.destroy()
|
||||
@zatanna.addSnippets [], @editorLang if @editorLang?
|
||||
$(window).off 'resize', @onWindowResize
|
||||
window.clearTimeout @saveSpadeTimeout
|
||||
@saveSpadeTimeout = null
|
||||
super()
|
||||
|
||||
commentStarts =
|
||||
|
|
47
scripts/analytics/mongodb/queries/playtime-over-10.js
Normal file
47
scripts/analytics/mongodb/queries/playtime-over-10.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Print out code language usage based on level session data
|
||||
|
||||
// Usage:
|
||||
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
|
||||
|
||||
|
||||
var startDate = new Date();
|
||||
startDate.setUTCDate(startDate.getUTCDate() - 7);
|
||||
var startDay = startDate.toISOString(0, 10);
|
||||
|
||||
const endDate = new Date();
|
||||
endDate.setUTCDate(endDate.getUTCDate());
|
||||
var endDay = endDate.toISOString().substr(0, 10);
|
||||
|
||||
var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
|
||||
var endObj = objectIdWithTimestamp(ISODate(endDay + "T00:00:00.000Z"))
|
||||
|
||||
var query = {
|
||||
$and:[
|
||||
{_id:{$gte:startObj}},
|
||||
{_id:{$lt:endObj}}
|
||||
]
|
||||
}
|
||||
var cursor = db.level.sessions.find(query, {playtime:1});
|
||||
var count = 0;
|
||||
var total = 0;
|
||||
|
||||
//Probably a built-in Mongo thing to do this... But it's not slow, so...
|
||||
while(cursor.hasNext()) {
|
||||
result = cursor.next();
|
||||
if(result.playtime >= 60 * 10) {
|
||||
count++;
|
||||
}
|
||||
total++;
|
||||
}
|
||||
|
||||
print("Number of sessions equal or over 60 * 10 playtime over the past 7 days: " + count + "\n" + "Total number of sessions over the past 7 days: " + total);
|
||||
|
||||
function objectIdWithTimestamp(timestamp) {
|
||||
// Convert string date to Date object (otherwise assume timestamp is a date)
|
||||
if (typeof(timestamp) == 'string') timestamp = new Date(timestamp);
|
||||
// Convert date object to hex seconds since Unix epoch
|
||||
const hexSeconds = Math.floor(timestamp/1000).toString(16);
|
||||
// Create an ObjectId with that hex timestamp
|
||||
const constructedObjectId = ObjectId(hexSeconds + "0000000000000000");
|
||||
return constructedObjectId
|
||||
}
|
|
@ -37,7 +37,13 @@ module.exports =
|
|||
if not _.size(_.intersection(req.user.get('permissions'), permissions))
|
||||
return next new errors.Forbidden('You do not have permissions necessary.')
|
||||
next()
|
||||
|
||||
|
||||
checkHasUser: ->
|
||||
return (req, res, next) ->
|
||||
if not req.user
|
||||
return next new errors.Unauthorized('No user associated with this request.')
|
||||
next()
|
||||
|
||||
whoAmI: wrap (req, res) ->
|
||||
if not req.user
|
||||
user = User.makeNew(req)
|
||||
|
|
22
server/middleware/codelogs.coffee
Normal file
22
server/middleware/codelogs.coffee
Normal file
|
@ -0,0 +1,22 @@
|
|||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
database = require '../commons/database'
|
||||
|
||||
mongoose = require 'mongoose'
|
||||
CodeLog = require '../models/CodeLog'
|
||||
LevelSession = require '../models/LevelSession'
|
||||
|
||||
module.exports =
|
||||
post: wrap (req, res) ->
|
||||
codeLog = database.initDoc(req, CodeLog)
|
||||
database.assignBody(req, codeLog)
|
||||
database.validateDoc(codeLog)
|
||||
codeLog = yield codeLog.save()
|
||||
|
||||
# Update the level session with sessionID to include the new codelog.
|
||||
yield LevelSession.update(
|
||||
{_id: mongoose.Types.ObjectId(req.body.sessionID)},
|
||||
{$push:{codeLogs: codeLog._id}}
|
||||
)
|
||||
|
||||
res.status(201).send(codeLog.toObject())
|
|
@ -3,6 +3,7 @@ module.exports =
|
|||
auth: require './auth'
|
||||
classrooms: require './classrooms'
|
||||
campaigns: require './campaigns'
|
||||
codelogs: require './codelogs'
|
||||
courseInstances: require './course-instances'
|
||||
files: require './files'
|
||||
named: require './named'
|
||||
|
|
34
server/models/CodeLog.coffee
Normal file
34
server/models/CodeLog.coffee
Normal file
|
@ -0,0 +1,34 @@
|
|||
mongoose = require 'mongoose'
|
||||
config = require '../../server_config'
|
||||
|
||||
CodeLogSchema = new mongoose.Schema({
|
||||
created:
|
||||
type: Date
|
||||
default: Date.now
|
||||
userID:
|
||||
type: mongoose.Schema.ObjectId
|
||||
sessionID:
|
||||
type: mongoose.Schema.ObjectId
|
||||
level:
|
||||
original:
|
||||
type: mongoose.Schema.ObjectId
|
||||
majorVersion:
|
||||
type: Number
|
||||
default: 0
|
||||
}, {strict: false, read: config.mongo.readpref})
|
||||
|
||||
CodeLogSchema.index({levelSlug: 1, created: -1}, {name: 'level slug index'})
|
||||
CodeLogSchema.index({userID: 1, created: -1}, {name: 'user id index'})
|
||||
|
||||
CodeLogSchema.statics.editableProperties = [
|
||||
'sessionID'
|
||||
'level'
|
||||
'levelSlug'
|
||||
'userID'
|
||||
'log'
|
||||
'created'
|
||||
]
|
||||
|
||||
CodeLogSchema.statics.jsonSchema = require '../../app/schemas/models/codelog.schema'
|
||||
|
||||
module.exports = CodeLog = mongoose.model('CodeLog', CodeLogSchema, 'codelogs')
|
|
@ -49,7 +49,11 @@ module.exports.setup = (app) ->
|
|||
app.get('/db/classroom/:handle/member-sessions', mw.classrooms.fetchMemberSessions)
|
||||
app.get('/db/classroom/:handle/members', mw.classrooms.fetchMembers) # TODO: Use mw.auth?
|
||||
app.get('/db/classroom/:handle', mw.auth.checkLoggedIn()) # TODO: Finish migrating route, adding now so 401 is returned
|
||||
|
||||
|
||||
CodeLog = require ('../models/CodeLog')
|
||||
app.post('/db/codelogs', mw.auth.checkHasUser(), mw.codelogs.post)
|
||||
app.get('/db/codelogs', mw.auth.checkHasPermission(['admin']), mw.rest.get(CodeLog))
|
||||
|
||||
Course = require '../models/Course'
|
||||
app.get('/db/course', mw.rest.get(Course))
|
||||
app.get('/db/course/:handle', mw.rest.getByHandle(Course))
|
||||
|
|
90
spec/server/functional/codelog.spec.coffee
Normal file
90
spec/server/functional/codelog.spec.coffee
Normal file
|
@ -0,0 +1,90 @@
|
|||
require '../common'
|
||||
utils = require '../utils'
|
||||
|
||||
Promise = require 'bluebird'
|
||||
request = require '../request'
|
||||
requestAsync = Promise.promisify(request, {multiArgs: true})
|
||||
|
||||
CodeLog = require '../../../server/models/CodeLog'
|
||||
User = require '../../../server/models/User'
|
||||
|
||||
testLog1 = {
|
||||
'sessionID': ObjectId("55b29efd1cd6abe8ce07db0d")
|
||||
'level': {
|
||||
'original': ObjectId("55b29efd1cd6abe8ce07db0d")
|
||||
'majorVersion': 0
|
||||
}
|
||||
'levelSlug': "d"
|
||||
'userID': ObjectId("55b29efd1cd6abe8ce07db0d")
|
||||
'userName': "b"
|
||||
'log': "a"
|
||||
}
|
||||
|
||||
testLog2 = {
|
||||
'sessionID': ObjectId("55b29efd1cd6abe8ce07db0d")
|
||||
'level': {
|
||||
'original': ObjectId("55b29efd1cd6abe8ce07db0d")
|
||||
'majorVersion': 0
|
||||
}
|
||||
'levelSlug': "dbbb"
|
||||
'userID': ObjectId("55b29efd1cd6abe8ce07db0d")
|
||||
'userName': "bbbb"
|
||||
'log': "abbb"
|
||||
}
|
||||
|
||||
describe 'POST /db/codelogs', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([CodeLog])
|
||||
user = yield utils.initUser({})
|
||||
yield utils.loginUser(user)
|
||||
done()
|
||||
it 'allows logged in users to create codelogs', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync {
|
||||
uri: getURL('/db/codelogs'), json: testLog1
|
||||
}
|
||||
expect(res.statusCode).toBe(201)
|
||||
done()
|
||||
it 'does allow anonymous users to create codelogs', utils.wrap (done) ->
|
||||
yield utils.becomeAnonymous()
|
||||
[res, body] = yield request.postAsync {
|
||||
uri: getURL('/db/codelogs'), json: testLog1
|
||||
}
|
||||
expect(res.statusCode).toBe(201)
|
||||
done()
|
||||
it 'does not allow unauthenticated users to create codelogs', utils.wrap (done) ->
|
||||
yield utils.logout()
|
||||
[res, body] = yield request.postAsync {
|
||||
uri: getURL('/db/codelogs'), json: testLog1
|
||||
}
|
||||
expect(res.statusCode).toBe(401)
|
||||
done()
|
||||
|
||||
describe 'GET /db/codelogs', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([CodeLog])
|
||||
# Fill database
|
||||
@admin = yield utils.initAdmin({})
|
||||
yield utils.loginUser(@admin)
|
||||
yield request.postAsync(getURL('/db/codelogs'), {json: testLog1})
|
||||
yield request.postAsync(getURL('/db/codelogs'), {json: testLog2})
|
||||
yield utils.logout()
|
||||
done()
|
||||
|
||||
it 'does not allow unauthenticated users to get codelogs', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri:getURL('/db/codelogs'), json:true}
|
||||
expect(res.statusCode).toBe(401)
|
||||
done()
|
||||
|
||||
it 'does not allow non-admins to get codelogs', utils.wrap (done) ->
|
||||
user = yield utils.initUser({})
|
||||
yield utils.loginUser(user)
|
||||
[res, body] = yield request.getAsync {uri:getURL('/db/codelogs'), json:true}
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'allows admins to get codelogs', utils.wrap (done) ->
|
||||
admin = yield utils.initAdmin({})
|
||||
yield utils.loginUser(admin)
|
||||
[res, body] = yield request.getAsync {uri:getURL('/db/codelogs'), json:true}
|
||||
expect(body.length).toBe(2)
|
||||
done()
|
12
sublime-project.json
Normal file
12
sublime-project.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"folders":[
|
||||
{
|
||||
"path":".",
|
||||
"folder_exclude_patterns":[
|
||||
"bower_components",
|
||||
"public",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
236
vendor/scripts/spade.js
vendored
Normal file
236
vendor/scripts/spade.js
vendored
Normal file
|
@ -0,0 +1,236 @@
|
|||
var Spade = function Spade() {
|
||||
this.stack = [];
|
||||
}
|
||||
Spade.prototype = {
|
||||
track: function(_elem) {
|
||||
this.target = _elem;
|
||||
|
||||
var spade = this;
|
||||
|
||||
var el = document.createElement("div");
|
||||
|
||||
keyHook = null;
|
||||
if(_elem.textInput && _elem.textInput.getElement) {
|
||||
keyHook = _elem.textInput.getElement();
|
||||
} else {
|
||||
keyHook = _elem;
|
||||
}
|
||||
keyHook.addEventListener("keydown", function(_event) {spade.createEvent(spade.target)});
|
||||
//Maybe this is needed depending on Firefox/other browsers? Duplicate non-diff events get compiled down.
|
||||
keyHook.addEventListener("keyup", function(_event) {spade.createEvent(spade.target)});
|
||||
_elem.addEventListener("mouseup", function(_event) {spade.createEvent(spade.target)});
|
||||
},
|
||||
createEvent: function(_target) {
|
||||
if(_target.getValue) {
|
||||
this.stack.push({
|
||||
"startPos":_target.selection.getCursor(),
|
||||
"endPos":_target.selection.getSelectionAnchor(),
|
||||
"content":_target.getValue(),
|
||||
"timestamp":(new Date()).getTime()
|
||||
});
|
||||
} else {
|
||||
this.stack.push({
|
||||
"startPos":_target.selectionStart,
|
||||
"endPos":_target.selectionEnd,
|
||||
"content":_target.value,
|
||||
"timestamp":(new Date()).getTime()
|
||||
});
|
||||
}
|
||||
},
|
||||
compile: function() {
|
||||
var compiledStack = [];
|
||||
if(this.stack.length > 0) {
|
||||
var startTime = this.stack[0].timestamp;
|
||||
var sum = 0;
|
||||
var sum2 = 0;
|
||||
for(var i = 0; i < this.stack.length; i++) {
|
||||
var c = this.stack[i];
|
||||
var adjustedTimestamp = c.timestamp - startTime;
|
||||
|
||||
var tString = ""; //The changed string.
|
||||
var fIndex = null; //The first index of changes.
|
||||
var eIndex = null; //The last index of changes.
|
||||
var dCount = 0; //Amount of character changes.
|
||||
if(i >= 1) {
|
||||
var p = this.stack[i - 1];
|
||||
var isOkay = false;
|
||||
for(var key in p) {
|
||||
if(key != "timestamp") {
|
||||
if(typeof p[key] === "string") {
|
||||
if(p[key] !== c[key]) {
|
||||
isOkay = true;
|
||||
}
|
||||
} else {
|
||||
for(var key2 in p[key]) {
|
||||
if(c[key][key2] !== undefined) {
|
||||
if(p[key][key2] !== c[key][key2]) {
|
||||
isOkay = true;
|
||||
}
|
||||
} else {
|
||||
console.warn("Warning: c[key][key2] doesn't exist, but p[key][key2] does.");
|
||||
isOkay = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!isOkay) {
|
||||
sum2++;
|
||||
continue;
|
||||
}
|
||||
sum++;
|
||||
if(p.content != c.content) {
|
||||
//Check from the start to the end, which characters are different.
|
||||
for(var j = 0; j < Math.max(p.content.length, c.content.length); j++) {
|
||||
if(p.content.charAt(j) === c.content.charAt(j)) {
|
||||
if(fIndex != null) {
|
||||
tString += c.content.charAt(j);
|
||||
dCount++;
|
||||
}
|
||||
} else {
|
||||
tString += c.content.charAt(j);
|
||||
if(fIndex === null) {
|
||||
fIndex = j;
|
||||
}
|
||||
dCount++;
|
||||
}
|
||||
}
|
||||
//Check from the end to the start, which characters are different.
|
||||
for(var j = 0; j < Math.min(p.content.length, c.content.length) - fIndex; j++) {
|
||||
if(p.content.charAt(p.content.length - 1 - j) !== c.content.charAt(c.content.length - 1 - j)) {
|
||||
if(eIndex == null) {
|
||||
eIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//This accounts for the fact when changing from "aa" to "aaa" (for example).
|
||||
if(eIndex === null) {
|
||||
eIndex = Math.min(p.content.length, c.content.length) - fIndex;
|
||||
}
|
||||
tString = tString.substring(0, tString.length - eIndex);
|
||||
}
|
||||
} else {
|
||||
tString = c.content;
|
||||
fIndex = 0;
|
||||
eIndex = tString.length;
|
||||
}
|
||||
compiledStack.push({
|
||||
"timestamp":adjustedTimestamp,
|
||||
"difContent":tString,
|
||||
"difFIndex":fIndex,
|
||||
"difEIndex":eIndex,
|
||||
"selFIndex":c.startPos,
|
||||
"selEIndex":c.endPos
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//Just return the empty array.
|
||||
}
|
||||
return compiledStack;
|
||||
},
|
||||
play: function(_stack, _elem) {
|
||||
if(_stack.length === 0) {
|
||||
console.warn("SPADE: No events to play.")
|
||||
return
|
||||
}
|
||||
if(_elem.setValue) {
|
||||
_elem.setValue(_stack[0].difContent);
|
||||
} else {
|
||||
_elem.value = _stack[0].difContent
|
||||
}
|
||||
_stack = _stack.slice();
|
||||
_stack.shift();
|
||||
var curTime, dTime;
|
||||
var elapsedTime = 0;
|
||||
var prevTime = (new Date()).getTime();
|
||||
var playbackInterval = setInterval(function() {
|
||||
curTime = (new Date()).getTime();
|
||||
dTime = curTime - prevTime;
|
||||
dTime *= 1; //Multiply for faster/slower playback speeds.
|
||||
elapsedTime += dTime;
|
||||
var tArray = _stack.filter(function(_event) {
|
||||
return ((_event.timestamp) >= (elapsedTime - dTime)) && ((_event.timestamp) < (elapsedTime));
|
||||
});
|
||||
for(var i = 0; i < tArray.length; i++) {
|
||||
var tEvent = tArray[i];
|
||||
var oVal = null;
|
||||
if(_elem.getValue) {
|
||||
oVal = _elem.getValue();
|
||||
} else {
|
||||
oVal = _elem.value;
|
||||
}
|
||||
if(tEvent.difFIndex !== null && tEvent.difEIndex !== null) {
|
||||
if(_elem.setValue) {
|
||||
_elem.setValue(oVal.substring(0, tEvent.difFIndex) + tEvent.difContent + oVal.substring(oVal.length - tEvent.difEIndex, oVal.length));
|
||||
} else {
|
||||
_elem.value = oVal.substring(0, tEvent.difFIndex) + tEvent.difContent + oVal.substring(oVal.length - tEvent.difEIndex, oVal.length)
|
||||
}
|
||||
}
|
||||
if(_elem.selection && _elem.selection.moveCursorToPosition) {
|
||||
//Maybe this will work someday
|
||||
_elem.selection.moveCursorToPosition(tEvent.selFIndex);
|
||||
_elem.selection.setSelectionAnchor(tEvent.selEIndex.row, tEvent.selEIndex.column);
|
||||
_elem.selection.selectTo(tEvent.selFIndex.row, tEvent.selFIndex.column);
|
||||
} else {
|
||||
//Likewise
|
||||
_elem.focus();
|
||||
_elem.setSelectionRange(tEvent.selFIndex, tEvent.selEIndex);
|
||||
}
|
||||
}
|
||||
if(_stack[_stack.length - 1] === undefined || elapsedTime > _stack[_stack.length - 1].timestamp) {
|
||||
clearInterval(playbackInterval);
|
||||
}
|
||||
prevTime = curTime;
|
||||
}, 10);
|
||||
},
|
||||
debugPlay: function(_stack) {
|
||||
var area = document.createElement('textarea');
|
||||
area.zIndex = 9999;
|
||||
area.style.width = "512px";
|
||||
area.style.height = "512px";
|
||||
area.style.position = "absolute";
|
||||
area.style.left = "100px";
|
||||
area.style.top = "100px";
|
||||
document.body.appendChild(area);
|
||||
this.play(_stack, area);
|
||||
},
|
||||
condense: function(_stack) {
|
||||
var compressedArray = [];
|
||||
for(var i = 0; i < _stack.length; i++) {
|
||||
var u = _stack[i];
|
||||
compressedArray.push([
|
||||
u.timestamp,
|
||||
u.difContent,
|
||||
u.difFIndex,
|
||||
u.difEIndex,
|
||||
u.selFIndex.row,
|
||||
u.selFIndex.column,
|
||||
u.selEIndex.row,
|
||||
u.selEIndex.column
|
||||
]);
|
||||
}
|
||||
return compressedArray;
|
||||
},
|
||||
expand: function(_array) {
|
||||
var uncompressedArray = [];
|
||||
for(var i = 0 ; i < _array.length; i++) {
|
||||
var c = _array[i];
|
||||
uncompressedArray.push({
|
||||
"timestamp":c[0],
|
||||
"difContent":c[1],
|
||||
"difFIndex":c[2],
|
||||
"difEIndex":c[3],
|
||||
"selFIndex":{
|
||||
"row":c[4],
|
||||
"column":c[5]
|
||||
},
|
||||
"selEIndex":{
|
||||
"row":c[6],
|
||||
"column":c[7]
|
||||
},
|
||||
});
|
||||
}
|
||||
return uncompressedArray;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue