Skip to content

Commit c4652d8

Browse files
committedApr 26, 2016
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
1 parent e439201 commit c4652d8

File tree

21 files changed

+607
-3
lines changed

21 files changed

+607
-3
lines changed
 

‎app/collections/CodeLogs.coffee

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CocoCollection = require 'collections/CocoCollection'
2+
CodeLog = require 'models/CodeLog'
3+
4+
module.exports = class CodeLogCollection extends CocoCollection
5+
url: '/db/codelogs'
6+
model: CodeLog

‎app/core/Router.coffee

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module.exports = class CocoRouter extends Backbone.Router
3737
'admin/trial-requests': go('admin/TrialRequestsView')
3838
'admin/user-code-problems': go('admin/UserCodeProblemsView')
3939
'admin/pending-patches': go('admin/PendingPatchesView')
40+
'admin/codelogs': go('admin/CodeLogsView')
4041

4142
'beta': go('HomeView')
4243

‎app/models/CodeLog.coffee

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CocoModel = require './CocoModel'
2+
3+
module.exports = class CodeLog extends CocoModel
4+
@className: 'CodeLog'
5+
@schema: require 'schemas/models/codelog.schema'
6+
urlRoot: '/db/codelogs'
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
c = require './../schemas'
2+
3+
LevelVersionSchema = c.object {required: ['original', 'majorVersion'], links: [{rel: 'db', href: '/db/level/{(original)}/version/{(majorVersion)}'}]},
4+
original: c.objectId()
5+
majorVersion:
6+
type: 'integer'
7+
minimum: 0
8+
9+
10+
CodeLogSchema =
11+
type: 'object'
12+
properties:
13+
sessionID: c.objectId()
14+
level: LevelVersionSchema
15+
levelSlug: {type:'string'}
16+
userID: c.objectId()
17+
log: {type:'string'}
18+
created: c.date()
19+
20+
module.exports = CodeLogSchema

‎app/schemas/models/level_session.coffee

+3
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ _.extend LevelSessionSchema.properties,
150150
type: 'string'
151151
format: 'code'
152152

153+
codeLogs:
154+
type: 'array'
155+
153156
codeLanguage:
154157
type: 'string'
155158

‎app/schemas/subscriptions/play.coffee

+2
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,5 @@ module.exports =
173173
'level:subscription-required': c.object {}
174174

175175
'level:course-membership-required': c.object {}
176+
177+
'level:contact-button-pressed': c.object {title: 'Contact Pressed', description: 'Dispatched when the contact button is pressed in a level.'}

‎app/styles/admin/codelogs-view.sass

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#codelogs-view
2+
#codelogs-tooltip
3+
z-index: 9999
4+
position: absolute
5+
width: 512px
6+
height: 512px
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
extends /templates/base
2+
3+
block content
4+
#codelogs-view
5+
#codelogtable
6+
table.table.table-striped
7+
tr
8+
th date
9+
th userID
10+
th levelSlug
11+
th
12+
if view.codelogs
13+
for codelog in view.codelogs.models
14+
+codeLogRow(codelog)
15+
16+
mixin codeLogRow(codelog)
17+
tr
18+
td= codelog.get('created')
19+
td= codelog.get('userID')
20+
td= codelog.get('levelSlug')
21+
td
22+
button.button.playback(data-codelog=codelog.get('log')) Playback

‎app/views/admin/CodeLogs.coffee

Whitespace-only changes.

