2013-12-05 18:40:41 +02:00
|
|
|
/**
|
|
|
|
* Copyright 2009 Google Inc.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
2013-12-13 11:59:17 +02:00
|
|
|
require('./jquery.class');
|
|
|
|
var libchangeset = require("./Changeset");
|
2013-12-12 17:29:04 +02:00
|
|
|
|
2013-12-24 19:37:22 +02:00
|
|
|
function log()
|
|
|
|
console.log.apply(console, arguments);
|
|
|
|
}
|
|
|
|
|
2013-12-05 18:40:41 +02:00
|
|
|
$.Class("Changeset",
|
|
|
|
{//statics
|
|
|
|
},
|
|
|
|
{//instance
|
2013-12-12 16:52:30 +02:00
|
|
|
init: function (from_revision, to_revision, deltatime, value) {
|
|
|
|
this.from_revision = from_revision;
|
|
|
|
this.to_revision = to_revision;
|
2013-12-05 18:40:41 +02:00
|
|
|
this.deltatime = deltatime;
|
|
|
|
this.value = value;
|
|
|
|
},
|
|
|
|
getValue: function () {
|
|
|
|
return this.value;
|
|
|
|
},
|
2013-12-16 13:59:07 +02:00
|
|
|
compose: function (other, pad) {
|
|
|
|
var newvalue = libchangeset.compose(this.value, other.value, pad.apool);
|
|
|
|
var newchangeset = new Changeset(this.from_revision, other.to_revision,
|
|
|
|
this.deltatime + other.deltatime, newvalue);
|
|
|
|
//TODO: insert new changeset into the graph somehow.
|
|
|
|
return newchangeset;
|
|
|
|
},
|
2013-12-13 11:59:17 +02:00
|
|
|
/**
|
|
|
|
* Apply this changeset to the passed pad.
|
|
|
|
* @param {PadClient} pad - The pad to apply the changeset to.
|
|
|
|
*/
|
|
|
|
apply: function (pad) {
|
|
|
|
// must mutate attribution lines before text lines
|
|
|
|
libchangeset.mutateAttributionLines(this.value, pad.alines, pad.apool);
|
|
|
|
|
|
|
|
// Looks like this function can take a regular array of strings
|
|
|
|
libchangeset.mutateTextLines(this.value, /* padcontents */ /*this.lines */ pad.divs);
|
|
|
|
},
|
2013-12-12 16:52:30 +02:00
|
|
|
/**
|
|
|
|
* 'Follow' the Changeset in a given direction, returning the revision at
|
|
|
|
* the specified end of the edge.
|
|
|
|
* @param {bool} direction - If true, go to the 'from' revision, otherwise
|
|
|
|
* go to the 'to' revision.
|
|
|
|
* @returns {Revision}
|
|
|
|
*/
|
2013-12-12 17:29:04 +02:00
|
|
|
follow: function () {
|
2013-12-12 16:52:30 +02:00
|
|
|
return this.to_revision;
|
|
|
|
}
|
2013-12-05 18:40:41 +02:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2013-12-10 19:00:17 +02:00
|
|
|
/**
|
|
|
|
* Revision class. Represents a specific revision. Each instance has three
|
|
|
|
* possible edges in each direction. Each edge is essentially a Changeset.
|
|
|
|
* We store three edges at different granularities, to make skipping fast.
|
|
|
|
* e.g. to go from r1 to r251, you start at r1, use the big edge to go to
|
|
|
|
* r100, use the big edge again to go to r200, use the next 5 medium edges to
|
|
|
|
* go from r200 to r210, etc. until reaching r250, follow the next small edge
|
|
|
|
* to get to 251. A total of 8 edges are traversed (a.k.a. applied),
|
|
|
|
* making this significantly cheaper than applying all 250 changesets from r1
|
|
|
|
* to r251.
|
|
|
|
*/
|
2013-12-12 16:52:30 +02:00
|
|
|
$.Class("Revision",
|
2013-12-10 19:00:17 +02:00
|
|
|
{//statics
|
|
|
|
// we rely on the fact that granularities are always traversed biggest to
|
|
|
|
// smallest. Changing this will break lots of stuff.
|
2013-12-22 18:21:54 +02:00
|
|
|
granularities: {huge: 1000, big: 100, medium: 10, small: 1}
|
2013-12-10 19:00:17 +02:00
|
|
|
},
|
|
|
|
{//instance
|
|
|
|
/**
|
|
|
|
* Create a new revision for the specified revision number.
|
|
|
|
* @constructor
|
|
|
|
* @param {number} revnum - The revision number this object represents.
|
|
|
|
*/
|
|
|
|
init: function (revnum) {
|
|
|
|
this.revnum = revnum;
|
|
|
|
// next/previous edges, granularityed as big, medium and small
|
|
|
|
this.next = {};
|
|
|
|
this.previous = {};
|
|
|
|
for (var granularity in this.granularties) {
|
|
|
|
this.next[granularity] = null;
|
|
|
|
this.previous[granularity] = null;
|
2013-12-12 13:43:33 +02:00
|
|
|
}
|
2013-12-10 19:00:17 +02:00
|
|
|
},
|
2013-12-12 16:52:30 +02:00
|
|
|
/**
|
|
|
|
* Add a changeset from this revision to the target.
|
|
|
|
* @param {Revision} target - The target revision.
|
|
|
|
* @param {object} changeset - The raw changeset data.
|
|
|
|
* @param {time} timedelta - The difference in time between this revision
|
|
|
|
* and the target.
|
|
|
|
* @returns {Changeset} - The new changeset object.
|
|
|
|
*/
|
|
|
|
addChangeset: function (target, changeset, timedelta) {
|
2013-12-10 19:00:17 +02:00
|
|
|
if (this.revnum == target.revnum)
|
2013-12-12 16:52:30 +02:00
|
|
|
// This should really never happen, but if it does, let's short-circuit.
|
2013-12-10 19:00:17 +02:00
|
|
|
return;
|
|
|
|
|
|
|
|
var delta_revnum = target.revnum - this.revnum;
|
|
|
|
// select the right edge set:
|
2013-12-12 16:52:30 +02:00
|
|
|
var direction_edges = delta_revnum < 0 ? this.previous : this.next;
|
2013-12-10 19:00:17 +02:00
|
|
|
|
|
|
|
// find the correct granularity and add an edge (changeset) for that granularity
|
2013-12-12 16:52:30 +02:00
|
|
|
for (var granularity in Revision.granularities) {
|
|
|
|
if (Math.abs(delta_revnum) == Revision.granularities[granularity]) {
|
2013-12-10 19:00:17 +02:00
|
|
|
//TODO: should we check whether the edge exists?
|
2013-12-12 16:52:30 +02:00
|
|
|
direction_edges[granularity] = new Changeset(this, target, timedelta, changeset);
|
|
|
|
return direction_edges[granularity];
|
2013-12-12 13:43:33 +02:00
|
|
|
}
|
|
|
|
}
|
2013-12-10 19:00:17 +02:00
|
|
|
// our delta_revnum isn't one of the granularities. Something is wrong
|
|
|
|
//TODO: handle this case?
|
|
|
|
return null;
|
|
|
|
},
|
2013-12-05 18:40:41 +02:00
|
|
|
lt: function (other, is_reverse) {
|
|
|
|
if (is_reverse)
|
|
|
|
return this.gt(other);
|
|
|
|
return this.revnum < other.revnum;
|
|
|
|
},
|
|
|
|
gt: function (other, is_reverse) {
|
|
|
|
if (is_reverse)
|
|
|
|
return this.lt(other);
|
|
|
|
return this.revnum > other.revnum;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
$.Class("RevisionCache",
|
|
|
|
{
|
2013-12-24 19:30:46 +02:00
|
|
|
VERBOSE: false,
|
2013-12-05 18:40:41 +02:00
|
|
|
},
|
|
|
|
{//instance
|
2013-12-10 17:57:16 +02:00
|
|
|
/**
|
|
|
|
* Create a new RevisionCache.
|
|
|
|
* @constructor
|
|
|
|
* @param {TimesliderClient} connection - The connection to be used for loading changesets.
|
|
|
|
* @param {number} head_revnum - The current head revision number. TODO: we can probably do away with this now.
|
|
|
|
*/
|
|
|
|
init: function (connection, head_revnum) {
|
2013-12-24 19:37:22 +02:00
|
|
|
this.log = RevisionCache.VERBOSE ? log : function () {};
|
2013-12-10 17:57:16 +02:00
|
|
|
this.connection = connection;
|
|
|
|
this.loader = new ChangesetLoader(connection);
|
2013-12-05 18:40:41 +02:00
|
|
|
this.revisions = {};
|
2013-12-12 16:52:30 +02:00
|
|
|
this.head_revision = this.getRevision(head_revnum || 0);
|
|
|
|
this.loader.start();
|
2013-12-05 18:40:41 +02:00
|
|
|
},
|
2013-12-21 19:25:25 +02:00
|
|
|
/**
|
|
|
|
* Get the head revision.
|
|
|
|
* @returns {Revision} - the head revision.
|
|
|
|
*/
|
|
|
|
getHeadRevision: function () {
|
|
|
|
return this.head_revision;
|
|
|
|
},
|
2013-12-12 16:52:30 +02:00
|
|
|
/**
|
|
|
|
* Get a Revision instance for the specified revision number.
|
|
|
|
* If we don't yet have a Revision instance for this revision, create a
|
|
|
|
* new one. Also make sure that the head_revision attribute always refers
|
|
|
|
* to the instance for the pad's head revision.
|
|
|
|
* @param {number} revnum - The revision number for which we want a
|
|
|
|
* Revision object.
|
|
|
|
* @returns {Revision}
|
|
|
|
*/
|
2013-12-05 18:40:41 +02:00
|
|
|
getRevision: function (revnum) {
|
|
|
|
if (revnum in this.revisions)
|
|
|
|
return this.revisions[revnum];
|
2013-12-12 16:52:30 +02:00
|
|
|
var revision = new Revision(revnum);
|
|
|
|
this.revisions[revnum] = revision;
|
|
|
|
if (this.head_revision && revnum > this.head_revision.revnum) {
|
|
|
|
this.head_revision = revision;
|
|
|
|
}
|
|
|
|
return revision;
|
2013-12-05 18:40:41 +02:00
|
|
|
},
|
2013-12-21 19:25:25 +02:00
|
|
|
/**
|
|
|
|
* Add a new revision at the head.
|
|
|
|
* @param {number} revnum - the revision number of the new revision.
|
|
|
|
* @param {string} forward - the forward changeset to get here from previous head.
|
|
|
|
* @param {string} reverse - the reverse changeset.
|
|
|
|
* @param {string} timedelta - the time difference.
|
|
|
|
*/
|
|
|
|
appendHeadRevision: function (revnum, forward, reverse, timedelta) {
|
|
|
|
this.addChangesetPair(this.head_revision.revnum, revnum, forward, reverse, timedelta);
|
|
|
|
this.head_revision = this.getRevision(revnum);
|
|
|
|
//TODO: render it if we are currently at the head_revision?
|
|
|
|
},
|
2013-12-12 16:52:30 +02:00
|
|
|
/**
|
|
|
|
* Links two revisions, specified by from and to with the changeset data
|
|
|
|
* in value and reverseValue respectively.
|
|
|
|
* @param {number} from - The revision number from which the forward
|
|
|
|
* changeset originates.
|
|
|
|
* @param {number} to - The revision number to which the forward changeset
|
|
|
|
* bring us.
|
|
|
|
* @param {changeset} forwardValue - The forward changeset data.
|
|
|
|
* @param {changeset} reverseValue - The reverse changeset data.
|
|
|
|
* @param {time} timedelta - The difference in time between the from and
|
|
|
|
* to revisions.
|
|
|
|
*/
|
|
|
|
addChangesetPair: function (from, to, value, reverseValue, timedelta) {
|
|
|
|
var from_rev = this.getRevision(from);
|
2013-12-05 18:40:41 +02:00
|
|
|
var to_rev = this.getRevision(to);
|
2013-12-12 16:52:30 +02:00
|
|
|
from_rev.addChangeset(to_rev, value, timedelta);
|
|
|
|
to_rev.addChangeset(from_rev, reverseValue, -timedelta);
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Find a (minimal) path from a given revision to another revision. If a
|
|
|
|
* complete path cannot be found, return a path which comes as close as
|
|
|
|
* possible to the to revision.
|
|
|
|
* @param {Revision} from - The revision from which to start.
|
|
|
|
* @param {Revision} to - The revision which the path should try to reach.
|
|
|
|
* @returns {object} - A list of Changesets which describe a (partial) path
|
|
|
|
* from 'from' to 'to', and the last revision reached.
|
|
|
|
*/
|
|
|
|
findPath: function (from, to) {
|
|
|
|
/*
|
|
|
|
*TODO: currently we only ever move in the direction of sign(to-from).
|
|
|
|
*It might be worth implementing 'jitter' movements, so that if you,
|
|
|
|
*for example, you are trying to go from 0 to 99, and you have the
|
|
|
|
*following edges:
|
|
|
|
* 0 -> 100
|
|
|
|
* 100 -> 99
|
|
|
|
*The algorithm would be smart enough to provide you with that as a path
|
|
|
|
*/
|
|
|
|
var path = [];
|
|
|
|
var found_discontinuity = false;
|
|
|
|
var current = from;
|
2013-12-12 17:29:04 +02:00
|
|
|
var direction = (to.revnum - from.revnum) < 0;
|
2013-12-17 16:54:37 +02:00
|
|
|
var granularity = 0;
|
|
|
|
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("[findpath] from: %d, to: %d", from.revnum, to.revnum);
|
2013-12-12 16:52:30 +02:00
|
|
|
while (current.lt(to, direction) && !found_discontinuity) {
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("\t[findPath] while current: ", current.revnum);
|
2013-12-12 16:52:30 +02:00
|
|
|
var delta_revnum = to.revnum - current.revnum;
|
|
|
|
var direction_edges = direction ? current.previous : current.next;
|
2013-12-17 16:54:37 +02:00
|
|
|
for (granularity in Revision.granularities) {
|
2013-12-12 16:52:30 +02:00
|
|
|
if (Math.abs(delta_revnum) >= Revision.granularities[granularity]) {
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("\t\t[findPath] for delta: %d, granularity: %d", delta_revnum, Revision.granularities[granularity]);
|
2013-12-12 16:52:30 +02:00
|
|
|
/*
|
|
|
|
* the delta is larger than the granularity, let's use the granularity
|
|
|
|
*TODO: what happens if we DON'T have the edge?
|
|
|
|
* in theory we need to fetch it (and this is certainly the case for playback
|
|
|
|
* at granularity = 1). However, when skipping, we might try to find the NEXT
|
|
|
|
* Revision (which is not linked by the graph to current) and request revisions
|
|
|
|
* from current to that Revision (at the largest possible granularity)
|
|
|
|
*/
|
|
|
|
var edge = direction_edges[granularity];
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("\t\t[findpath] edge:", edge);
|
2013-12-12 16:52:30 +02:00
|
|
|
if (edge) {
|
|
|
|
// add this edge to our path
|
|
|
|
path.push(edge);
|
|
|
|
// follow the edge to the next Revision node
|
2013-12-12 17:29:04 +02:00
|
|
|
current = edge.follow();
|
2013-12-12 16:52:30 +02:00
|
|
|
// no need to look for smaller granularities
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
// we don't have an edge. Normally we can just continue to the
|
|
|
|
// next granularity level. BUT, if we are at the lowest
|
|
|
|
// granularity and don't have an edge, we've reached a DISCONTINUITY
|
|
|
|
// and can no longer continue.
|
2013-12-12 20:29:29 +02:00
|
|
|
if (Revision.granularities[granularity] == 1)
|
|
|
|
found_discontinuity = true;
|
2013-12-12 16:52:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-12-05 18:40:41 +02:00
|
|
|
}
|
2013-12-12 17:29:04 +02:00
|
|
|
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("[findpath] ------------------");
|
2013-12-12 16:52:30 +02:00
|
|
|
// return either a full path, or a path ending as close as we can get to
|
|
|
|
// the target revision.
|
2013-12-17 16:54:37 +02:00
|
|
|
return {path: path, end_revision: current, granularity: granularity};
|
2013-12-12 16:52:30 +02:00
|
|
|
},
|
|
|
|
//TODO: horrible name!
|
|
|
|
transition: function (from_revnum, to_revnum, applyChangeset_callback) {
|
|
|
|
var path = [];
|
|
|
|
var current_revision = this.getRevision(from_revnum);
|
|
|
|
var target_revision = this.getRevision(to_revnum);
|
|
|
|
|
2013-12-12 19:52:34 +02:00
|
|
|
// For debugging:
|
|
|
|
function print_path(path) {
|
|
|
|
var res = "[";
|
|
|
|
for (var p in path) {
|
|
|
|
res += path[p].from_revision.revnum + "->" + path[p].to_revision.revnum + ", ";
|
|
|
|
}
|
|
|
|
res += "]";
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
var _this = this;
|
|
|
|
function partialTransition (current_revnum) {
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("from: %d, to: %d, current: %d", from_revnum, to_revnum, current_revnum);
|
2013-12-12 19:52:34 +02:00
|
|
|
var res = _this.findPath(_this.getRevision(from_revnum), target_revision);
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("find: ", print_path(res.path));
|
2013-12-12 19:52:34 +02:00
|
|
|
if (res.end_revision == target_revision) {
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("found: ", print_path(res.path));
|
2013-12-12 19:52:34 +02:00
|
|
|
if(applyChangeset_callback) {
|
|
|
|
applyChangeset_callback(res.path);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else {
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("end: %d, target: %d", res.end_revision.revnum, target_revision.revnum);
|
2013-12-12 16:52:30 +02:00
|
|
|
}
|
2013-12-05 18:40:41 +02:00
|
|
|
|
2013-12-12 19:52:34 +02:00
|
|
|
// we don't yet have all the changesets we need. Let's try to
|
|
|
|
// build a path from the current revision (the start of the range
|
|
|
|
// in the response) to the target.
|
|
|
|
res = _this.findPath(_this.getRevision(current_revnum), target_revision);
|
2013-12-24 19:30:46 +02:00
|
|
|
//log(print_path(res.path));
|
2013-12-12 19:52:34 +02:00
|
|
|
// we can now request changesets from the end of that partial path
|
|
|
|
// to the target:
|
2013-12-17 17:32:26 +02:00
|
|
|
_this.requestChangesets(res.end_revision, target_revision, partialTransition);
|
2013-12-05 18:40:41 +02:00
|
|
|
}
|
2013-12-12 19:52:34 +02:00
|
|
|
|
|
|
|
partialTransition(from_revnum);
|
|
|
|
|
2013-12-12 16:52:30 +02:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Request changesets which will allow transitioning from 'from' to 'to'
|
|
|
|
* from the server.
|
|
|
|
* @param {Revision} from - The start revision.
|
|
|
|
* @param {Revision} to - The end revision.
|
2013-12-12 19:52:34 +02:00
|
|
|
* @param {function} changesetsProcessed_callback - A callback triggered
|
|
|
|
* when the requested changesets have been
|
|
|
|
* received and processed (added to the graph)
|
2013-12-12 16:52:30 +02:00
|
|
|
*/
|
2013-12-17 17:32:26 +02:00
|
|
|
requestChangesets: function (from, to, changesetsProcessed_callback) {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[revisioncache] requestChangesets: %d -> %d", from.revnum, to.revnum);
|
2013-12-12 16:52:30 +02:00
|
|
|
var delta = to.revnum - from.revnum;
|
|
|
|
var sign = delta > 0 ? 1 : -1;
|
|
|
|
var start = delta > 0 ? from.revnum : to.revnum;
|
|
|
|
var end = delta > 0 ? to.revnum : from.revnum;
|
|
|
|
var adelta = Math.abs(delta);
|
2013-12-05 18:40:41 +02:00
|
|
|
|
2013-12-12 16:52:30 +02:00
|
|
|
var _this = this;
|
|
|
|
function process_received_changesets (data) {
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("[revisioncache] received changesets {from: %d, to: %d} @ granularity: %d", data.start, data.actualEndNum, data.granularity);
|
2013-12-12 16:52:30 +02:00
|
|
|
var start = data.start;
|
|
|
|
for (var i = 0; i < data.timeDeltas.length; i++, start += data.granularity) {
|
|
|
|
_this.addChangesetPair(start, start + data.granularity, data.forwardsChangesets[i], data.backwardsChangesets[i], data.timeDeltas[i]);
|
2013-12-05 19:05:08 +02:00
|
|
|
}
|
2013-12-12 19:52:34 +02:00
|
|
|
if (changesetsProcessed_callback)
|
|
|
|
changesetsProcessed_callback(data.start);
|
2013-12-05 19:05:08 +02:00
|
|
|
}
|
2013-12-10 17:57:16 +02:00
|
|
|
|
2013-12-17 17:32:26 +02:00
|
|
|
var rounddown = function (a, b) {
|
|
|
|
return Math.floor(a / b) * b;
|
|
|
|
};
|
|
|
|
var roundup = function (a, b) {
|
|
|
|
return (Math.floor(a / b)+1) * b;
|
|
|
|
};
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("[requestChangesets] start: %d, end: %d, delta: %d, adelta: %d", start, end, delta, adelta);
|
2013-12-17 17:32:26 +02:00
|
|
|
for (var g in Revision.granularities) {
|
|
|
|
var granularity = Revision.granularities[g];
|
|
|
|
var remainder = Math.floor(adelta / granularity);
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("\t[requestChangesets] start: %d, granularity: %d, adelta: %d, //: %d", start, granularity, adelta, remainder);
|
|
|
|
//log("\t rounddown delta: %d, start: %d", rounddown(adelta, granularity), rounddown(start, granularity));
|
2013-12-17 17:32:26 +02:00
|
|
|
if (remainder) {
|
|
|
|
//this.loader.enqueue(start, granularity, process_received_changesets);
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("\t[requestChangesets] REQUEST start: %d, end: %d, granularity: %d", rounddown(start, granularity), roundup(adelta, granularity), granularity);
|
2013-12-17 17:32:26 +02:00
|
|
|
this.loader.enqueue(rounddown(start, granularity), granularity, process_received_changesets);
|
2013-12-22 18:41:39 +02:00
|
|
|
// for the next granularity, we assume that we have now successfully navigated
|
|
|
|
// as far as required for this granularity. We should also make sure that only
|
|
|
|
// the significant part of the adelta is used in the next granularity.
|
|
|
|
start = rounddown(start, granularity) + rounddown(adelta, granularity);
|
|
|
|
adelta = adelta - rounddown(adelta, granularity);
|
2013-12-24 19:30:46 +02:00
|
|
|
//log("\t new start: %d, delta: %d", start, adelta);
|
2013-12-17 17:32:26 +02:00
|
|
|
}
|
|
|
|
}
|
2013-12-10 17:57:16 +02:00
|
|
|
},
|
2013-12-08 20:16:53 +02:00
|
|
|
}
|
|
|
|
);
|
2013-12-05 19:05:08 +02:00
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
$.Class("Thread",
|
|
|
|
{//statics
|
2013-12-24 19:30:46 +02:00
|
|
|
VERBOSE: true
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
|
|
|
{//instance
|
2013-12-08 20:22:14 +02:00
|
|
|
init: function (interval) {
|
|
|
|
this._is_running = false;
|
|
|
|
this._is_stopping = false;
|
2013-12-08 20:16:53 +02:00
|
|
|
this._interval_id = null;
|
2013-12-08 20:22:14 +02:00
|
|
|
this._interval = interval ? interval : 1000;
|
2013-12-24 19:37:22 +02:00
|
|
|
this.log = Thread.VERBOSE ? log : function () {};
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
|
|
|
_run: function () {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[thread] tick");
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
|
|
|
// start the run loop
|
|
|
|
start: function () {
|
|
|
|
var _this = this;
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[thread] starting");
|
2013-12-08 20:16:53 +02:00
|
|
|
var wrapper = function () {
|
2013-12-08 20:22:14 +02:00
|
|
|
if (_this._is_running && _this._is_stopping) {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[thread] shutting down");
|
2013-12-08 20:16:53 +02:00
|
|
|
clearInterval(_this._interval_id);
|
2013-12-08 20:22:14 +02:00
|
|
|
_this._is_running = false;
|
2013-12-08 20:16:53 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
_this._run.apply(_this);
|
|
|
|
};
|
2013-12-08 20:22:14 +02:00
|
|
|
this._is_running = true;
|
2013-12-09 15:58:17 +02:00
|
|
|
this._is_stopping = false;
|
|
|
|
this._interval_id = setInterval(wrapper, this._interval);
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
|
|
|
// stop the run loop
|
|
|
|
stop: function () {
|
2013-12-08 20:22:14 +02:00
|
|
|
this._is_stopping = true;
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[thread] request stop");
|
2013-12-08 20:16:53 +02:00
|
|
|
// TODO: consider finding a way to make this block
|
|
|
|
// or alternatively, having a callback which is called
|
|
|
|
// when the thread stops
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2013-12-05 19:05:08 +02:00
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
$.Class("ChangesetRequest",
|
|
|
|
{//statics
|
|
|
|
},
|
|
|
|
{//instance
|
2013-12-09 15:58:17 +02:00
|
|
|
init: function (start, granularity, callback) {
|
2013-12-24 19:37:22 +02:00
|
|
|
this.log = ChangesetLoader.VERBOSE ? log : function () {};
|
2013-12-08 20:16:53 +02:00
|
|
|
this.start = start;
|
2013-12-09 15:58:17 +02:00
|
|
|
this.granularity = granularity;
|
2013-12-12 16:52:30 +02:00
|
|
|
this.request_id = (this.granularity << 16) + this.start;
|
2013-12-09 15:58:17 +02:00
|
|
|
this.fulfill_callback = callback;
|
|
|
|
},
|
|
|
|
getRequestID: function () {
|
|
|
|
return this.request_id;
|
|
|
|
},
|
|
|
|
fulfill: function (data) {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[changesetrequest] Fulfilling request %d", this.getRequestID());
|
2013-12-09 15:58:17 +02:00
|
|
|
if (this.fulfill_callback)
|
|
|
|
this.fulfill_callback(data);
|
2013-12-08 20:16:53 +02:00
|
|
|
}
|
2013-12-09 15:58:17 +02:00
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
}
|
|
|
|
);
|
2013-12-05 18:40:41 +02:00
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
Thread("ChangesetLoader",
|
|
|
|
{//statics
|
2013-12-24 19:30:46 +02:00
|
|
|
VERBOSE: false
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
|
|
|
{//instance
|
2013-12-09 15:58:17 +02:00
|
|
|
/**
|
|
|
|
* Create a new ChangesetLoader.
|
|
|
|
* @constructor
|
2013-12-10 17:57:16 +02:00
|
|
|
* @param {TimesliderClient} connection - a TimesliderClient object to be used
|
2013-12-09 15:58:17 +02:00
|
|
|
* for communication with the server.
|
|
|
|
*/
|
2013-12-10 17:57:16 +02:00
|
|
|
init: function (connection) {
|
2013-12-16 14:31:48 +02:00
|
|
|
this._super(200);
|
2013-12-10 17:57:16 +02:00
|
|
|
this.connection = connection;
|
2013-12-22 18:21:54 +02:00
|
|
|
this.queues = {};
|
|
|
|
for (var granularity in Revision.granularities) {
|
|
|
|
this.queues[granularity] = [];
|
|
|
|
}
|
2013-12-09 15:58:17 +02:00
|
|
|
this.pending = {};
|
|
|
|
var _this = this;
|
2013-12-10 17:57:16 +02:00
|
|
|
this.connection.on("CHANGESET_REQ", function () {
|
2013-12-09 15:58:17 +02:00
|
|
|
_this.on_response.apply(_this, arguments);
|
|
|
|
});
|
2013-12-24 19:37:22 +02:00
|
|
|
this.log = ChangesetLoader.VERBOSE ? log : function () {};
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
2013-12-09 15:58:17 +02:00
|
|
|
/**
|
|
|
|
* Enqueue a request for changesets. The changesets will be retrieved
|
|
|
|
* asynchronously.
|
|
|
|
* @param {number} start - The revision from which to start.
|
2013-12-21 19:25:25 +02:00
|
|
|
*
|
2013-12-09 15:58:17 +02:00
|
|
|
* @param {number} granularity - The granularity of the changesets. If this
|
|
|
|
* is 1, the response will include changesets which
|
|
|
|
* can be applied to go from revision r to r+1.
|
|
|
|
* If 10 is specified, the resulting changesets will
|
|
|
|
* be 'condensed', so that each changeset will go from
|
|
|
|
* r to r+10.
|
|
|
|
* If any other number is specified, that granularity will
|
|
|
|
* apply.
|
|
|
|
*
|
|
|
|
* TODO: there is currently no 'END' revision implemented
|
|
|
|
* in the server. The 'END' calculated at the server is:
|
|
|
|
* start + (100 * granularity)
|
|
|
|
* We should probably fix this so that you can specify
|
|
|
|
* exact ranges. Right now, the minimum number of
|
|
|
|
* changesets/revisions you can retrieve is 100, which
|
|
|
|
* feels broken.
|
|
|
|
* @param {function} callback - A callback which will be triggered when the request has
|
2013-12-10 17:57:16 +02:00
|
|
|
* been fulfilled. The context of the callback will be the
|
|
|
|
* ChangesetRequest object, so you can check what you actually
|
|
|
|
* asked for.
|
2013-12-09 15:58:17 +02:00
|
|
|
*/
|
|
|
|
enqueue: function (start, granularity, callback) {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[changeset_loader] enqueue: %d, %d", start, granularity);
|
2013-12-08 20:16:53 +02:00
|
|
|
//TODO: check cache to see if we really need to fetch this
|
2013-12-22 18:21:54 +02:00
|
|
|
// maybe even do splices if we just need a smaller range
|
2013-12-08 20:16:53 +02:00
|
|
|
// in the middle
|
|
|
|
var queue = null;
|
2013-12-22 18:21:54 +02:00
|
|
|
for (var g in Revision.granularities) {
|
|
|
|
if (granularity == Revision.granularities[g]) {
|
|
|
|
queue = this.queues[g];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2013-12-05 18:40:41 +02:00
|
|
|
|
2013-12-12 19:52:34 +02:00
|
|
|
var request = new ChangesetRequest(start, granularity, callback);
|
|
|
|
if (! (request.getRequestID() in this.pending))
|
|
|
|
queue.push(request);
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
|
|
|
_run: function () {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[changesetloader] tick");
|
2013-12-12 13:43:33 +02:00
|
|
|
var _this = this;
|
|
|
|
function addToPending () {
|
|
|
|
_this.pending[request.getRequestID()] = request;
|
|
|
|
}
|
2013-12-08 20:16:53 +02:00
|
|
|
//TODO: pop an item from the queue and perform a request.
|
2013-12-12 13:43:33 +02:00
|
|
|
for (var q in this.queues) {
|
2013-12-09 15:58:17 +02:00
|
|
|
var queue = this.queues[q];
|
|
|
|
if (queue.length > 0) {
|
|
|
|
// TODO: pop and handle
|
|
|
|
var request = queue.pop();
|
2013-12-22 18:21:54 +02:00
|
|
|
if (request.getRequestID() in this.pending) {
|
|
|
|
//this request is already pending!
|
|
|
|
var id = request.getRequestID();
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("ALREADY PENDING REQUEST: %d, start: %d, granularity: %d", id, id & 0xffff, id >> 16);
|
2013-12-22 18:21:54 +02:00
|
|
|
continue;
|
|
|
|
}
|
2013-12-09 15:58:17 +02:00
|
|
|
//TODO: test AGAIN to make sure that it hasn't been retrieved and cached by
|
|
|
|
//a previous request. This should handle the case when two requests for the
|
|
|
|
//same changesets are enqueued (which would be fine, as at enqueue time, we
|
|
|
|
//only check the cache of AVAILABLE changesets, not the pending requests),
|
|
|
|
//the first one is fulfilled, and then we pop the second one, and don't
|
|
|
|
//need to perform a server request. Note: it might be worth changing enqueue
|
|
|
|
//to check the pending requests queue to avoid this situation entirely.
|
|
|
|
|
2013-12-10 17:57:16 +02:00
|
|
|
this.connection.sendMessage("CHANGESET_REQ", {
|
2013-12-09 15:58:17 +02:00
|
|
|
start: request.start,
|
|
|
|
granularity: request.granularity,
|
|
|
|
requestID: request.getRequestID(),
|
2013-12-12 13:43:33 +02:00
|
|
|
}, addToPending);
|
|
|
|
}
|
|
|
|
}
|
2013-12-09 15:58:17 +02:00
|
|
|
//TODO: this stop is just for debugging!!!!
|
|
|
|
//FIXME: remove when done testing
|
2013-12-12 16:52:30 +02:00
|
|
|
//this.stop();
|
2013-12-09 15:58:17 +02:00
|
|
|
},
|
|
|
|
on_response: function (data) {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[changesetloader] on_response: ", data);
|
2013-12-12 13:43:33 +02:00
|
|
|
if (!(data.requestID in this.pending)) {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[changesetloader] WTF? changeset not pending: ", data.requestID);
|
2013-12-09 15:58:17 +02:00
|
|
|
return;
|
|
|
|
}
|
2013-12-05 18:40:41 +02:00
|
|
|
|
2013-12-09 15:58:17 +02:00
|
|
|
// pop it from the pending list:
|
|
|
|
var request = this.pending[data.requestID];
|
|
|
|
delete this.pending[data.requestID];
|
|
|
|
//fulfill the request
|
|
|
|
request.fulfill(data);
|
2013-12-05 18:40:41 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
2013-12-10 17:57:16 +02:00
|
|
|
|
2013-12-20 14:12:48 +02:00
|
|
|
var libcssmanager = require("./cssmanager");
|
|
|
|
var linestylefilter = require("./linestylefilter").linestylefilter;
|
|
|
|
var libcolorutils = require('./colorutils').colorutils;
|
|
|
|
$.Class("Author",
|
|
|
|
{//static
|
|
|
|
},
|
|
|
|
{//instance
|
|
|
|
init: function (id, data, palette) {
|
|
|
|
this.id = id;
|
|
|
|
this.name = data.name;
|
|
|
|
this.is_anonymous = !this.name;
|
|
|
|
// if the colorId is an integer, it's an index into the color palette,
|
|
|
|
// otherwise we assume it is a valid css color string
|
|
|
|
this.background_color = typeof data.colorId == "number" ? palette[data.colorId] : data.colorId;
|
|
|
|
// foreground color should be black unless the luminosity of the
|
|
|
|
// background color is lower than 0.5. This effectively makes sure
|
|
|
|
// that the text is readable.
|
|
|
|
this.color = (libcolorutils.luminosity(libcolorutils.css2triple(this.background_color)) < 0.5 ? "#ffffff" : "#000000");
|
|
|
|
// generate a css class name for this author.
|
|
|
|
this.cssclass = linestylefilter.getAuthorClassName(this.id);
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Create and add a rule to the stylesheet setting the foreground and
|
|
|
|
* background colors for this authors cssclass. This class can then be
|
|
|
|
* applied to any span authored by this author, and the colors will just work.
|
|
|
|
* @param {object} cssmanager - A cssmanager wrapper for the stylesheet to
|
|
|
|
* which the rules should be added.
|
|
|
|
*/
|
|
|
|
addStyleRule: function (cssmanager) {
|
|
|
|
// retrieve a style selector for '.<authorid>' class, which is applied
|
|
|
|
// to blobs which were authored by that <authorid>.
|
|
|
|
var selector = cssmanager.selectorStyle("." + this.cssclass);
|
|
|
|
// apply the colors
|
|
|
|
selector.backgroundColor = this.background_color;
|
|
|
|
selector.color = this.color;
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Retrieve the name of this user.
|
|
|
|
*/
|
|
|
|
getName: function () {
|
|
|
|
return this.is_anonymous ? "anonymous" : this.name;
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Retrieve the cssclass for this user.
|
|
|
|
*/
|
|
|
|
getCSSClass: function () {
|
|
|
|
return this.cssclass;
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2013-12-10 17:57:16 +02:00
|
|
|
var AttribPool = require("./AttributePool");
|
|
|
|
var domline = require("./domline").domline;
|
|
|
|
$.Class("PadClient",
|
|
|
|
{//static
|
2013-12-17 17:32:26 +02:00
|
|
|
USE_COMPOSE: false,
|
2013-12-24 19:37:22 +02:00
|
|
|
VERBOSE: true,
|
2013-12-10 17:57:16 +02:00
|
|
|
},
|
|
|
|
{//instance
|
|
|
|
/**
|
|
|
|
* Create a PadClient.
|
|
|
|
* @constructor
|
2013-12-14 19:31:24 +02:00
|
|
|
* @param {RevisionCache} revisionCache - A RevisionCache object to use.
|
2013-12-20 14:12:48 +02:00
|
|
|
* @param {dict} options - All the necessary options. TODO: document this.
|
2013-12-10 17:57:16 +02:00
|
|
|
*/
|
2013-12-20 14:12:48 +02:00
|
|
|
init: function (revisionCache, options) {
|
2013-12-14 19:31:24 +02:00
|
|
|
this.revisionCache = revisionCache;
|
2013-12-20 14:12:48 +02:00
|
|
|
this.revision = this.revisionCache.getRevision(options.revnum);
|
|
|
|
this.timestamp = options.timestamp;
|
|
|
|
this.alines = libchangeset.splitAttributionLines(options.atext.attributes, options.atext.text);
|
|
|
|
this.apool = (new AttribPool()).fromJsonable(options.atext.apool);
|
|
|
|
this.lines = libchangeset.splitTextLines(options.atext.text);
|
|
|
|
this.authors = {};
|
|
|
|
this.dynamicCSS = libcssmanager.makeCSSManager('dynamicsyntax');
|
|
|
|
this.palette = options.palette;
|
2013-12-24 19:37:22 +02:00
|
|
|
this.log = PadClient.VERBOSE ? log : function () {};
|
2013-12-20 14:12:48 +02:00
|
|
|
|
|
|
|
this.updateAuthors(options.author_info);
|
2013-12-10 17:57:16 +02:00
|
|
|
|
|
|
|
//TODO: this is a kludge! we should receive the padcontent as an
|
|
|
|
//injected dependency
|
|
|
|
this.divs = [];
|
|
|
|
this.padcontent = $("#padcontent");
|
|
|
|
for (var i in this.lines) {
|
|
|
|
var div = this._getDivForLine(this.lines[i], this.alines[i]);
|
|
|
|
this.divs.push(div);
|
|
|
|
this.padcontent.append(div);
|
2013-12-12 13:43:33 +02:00
|
|
|
}
|
2013-12-10 17:57:16 +02:00
|
|
|
|
|
|
|
//TODO: monkey patch divs.splice to use our custom splice function
|
|
|
|
this.divs.original_splice = this.divs.splice;
|
2013-12-14 19:31:24 +02:00
|
|
|
var _this = this;
|
|
|
|
this.divs.splice = function () {
|
|
|
|
return _this._spliceDivs.apply(_this, arguments);
|
2013-12-16 13:59:07 +02:00
|
|
|
};
|
2013-12-14 19:31:24 +02:00
|
|
|
// we need to provide a get, as we want to give
|
|
|
|
// libchangeset the text of a div, not the div itself
|
|
|
|
this.divs.get = function (index) {
|
|
|
|
return this[index].data('text');
|
|
|
|
};
|
2013-12-10 17:57:16 +02:00
|
|
|
},
|
2013-12-14 19:31:24 +02:00
|
|
|
goToRevision: function (revnum, atRevision_callback) {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[padclient > goToRevision] revnum: %d", revnum);
|
2013-12-14 19:31:24 +02:00
|
|
|
var _this = this;
|
2013-12-16 13:59:07 +02:00
|
|
|
if (this.revision.revnum == revnum) {
|
|
|
|
if (atRevision_callback)
|
|
|
|
atRevision_callback.call(this, this.revision, this.timestamp);
|
|
|
|
return;
|
2013-12-16 14:31:48 +02:00
|
|
|
}
|
2013-12-16 13:59:07 +02:00
|
|
|
|
2013-12-14 19:31:24 +02:00
|
|
|
this.revisionCache.transition(this.revision.revnum, revnum, function (path) {
|
2013-12-24 19:30:46 +02:00
|
|
|
_this.log("[padclient > applyChangeset_callback] path:", path);
|
2013-12-14 19:31:24 +02:00
|
|
|
var time = _this.timestamp;
|
2013-12-17 17:32:26 +02:00
|
|
|
var p, changeset = null; //pre-declare, because they're used in both blocks.
|
2013-12-17 16:24:32 +02:00
|
|
|
if (PadClient.USE_COMPOSE) {
|
|
|
|
var composed = path[0];
|
|
|
|
var _path = path.slice(1);
|
2013-12-17 17:32:26 +02:00
|
|
|
for (p in _path) {
|
|
|
|
changeset = _path[p];
|
2013-12-17 16:24:32 +02:00
|
|
|
composed = composed.compose(changeset, _this);
|
|
|
|
}
|
|
|
|
composed.apply(_this);
|
|
|
|
time += composed.deltatime * 1000;
|
|
|
|
} else { // Don't compose, just apply
|
2013-12-17 17:32:26 +02:00
|
|
|
for (p in path) {
|
|
|
|
changeset = path[p];
|
2013-12-17 16:24:32 +02:00
|
|
|
time += changeset.deltatime * 1000;
|
2013-12-24 19:30:46 +02:00
|
|
|
//try {
|
|
|
|
_this.log("[transition] %d -> %d, changeset: %s", changeset.from_revision.revnum, changeset.to_revision.revnum, changeset.value);
|
2013-12-24 19:37:22 +02:00
|
|
|
_this.log(_this.alines, _this.lines, _this.apool);
|
2013-12-22 18:21:54 +02:00
|
|
|
changeset.apply(_this);
|
2013-12-24 19:30:46 +02:00
|
|
|
/*} catch (err) {
|
|
|
|
log("Error applying changeset: ");
|
|
|
|
log("\t", changeset.value);
|
|
|
|
log("\t %d -> %d ", changeset.from_revision.revnum, changeset.to_revision.revnum);
|
|
|
|
log(err);
|
|
|
|
log("--------------");
|
|
|
|
}*/
|
2013-12-17 16:24:32 +02:00
|
|
|
}
|
2013-12-14 19:31:24 +02:00
|
|
|
}
|
2013-12-10 17:57:16 +02:00
|
|
|
|
2013-12-14 19:31:24 +02:00
|
|
|
// set revision and timestamp
|
|
|
|
_this.revision = path.slice(-1)[0].to_revision;
|
|
|
|
_this.timestamp = time;
|
|
|
|
// fire the callback
|
2013-12-16 13:59:07 +02:00
|
|
|
if (atRevision_callback) {
|
2013-12-24 19:30:46 +02:00
|
|
|
_this.log("[padclient] about to call atRevision_callback", _this.revision, _this.timestamp);
|
2013-12-16 13:59:07 +02:00
|
|
|
atRevision_callback.call(_this, _this.revision, _this.timestamp);
|
|
|
|
}
|
2013-12-14 19:31:24 +02:00
|
|
|
});
|
2013-12-10 17:57:16 +02:00
|
|
|
},
|
2013-12-20 14:12:48 +02:00
|
|
|
/**
|
|
|
|
* Update the authors of this pad.
|
|
|
|
* @param {object} author_info - The author info object sent by the server
|
|
|
|
*/
|
|
|
|
updateAuthors: function (author_info) {
|
|
|
|
var authors = author_info;
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[updateAuthors]: ", authors);
|
2013-12-20 14:12:48 +02:00
|
|
|
for (var authorid in authors) {
|
|
|
|
if (authorid in this.authors) {
|
|
|
|
// just dispose of existing ones instead of trying to update existing
|
|
|
|
// objects.
|
|
|
|
delete this.authors[authorid];
|
|
|
|
}
|
|
|
|
var author = new Author(authorid, authors[authorid], this.palette);
|
|
|
|
this.authors[authorid] = author;
|
|
|
|
author.addStyleRule(this.dynamicCSS);
|
|
|
|
}
|
|
|
|
},
|
2013-12-21 19:25:25 +02:00
|
|
|
/**
|
|
|
|
* Merge a foreign (forward) changeset into our data. This involves rebuilding
|
|
|
|
* the forward changeset in our apool and building a reverse changeset.
|
|
|
|
* This is used to move new upstream changesets/revisions into our apool context.
|
|
|
|
* @param {string} changeset - The foreign changeset to merge
|
|
|
|
* @param {object} apool - The apool for that changeset
|
|
|
|
* @returns {object} - A values object with forward and reverse changesets.
|
|
|
|
*/
|
|
|
|
mergeForeignChangeset: function (changeset, apool) {
|
|
|
|
var values = {};
|
|
|
|
values.forward = libchangeset.moveOpsToNewPool(
|
|
|
|
changeset,
|
|
|
|
(new AttribPool()).fromJsonable(apool),
|
|
|
|
this.apool
|
|
|
|
);
|
|
|
|
var reverseValue = libchangeset.inverse(
|
|
|
|
changeset,
|
|
|
|
this.divs,
|
|
|
|
this.alines,
|
|
|
|
this.apool
|
|
|
|
);
|
|
|
|
values.reverse = libchangeset.moveOpsToNewPool(
|
|
|
|
reverseValue,
|
|
|
|
(new AttribPool()).fromJsonable(apool),
|
|
|
|
this.apool
|
|
|
|
);
|
|
|
|
return values;
|
|
|
|
},
|
2013-12-20 14:12:48 +02:00
|
|
|
/**
|
|
|
|
* Get a div jquery element for a given attributed text line.
|
|
|
|
* @param {string} text - The text content of the line.
|
|
|
|
* @param {string} atext - The attributes string.
|
|
|
|
* @return {jquery object} - The div element ready for insertion into the DOM.
|
|
|
|
*/
|
2013-12-10 17:57:16 +02:00
|
|
|
_getDivForLine: function (text, atext) {
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[_getDivsForLine] %s; %s", text, atext);
|
2013-12-10 17:57:16 +02:00
|
|
|
var dominfo = domline.createDomLine(text != '\n', true);
|
|
|
|
|
|
|
|
// Here begins the magic invocation:
|
|
|
|
linestylefilter.populateDomLine(text, atext, this.apool, dominfo);
|
|
|
|
dominfo.prepareForAdd();
|
|
|
|
|
2013-12-14 19:31:24 +02:00
|
|
|
var div = $("<div class='" + dominfo.node.className + "' " +
|
|
|
|
"id='" + Math.random() + "' " +
|
|
|
|
"data-text='" + text + "'>" +
|
2013-12-10 17:57:16 +02:00
|
|
|
dominfo.node.innerHTML + "</div>");
|
|
|
|
return div;
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* we need a customized splice function for our divs array, because we
|
|
|
|
* need to be able to:
|
|
|
|
* - remove elements from the DOM when they are spliced out
|
|
|
|
* - create a new div for line elements and add them to the array
|
|
|
|
* instead of the raw line
|
|
|
|
* - add the new divs to the DOM
|
|
|
|
* this function is fully compliant with the Array.prototype.splice
|
|
|
|
* spec, as we're monkey-patching it on to the divs array.
|
|
|
|
* @param {number} index - Index at which to start changing the array.
|
|
|
|
* @param {number} howMany - An integer indicating the number of old array elements to remove.
|
|
|
|
* @param {array} elements - The elements to add to the array. In our case, these are lines.
|
|
|
|
*/
|
|
|
|
_spliceDivs: function (index, howMany, elements) {
|
2013-12-14 19:31:24 +02:00
|
|
|
elements = Array.prototype.slice.call(arguments, 2);
|
2013-12-24 19:30:46 +02:00
|
|
|
this.log("[_spliceDivs]: ", index, howMany);
|
2013-12-10 17:57:16 +02:00
|
|
|
// remove howMany divs starting from index. We need to remove them from
|
|
|
|
// the DOM.
|
2013-12-14 19:31:24 +02:00
|
|
|
for (var i = index; i < index + howMany && i < this.divs.length; i++)
|
2013-12-12 13:43:33 +02:00
|
|
|
this.divs[i].remove();
|
2013-12-10 17:57:16 +02:00
|
|
|
|
|
|
|
// generate divs for the new elements:
|
|
|
|
var newdivs = [];
|
|
|
|
for (i in elements)
|
|
|
|
newdivs.push(this._getDivForLine(elements[i], this.alines[index + i]));
|
|
|
|
|
|
|
|
// if we are splicing at the beginning of the array, we need to prepend
|
|
|
|
// to the padcontent DOM element
|
|
|
|
if (!this.divs[index - 1])
|
|
|
|
this.padcontent.prepend(newdivs);
|
|
|
|
// otherwise just add the new divs after the index-th div
|
|
|
|
else
|
|
|
|
this.divs[index - 1].after(newdivs);
|
2013-12-22 18:41:39 +02:00
|
|
|
// super primitive scrollIntoView
|
2013-12-24 19:30:46 +02:00
|
|
|
if (newdivs.length) {
|
|
|
|
for(var x in newdivs){
|
|
|
|
var div = newdivs[x][0];
|
|
|
|
this.log("ND> ", div.id, div.className, div.innerHTML);
|
|
|
|
}
|
|
|
|
newdivs[0][0].scrollIntoView(false);
|
|
|
|
}
|
2013-12-10 17:57:16 +02:00
|
|
|
|
|
|
|
// perform the splice on our array itself
|
|
|
|
// TODO: monkey patching divs.splice, so use divs.original_splice or something
|
2013-12-14 19:31:24 +02:00
|
|
|
args = [index, howMany].concat(newdivs);
|
|
|
|
return this.divs.original_splice.apply(this.divs, args);
|
2013-12-10 17:57:16 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|