‎app/views/admin/CodeLogsView.coffee

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
RootView = require 'views/core/RootView'
2+
template = require 'templates/admin/codelogs-view'
3+
CodeLogCollection = require 'collections/CodeLogs'
4+
CodeLog = require 'models/CodeLog'
5+
utils = require 'core/utils'
6+
7+
module.exports = class CodeLogsView extends RootView
8+
template: template
9+
id: 'codelogs-view'
10+
tooltip: null
11+
events:
12+
'click .playback': 'onClickPlayback'
13+
14+
initialize: ->
15+
@spade = new Spade()
16+
@codelogs = new CodeLogCollection()
17+
@supermodel.trackRequest(@codelogs.fetch())
18+
19+
onClickPlayback: (e) ->
20+
@deleteTooltip()
21+
events = LZString.decompressFromUTF16($(e.target).data('codelog'))
22+
events = @spade.expand(JSON.parse(events))
23+
24+
@tooltip = $(document.createElement('textarea'))
25+
@tooltip.attr('id', "codelogs-tooltip")
26+
@tooltip.css({left: e.pageX + 20, top: e.pageY}) # Position near the cursor
27+
@tooltip.blur @onBlurTooltip
28+
@$('#codelogs-view').append @tooltip
29+
@tooltip.focus()
30+
@spade.play(events, @tooltip.context)
31+
32+
deleteTooltip: ->
33+
if @tooltip?
34+
@tooltip.off 'blur'
35+
@tooltip.remove()
36+
@tooltip = null
37+
38+
onBlurTooltip: (e) =>
39+
@deleteTooltip()
40+
41+
destroy: ->
42+
@deleteTooltip()
43+
super()

‎app/views/play/level/PlayLevelView.coffee

+1
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ module.exports = class PlayLevelView extends RootView
593593
session.save {screenshot: screenshot}, {patch: true, type: 'PUT'}
594594

595595
onContactClicked: (e) ->
596+
Backbone.Mediator.publish 'level:contact-button-pressed', {}
596597
@openModalView contactModal = new ContactModal levelID: @level.get('slug') or @level.id, courseID: @courseID, courseInstanceID: @courseInstanceID
597598
screenshot = @surface.screenshot(1, 'image/png', 1.0, 1)
598599
body =

‎app/views/play/level/tome/SpellView.coffee

+43-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ SpellToolbarView = require './SpellToolbarView'
1010
LevelComponent = require 'models/LevelComponent'
1111
UserCodeProblem = require 'models/UserCodeProblem'
1212
utils = require 'core/utils'
13+
CodeLog = require 'models/CodeLog'
1314

1415
module.exports = class SpellView extends CocoView
1516
id: 'spell-view'
@@ -47,6 +48,8 @@ module.exports = class SpellView extends CocoView
4748
'tome:maximize-toggled': 'onMaximizeToggled'
4849
'script:state-changed': 'onScriptStateChange'
4950
'playback:ended-changed': 'onPlaybackEndedChanged'
51+
'level:contact-button-pressed': 'onContactButtonPressed'
52+
'level:show-victory': 'onShowVictory'
5053

5154
events:
5255
'mouseout': 'onMouseOut'
@@ -63,7 +66,6 @@ module.exports = class SpellView extends CocoView
6366
@highlightCurrentLine = _.throttle @highlightCurrentLine, 100
6467
$(window).on 'resize', @onWindowResize
6568
@observing = @session.get('creator') isnt me.id
66-
6769
afterRender: ->
6870
super()
6971
@createACE()
@@ -106,6 +108,14 @@ module.exports = class SpellView extends CocoView
106108
$(@ace.container).find('.ace_gutter').on 'click', @onGutterClick
107109
@initAutocomplete aceConfig.liveCompletion ? true
108110

111+
return if @session.get('creator') isnt me.id or @session.fake
112+
# Create a Spade to 'dig' into Ace.
113+
@spade = new Spade()
114+
@spade.track(@ace)
115+
# If a user is taking longer than 10 minutes, let's log it.
116+
saveSpadeDelay = 10 * 60 * 1000
117+
@saveSpadeTimeout = setTimeout @saveSpade, saveSpadeDelay
118+
109119
createACEShortcuts: ->
110120
@aceCommands = aceCommands = []
111121
ace = @ace
@@ -636,6 +646,9 @@ module.exports = class SpellView extends CocoView
636646
onMouseOut: (e) ->
637647
@debugView?.onMouseOut e
638648

649+
onContactButtonPressed: (e) ->
650+
@saveSpade()
651+
639652
getSource: ->
640653
@ace.getValue() # could also do @firepad.getText()
641654

@@ -705,6 +718,33 @@ module.exports = class SpellView extends CocoView
705718
return if @destroyed
706719
Backbone.Mediator.publish 'tome:hide-problem-alert', {}
707720

721+
saveSpade: =>
722+
return if @destroyed
723+
spadeEvents = @spade.compile()
724+
# Uncomment the below line for a debug panel to display inside the level
725+
#@spade.debugPlay(spadeEvents)
726+
condensedEvents = @spade.condense(spadeEvents)
727+
728+
return unless condensedEvents.length
729+
compressedEvents = LZString.compressToUTF16(JSON.stringify(condensedEvents))
730+
731+
codeLog = new CodeLog({
732+
sessionID: @options.session.id
733+
level:
734+
original: @options.level.get 'original'
735+
majorVersion: (@options.level.get 'version').major
736+
levelSlug: @options.level.get 'slug'
737+
userID: @options.session.get 'creator'
738+
log: compressedEvents
739+
})
740+
741+
codeLog.save()
742+
743+
onShowVictory: (e) ->
744+
if @saveSpadeTimeout?
745+
window.clearTimeout @saveSpadeTimeout
746+
@saveSpadeTimeout = null
747+
708748
onManualCast: (e) ->
709749
cast = @$el.parent().length
710750
@recompile cast, e.realTime
@@ -1299,6 +1339,8 @@ module.exports = class SpellView extends CocoView
12991339
@toolbarView?.destroy()
13001340
@zatanna.addSnippets [], @editorLang if @editorLang?
13011341
$(window).off 'resize', @onWindowResize
1342+
window.clearTimeout @saveSpadeTimeout
1343+
@saveSpadeTimeout = null
13021344
super()
13031345

13041346
commentStarts =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Print out code language usage based on level session data
2+
3+
// Usage:
4+
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
5+
6+
7+
var startDate = new Date();
8+
startDate.setUTCDate(startDate.getUTCDate() - 7);
9+
var startDay = startDate.toISOString(0, 10);
10+
11+
const endDate = new Date();
12+
endDate.setUTCDate(endDate.getUTCDate());
13+
var endDay = endDate.toISOString().substr(0, 10);
14+
15+
var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
16+
var endObj = objectIdWithTimestamp(ISODate(endDay + "T00:00:00.000Z"))
17+
18+
var query = {
19+
$and:[
20+
{_id:{$gte:startObj}},
21+
{_id:{$lt:endObj}}
22+
]
23+
}
24+
var cursor = db.level.sessions.find(query, {playtime:1});
25+
var count = 0;
26+
var total = 0;
27+
28+
//Probably a built-in Mongo thing to do this... But it's not slow, so...
29+
while(cursor.hasNext()) {
30+
result = cursor.next();
31+
if(result.playtime >= 60 * 10) {
32+
count++;
33+
}
34+
total++;
35+
}
36+
37+
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);
38+
39+
function objectIdWithTimestamp(timestamp) {
40+
// Convert string date to Date object (otherwise assume timestamp is a date)
41+
if (typeof(timestamp) == 'string') timestamp = new Date(timestamp);
42+
// Convert date object to hex seconds since Unix epoch
43+
const hexSeconds = Math.floor(timestamp/1000).toString(16);
44+
// Create an ObjectId with that hex timestamp
45+
const constructedObjectId = ObjectId(hexSeconds + "0000000000000000");
46+
return constructedObjectId
47+
}

‎server/middleware/auth.coffee

+7-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ module.exports =
3737
if not _.size(_.intersection(req.user.get('permissions'), permissions))
3838
return next new errors.Forbidden('You do not have permissions necessary.')
3939
next()
40-
40+
41+
checkHasUser: ->
42+
return (req, res, next) ->
43+
if not req.user
44+
return next new errors.Unauthorized('No user associated with this request.')
45+
next()
46+
4147
whoAmI: wrap (req, res) ->
4248
if not req.user
4349
user = User.makeNew(req)

‎server/middleware/codelogs.coffee

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
errors = require '../commons/errors'
2+
wrap = require 'co-express'
3+
database = require '../commons/database'
4+
5+
mongoose = require 'mongoose'
6+
CodeLog = require '../models/CodeLog'
7+
LevelSession = require '../models/LevelSession'
8+
9+
module.exports =
10+
post: wrap (req, res) ->
11+
codeLog = database.initDoc(req, CodeLog)
12+
database.assignBody(req, codeLog)
13+
database.validateDoc(codeLog)
14+
codeLog = yield codeLog.save()
15+
16+
# Update the level session with sessionID to include the new codelog.
17+
yield LevelSession.update(
18+
{_id: mongoose.Types.ObjectId(req.body.sessionID)},
19+
{$push:{codeLogs: codeLog._id}}
20+
)
21+
22+
res.status(201).send(codeLog.toObject())

‎server/middleware/index.coffee

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports =
33
auth: require './auth'
44
classrooms: require './classrooms'
55
campaigns: require './campaigns'
6+
codelogs: require './codelogs'
67
courseInstances: require './course-instances'
78
files: require './files'
89
named: require './named'

‎server/models/CodeLog.coffee

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
mongoose = require 'mongoose'
2+
config = require '../../server_config'
3+
4+
CodeLogSchema = new mongoose.Schema({
5+
created:
6+
type: Date
7+
default: Date.now
8+
userID:
9+
type: mongoose.Schema.ObjectId
10+
sessionID:
11+
type: mongoose.Schema.ObjectId
12+
level:
13+
original:
14+
type: mongoose.Schema.ObjectId
15+
majorVersion:
16+
type: Number
17+
default: 0
18+
}, {strict: false, read: config.mongo.readpref})
19+
20+
CodeLogSchema.index({levelSlug: 1, created: -1}, {name: 'level slug index'})
21+
CodeLogSchema.index({userID: 1, created: -1}, {name: 'user id index'})
22+
23+
CodeLogSchema.statics.editableProperties = [
24+
'sessionID'
25+
'level'
26+
'levelSlug'
27+
'userID'
28+
'log'
29+
'created'
30+
]
31+
32+
CodeLogSchema.statics.jsonSchema = require '../../app/schemas/models/codelog.schema'
33+
34+
module.exports = CodeLog = mongoose.model('CodeLog', CodeLogSchema, 'codelogs')

‎server/routes/index.coffee

+5-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ module.exports.setup = (app) ->
4949
app.get('/db/classroom/:handle/member-sessions', mw.classrooms.fetchMemberSessions)
5050
app.get('/db/classroom/:handle/members', mw.classrooms.fetchMembers) # TODO: Use mw.auth?
5151
app.get('/db/classroom/:handle', mw.auth.checkLoggedIn()) # TODO: Finish migrating route, adding now so 401 is returned
52-
52+
53+
CodeLog = require ('../models/CodeLog')
54+
app.post('/db/codelogs', mw.auth.checkHasUser(), mw.codelogs.post)
55+
app.get('/db/codelogs', mw.auth.checkHasPermission(['admin']), mw.rest.get(CodeLog))
56+
5357
Course = require '../models/Course'
5458
app.get('/db/course', mw.rest.get(Course))
5559
app.get('/db/course/:handle', mw.rest.getByHandle(Course))
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
require '../common'
2+
utils = require '../utils'
3+
4+
Promise = require 'bluebird'
5+
request = require '../request'
6+
requestAsync = Promise.promisify(request, {multiArgs: true})
7+
8+
CodeLog = require '../../../server/models/CodeLog'
9+
User = require '../../../server/models/User'
10+
11+
testLog1 = {
12+
'sessionID': ObjectId("55b29efd1cd6abe8ce07db0d")
13+
'level': {
14+
'original': ObjectId("55b29efd1cd6abe8ce07db0d")
15+
'majorVersion': 0
16+
}
17+
'levelSlug': "d"
18+
'userID': ObjectId("55b29efd1cd6abe8ce07db0d")
19+
'userName': "b"
20+
'log': "a"
21+
}
22+
23+
testLog2 = {
24+
'sessionID': ObjectId("55b29efd1cd6abe8ce07db0d")
25+
'level': {
26+
'original': ObjectId("55b29efd1cd6abe8ce07db0d")
27+
'majorVersion': 0
28+
}
29+
'levelSlug': "dbbb"
30+
'userID': ObjectId("55b29efd1cd6abe8ce07db0d")
31+
'userName': "bbbb"
32+
'log': "abbb"
33+
}
34+
35+
describe 'POST /db/codelogs', ->
36+
beforeEach utils.wrap (done) ->
37+
yield utils.clearModels([CodeLog])
38+
user = yield utils.initUser({})
39+
yield utils.loginUser(user)
40+
done()
41+
it 'allows logged in users to create codelogs', utils.wrap (done) ->
42+
[res, body] = yield request.postAsync {
43+
uri: getURL('/db/codelogs'), json: testLog1
44+
}
45+
expect(res.statusCode).toBe(201)
46+
done()
47+
it 'does allow anonymous users to create codelogs', utils.wrap (done) ->
48+
yield utils.becomeAnonymous()
49+
[res, body] = yield request.postAsync {
50+
uri: getURL('/db/codelogs'), json: testLog1
51+
}
52+
expect(res.statusCode).toBe(201)
53+
done()
54+
it 'does not allow unauthenticated users to create codelogs', utils.wrap (done) ->
55+
yield utils.logout()
56+
[res, body] = yield request.postAsync {
57+
uri: getURL('/db/codelogs'), json: testLog1
58+
}
59+
expect(res.statusCode).toBe(401)
60+
done()
61+
62+
describe 'GET /db/codelogs', ->
63+
beforeEach utils.wrap (done) ->
64+
yield utils.clearModels([CodeLog])
65+
# Fill database
66+
@admin = yield utils.initAdmin({})
67+
yield utils.loginUser(@admin)
68+
yield request.postAsync(getURL('/db/codelogs'), {json: testLog1})
69+
yield request.postAsync(getURL('/db/codelogs'), {json: testLog2})
70+
yield utils.logout()
71+
done()
72+
73+
it 'does not allow unauthenticated users to get codelogs', utils.wrap (done) ->
74+
[res, body] = yield request.getAsync {uri:getURL('/db/codelogs'), json:true}
75+
expect(res.statusCode).toBe(401)
76+
done()
77+
78+
it 'does not allow non-admins to get codelogs', utils.wrap (done) ->
79+
user = yield utils.initUser({})
80+
yield utils.loginUser(user)
81+
[res, body] = yield request.getAsync {uri:getURL('/db/codelogs'), json:true}
82+
expect(res.statusCode).toBe(403)
83+
done()
84+
85+
it 'allows admins to get codelogs', utils.wrap (done) ->
86+
admin = yield utils.initAdmin({})
87+
yield utils.loginUser(admin)
88+
[res, body] = yield request.getAsync {uri:getURL('/db/codelogs'), json:true}
89+
expect(body.length).toBe(2)
90+
done()

‎sublime-project.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"folders":[
3+
{
4+
"path":".",
5+
"folder_exclude_patterns":[
6+
"bower_components",
7+
"public",
8+
"node_modules"
9+
]
10+
}
11+
]
12+
}

‎vendor/scripts/spade.js

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
var Spade = function Spade() {
2+
this.stack = [];
3+
}
4+
Spade.prototype = {
5+
track: function(_elem) {
6+
this.target = _elem;
7+
8+
var spade = this;
9+
10+
var el = document.createElement("div");
11+
12+
keyHook = null;
13+
if(_elem.textInput && _elem.textInput.getElement) {
14+
keyHook = _elem.textInput.getElement();
15+
} else {
16+
keyHook = _elem;
17+
}
18+
keyHook.addEventListener("keydown", function(_event) {spade.createEvent(spade.target)});
19+
//Maybe this is needed depending on Firefox/other browsers? Duplicate non-diff events get compiled down.
20+
keyHook.addEventListener("keyup", function(_event) {spade.createEvent(spade.target)});
21+
_elem.addEventListener("mouseup", function(_event) {spade.createEvent(spade.target)});
22+
},
23+
createEvent: function(_target) {
24+
if(_target.getValue) {
25+
this.stack.push({
26+
"startPos":_target.selection.getCursor(),
27+
"endPos":_target.selection.getSelectionAnchor(),
28+
"content":_target.getValue(),
29+
"timestamp":(new Date()).getTime()
30+
});
31+
} else {
32+
this.stack.push({
33+
"startPos":_target.selectionStart,
34+
"endPos":_target.selectionEnd,
35+
"content":_target.value,
36+
"timestamp":(new Date()).getTime()
37+
});
38+
}
39+
},
40+
compile: function() {
41+
var compiledStack = [];
42+
if(this.stack.length > 0) {
43+
var startTime = this.stack[0].timestamp;
44+
var sum = 0;
45+
var sum2 = 0;
46+
for(var i = 0; i < this.stack.length; i++) {
47+
var c = this.stack[i];
48+
var adjustedTimestamp = c.timestamp - startTime;
49+
50+
var tString = ""; //The changed string.
51+
var fIndex = null; //The first index of changes.
52+
var eIndex = null; //The last index of changes.
53+
var dCount = 0; //Amount of character changes.
54+
if(i >= 1) {
55+
var p = this.stack[i - 1];
56+
var isOkay = false;
57+
for(var key in p) {
58+
if(key != "timestamp") {
59+
if(typeof p[key] === "string") {
60+
if(p[key] !== c[key]) {
61+
isOkay = true;
62+
}
63+
} else {
64+
for(var key2 in p[key]) {
65+
if(c[key][key2] !== undefined) {
66+
if(p[key][key2] !== c[key][key2]) {
67+
isOkay = true;
68+
}
69+
} else {
70+
console.warn("Warning: c[key][key2] doesn't exist, but p[key][key2] does.");
71+
isOkay = true;
72+
}
73+
}
74+
}
75+
}
76+
}
77+
if(!isOkay) {
78+
sum2++;
79+
continue;
80+
}
81+
sum++;
82+
if(p.content != c.content) {
83+
//Check from the start to the end, which characters are different.
84+
for(var j = 0; j < Math.max(p.content.length, c.content.length); j++) {
85+
if(p.content.charAt(j) === c.content.charAt(j)) {
86+
if(fIndex != null) {
87+
tString += c.content.charAt(j);
88+
dCount++;
89+
}
90+
} else {
91+
tString += c.content.charAt(j);
92+
if(fIndex === null) {
93+
fIndex = j;
94+
}
95+
dCount++;
96+
}
97+
}
98+
//Check from the end to the start, which characters are different.
99+
for(var j = 0; j < Math.min(p.content.length, c.content.length) - fIndex; j++) {
100+
if(p.content.charAt(p.content.length - 1 - j) !== c.content.charAt(c.content.length - 1 - j)) {
101+
if(eIndex == null) {
102+
eIndex = j;
103+
break;
104+
}
105+
}
106+
}
107+
//This accounts for the fact when changing from "aa" to "aaa" (for example).
108+
if(eIndex === null) {
109+
eIndex = Math.min(p.content.length, c.content.length) - fIndex;
110+
}
111+
tString = tString.substring(0, tString.length - eIndex);
112+
}
113+
} else {
114+
tString = c.content;
115+
fIndex = 0;
116+
eIndex = tString.length;
117+
}
118+
compiledStack.push({
119+
"timestamp":adjustedTimestamp,
120+
"difContent":tString,
121+
"difFIndex":fIndex,
122+
"difEIndex":eIndex,
123+
"selFIndex":c.startPos,
124+
"selEIndex":c.endPos
125+
});
126+
}
127+
} else {
128+
//Just return the empty array.
129+
}
130+
return compiledStack;
131+
},
132+
play: function(_stack, _elem) {
133+
if(_stack.length === 0) {
134+
console.warn("SPADE: No events to play.")
135+
return
136+
}
137+
if(_elem.setValue) {
138+
_elem.setValue(_stack[0].difContent);
139+
} else {
140+
_elem.value = _stack[0].difContent
141+
}
142+
_stack = _stack.slice();
143+
_stack.shift();
144+
var curTime, dTime;
145+
var elapsedTime = 0;
146+
var prevTime = (new Date()).getTime();
147+
var playbackInterval = setInterval(function() {
148+
curTime = (new Date()).getTime();
149+
dTime = curTime - prevTime;
150+
dTime *= 1; //Multiply for faster/slower playback speeds.
151+
elapsedTime += dTime;
152+
var tArray = _stack.filter(function(_event) {
153+
return ((_event.timestamp) >= (elapsedTime - dTime)) && ((_event.timestamp) < (elapsedTime));
154+
});
155+
for(var i = 0; i < tArray.length; i++) {
156+
var tEvent = tArray[i];
157+
var oVal = null;
158+
if(_elem.getValue) {
159+
oVal = _elem.getValue();
160+
} else {
161+
oVal = _elem.value;
162+
}
163+
if(tEvent.difFIndex !== null && tEvent.difEIndex !== null) {
164+
if(_elem.setValue) {
165+
_elem.setValue(oVal.substring(0, tEvent.difFIndex) + tEvent.difContent + oVal.substring(oVal.length - tEvent.difEIndex, oVal.length));
166+
} else {
167+
_elem.value = oVal.substring(0, tEvent.difFIndex) + tEvent.difContent + oVal.substring(oVal.length - tEvent.difEIndex, oVal.length)
168+
}
169+
}
170+
if(_elem.selection && _elem.selection.moveCursorToPosition) {
171+
//Maybe this will work someday
172+
_elem.selection.moveCursorToPosition(tEvent.selFIndex);
173+
_elem.selection.setSelectionAnchor(tEvent.selEIndex.row, tEvent.selEIndex.column);
174+
_elem.selection.selectTo(tEvent.selFIndex.row, tEvent.selFIndex.column);
175+
} else {
176+
//Likewise
177+
_elem.focus();
178+
_elem.setSelectionRange(tEvent.selFIndex, tEvent.selEIndex);
179+
}
180+
}
181+
if(_stack[_stack.length - 1] === undefined || elapsedTime > _stack[_stack.length - 1].timestamp) {
182+
clearInterval(playbackInterval);
183+
}
184+
prevTime = curTime;
185+
}, 10);
186+
},
187+
debugPlay: function(_stack) {
188+
var area = document.createElement('textarea');
189+
area.zIndex = 9999;
190+
area.style.width = "512px";
191+
area.style.height = "512px";
192+
area.style.position = "absolute";
193+
area.style.left = "100px";
194+
area.style.top = "100px";
195+
document.body.appendChild(area);
196+
this.play(_stack, area);
197+
},
198+
condense: function(_stack) {
199+
var compressedArray = [];
200+
for(var i = 0; i < _stack.length; i++) {
201+
var u = _stack[i];
202+
compressedArray.push([
203+
u.timestamp,
204+
u.difContent,
205+
u.difFIndex,
206+
u.difEIndex,
207+
u.selFIndex.row,
208+
u.selFIndex.column,
209+
u.selEIndex.row,
210+
u.selEIndex.column
211+
]);
212+
}
213+
return compressedArray;
214+
},
215+
expand: function(_array) {
216+
var uncompressedArray = [];
217+
for(var i = 0 ; i < _array.length; i++) {
218+
var c = _array[i];
219+
uncompressedArray.push({
220+
"timestamp":c[0],
221+
"difContent":c[1],
222+
"difFIndex":c[2],
223+
"difEIndex":c[3],
224+
"selFIndex":{
225+
"row":c[4],
226+
"column":c[5]
227+
},
228+
"selEIndex":{
229+
"row":c[6],
230+
"column":c[7]
231+
},
232+
});
233+
}
234+
return uncompressedArray;
235+
}
236+
}

0 commit comments

Comments
 (0)
Please sign in to comment.